UByte Array Implementation#2
Open
RandVid wants to merge 3 commits into
Open
Conversation
added 3 commits
June 18, 2026 13:36
…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.
…ative
- Extend Kotlin/Native generator to handle `[UInt8]` as `UByteArray`, with automatic parameter pinning.
- Introduce `usePinned { }` blocks for safe array memory management during native calls.
- Update generator to support nested `usePinned` for multiple `UByteArray` parameters.
- Add integration tests and demo examples to validate correct handling of `[UInt8]` parameters with various return types (`Unit`, `Int`, `String`, etc.).
- Enhance `KotlinType` mapping to include `UByteArray`.
- Extend unit tests for Kotlin/Native interop with `[UInt8]`.
- Update Kotlin/Native generator to handle `[UInt8]` return types with a KN-specific ABI. - Enhance handling of heap-allocated pointers (`uint8_t*`) and proper memory management using `free()`. - Add new demo functions for `[UInt8]` return and mixed `[UInt8]` parameters and returns. - Extend integration and unit tests to validate `[UInt8]` interop in cinterop, thunks, and wrappers. - Refactor generator logic to streamline array handling and support cases like `[UInt8]` return lengths.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.