UByte Array Implementation#2
Conversation
…d type safety and clarity - Replace `String`-based type mapping with `KotlinType` enum for consistency across primitive and reference types. - Update `swiftTypeToKotlin` function to return `KotlinType?` and adjust type mapping logic accordingly. - Introduce `KotlinType.swift` to define the `KotlinType` enum, associated cases, and descriptions. - Refactor call argument handling to leverage `KotlinType` cases (e.g., `.string`, `.unit`). - Update code to switch on `KotlinType` enum instead of raw strings for type checks. - Ensure proper handling of `String` and other specific types with the new enum.
- Extend `swiftTypeToKotlin` and `KotlinType` to map `[UInt8]` to `UByteArray`. - Add struct `Pinning` to encapsulate pinning information for parameters requiring `usePinned`.
mMaxy
left a comment
There was a problem hiding this comment.
I would love for the generator to be rewritten. Current implementation is strange to say the least.
But I think we will have to plan a normal architectural design of the generator somewhere closer to Autumn.
And the second thought - I would love to not merge this as we don't need the arrays as an implementation ATM - and the latest design session demonstrated that they are much more complicated then this solution proposes. But there are different refactorings around types mixed with the implementation of a feature. It would be great if you were able to separate PRs with refactorings for future needs from the PRs with an actual implementation of a feature.
Shipping this just because you already built on top of it
Overview
The Kotlin/Native generator (
KotlinNativeSwift2KotlinGenerator) emits three artifacts from thesame resolved model:
<Module>.kt, functions the app calls.<Module>.h, plain-C declarations consumed by the cinterop.deffile.<Module>Module+SwiftJava.swift,@_cdeclfunctions compiled into theSwift dynamic library.
All three artifacts must agree on the C ABI.
[UInt8]required custom handling in each artifactbecause the FFM and Kotlin/Native ABIs differ for array types.
Why a Custom ABI for Arrays
The FFM generator uses a callback ABI for array returns:
Swift calls back into Java with a temporary pointer via the function pointer. This works with
Linker.upcallStub()in Panama (Java FFM), which creates a C function pointer backed by a JVMtrampoline with embedded context.
Kotlin/Native has no equivalent. Its
staticCFunctionproduces a compile-time static symbol withno closure context — it cannot capture local variables. Passing a
staticCFunctionthat writesto a stack variable would be undefined behaviour.
Instead, Kotlin/Native uses the heap-pointer + out-count ABI:
Swift heap-allocates the array, writes the count through the pointer, and returns the base address.
The caller frees it after copying.
[UInt8]as a ParameterType mapping
swiftTypeToKotlinmaps[UInt8](Swift) →.array(.uByte)(Kotlin):The string-fallback path also handles
"[UInt8]"and"[Swift.UInt8]"directly.C ABI lowering
[UInt8]parameters are lowered to a(const void*, ptrdiff_t)pair by the sharedCdeclLoweringmachinery — the same lowering FFM uses. The C header declaration looks like:Kotlin wrapper —
usePinnedKotlin/Native's GC can move heap objects. Before passing a
UByteArray's address to C, the arraymust be pinned (address-stabilised).
usePinnedis a Kotlin/Nativeinlinefunction that pinsfor the duration of its lambda:
pinned_data.addressOf(0)passes the base address;data.size.toLong()passes the element countas
ptrdiff_t.For multiple array parameters the blocks nest:
Only the outermost
usePinnedcarries areturnprefix (for non-Unitreturns) becauseusePinnedisinline— the lambda's last expression propagates out to the function.Swift thunk — standard
cdeclThunkArray parameters use the standard FFM
cdeclThunkpath (no special handling needed on the Swiftside). The lowered
@_cdeclthunk reconstructs the[UInt8]from the raw pointer and count:[UInt8]as a Return TypeKotlin wrapper —
memScoped+ out-countThe KN heap-pointer ABI requires a stack-allocated variable to receive the count.
memScopedisa Kotlin/Native
inlinefunction that provides aMemScopefor stack allocation viaalloc<T>().Because it is
inline, a non-localreturnis valid inside its lambda.Steps:
alloc<LongVar>()— stack-allocates the out-count (ptrdiff_t*, mapped toLongVarinKotlin/Native cinterop on 64-bit).
countVar.ptr; returnsnullif the array is empty → early return ofUByteArray(0).countVar.value.convert<Int>()— reads the written count.ptr.reinterpret<ByteVar>().readBytes(count).asUByteArray()— copies bytes into a managedUByteArray.free(ptr)— releases the heap allocation; needsimport platform.posix.free(not inkotlinx.cinterop).When array parameters are also present,
memScopednests inside the innermostusePinnedblock. In that case the last expression of
memScoped(result) propagates out through eachusePinnedlambda to the enclosingreturn:C header declaration
resolve()builds a customCFunctionfor array-returning functions instead of delegatingto
CdeclLowering.cdeclSignature(which would produce the FFM callback ABI). It manuallyconstructs the KN-specific signature:
The resulting C header entry is:
Swift thunk —
arrayReturningThunkwriteSwiftThunkSourcesdetects.array(.uByte)returns and callsarrayReturningThunkinsteadof the shared
cdeclThunk. The generated thunk heap-allocates and copies the Swift array:The caller (
free(ptr)in Kotlin) releases this allocation.