Skip to content

Optional Type Support in Kotlin/Native Generator#3

Open
RandVid wants to merge 7 commits into
mainfrom
optionals-impl
Open

Optional Type Support in Kotlin/Native Generator#3
RandVid wants to merge 7 commits into
mainfrom
optionals-impl

Conversation

@RandVid

@RandVid RandVid commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Kotlin natively supports nullable types (T?), so Swift optionals map cleanly to Kotlin nullable
types. The challenge is the C ABI layer between Swift and Kotlin/Native:

  • Optional parameters — a null pointer means the value is absent; a valid pointer means it is
    present.
  • Optional returns — the Swift thunk heap-allocates the result and returns a pointer, returning
    null if the Swift function returned nil. The Kotlin wrapper must free the pointer after copying.
  • String — requires its own handling because String is not a C-compatible type; it is passed
    and returned as a null-terminated C string (char*), so String? needs a dedicated nullable
    pointer strategy.

The work was delivered in two phases:

Phase Scope
1 Optional parameters: Int?, Int32?, Bool?, Double?, unsigned variants
2 Optional returns of the same types, plus String? as both parameter and return

Supported Types

Swift type Kotlin type Notes
Int? Long? 64-bit signed
Int32? Int? 32-bit signed
Int16? Short?
Int8? Byte?
UInt? ULong? 64-bit unsigned
UInt32? UInt?
UInt16? UShort?
UInt8? UByte?
Bool? Boolean?
Float? Float?
Double? Double?
String? String? Separate ABI — see §String

Optional<Array> and nested optionals (e.g., Int??) are not supported and are skipped with a
comment.


Phase 1: Optional Parameters

ABI

The shared FFM lowering (lowerOptionalParameter in
FFMSwift2JavaGenerator+FunctionLowering.swift) lowers a T? parameter where T has a 1:1 C
representation to UnsafePointer<T>? (nullable pointer). The C declaration is therefore:

// Swift: func fn(x: Int?) -> Void
void swiftjava_Module_fn(const ptrdiff_t* x);

A null pointer means the argument was absent; a valid pointer means the caller stored the value at
that address.

Swift thunk

The shared cdeclThunk path handles optional parameters correctly — no custom thunk is needed.
The generated thunk:

@_cdecl("swiftjava_Module_fn_x")
public func swiftjava_Module_fn_x(_ x: UnsafePointer<Int>?) {
    fn(x: x?.pointee)
}

Kotlin wrapper

The Kotlin call-site must pass a nullable pointer. The strategy differs by signedness:

Signed / floatcValuesOf(it) creates a single-element CValues<TVar> on the caller's
stack. The type is inferred from the value; no explicit Var name is needed:

fun fn(x: Long?): Unit {
  swiftjava_Module_fn_x(x?.let { cValuesOf(it) })
}

Unsigned and BooleancValuesOf does not have overloads for unsigned types or Boolean.
Instead, a single-element array is created and its address is taken via refTo(0):

fun acceptOptByte(b: UByte?): Unit {
  swiftjava_Module_acceptOptByte_b(b?.let { ubyteArrayOf(it).refTo(0) })
}

fun acceptOptBool(flag: Boolean?): Unit {
  swiftjava_Module_acceptOptBool_flag(flag?.let { booleanArrayOf(it).refTo(0) })
}

The helper callArgForOptional(_:value:) in KotlinNativeSwift2KotlinGenerator.swift encodes
this mapping:

func callArgForOptional(_ inner: KotlinType, value: String) -> String {
  switch inner {
  case .long, .int, .short, .byte, .float, .double:
    return "cValuesOf(\(value))"
  case .uLong:  return "ulongArrayOf(\(value)).refTo(0)"
  case .uInt:   return "uintArrayOf(\(value)).refTo(0)"
  case .uShort: return "ushortArrayOf(\(value)).refTo(0)"
  case .uByte:  return "ubyteArrayOf(\(value)).refTo(0)"
  case .boolean: return "booleanArrayOf(\(value)).refTo(0)"
  default: return "cValuesOf(\(value))"
  }
}

Phase 2: Optional Returns (Numeric / Boolean / Float)

Why CDeclLowering is not used for optional returns

lowerResult in FFMSwift2JavaGenerator+FunctionLowering.swift explicitly rejects optional
returns:

case .optional:
  throw LoweringError.unhandledType(type)

This is a Java/FFM limitation: Java has no nullable scalar types. Kotlin/Native does, so the
Kotlin/Native generator implements its own return strategy.

ABI

The Swift thunk heap-allocates a value of type T and returns a pointer to it. A null pointer
means the Swift function returned nil. The caller (Kotlin) must free the pointer after copying
the value.

// Swift: func maybeInt() -> Int?
ptrdiff_t* swiftjava_Module_maybeInt(void);

CType (via CRepresentation.swift:34) unwraps UnsafeMutablePointer<T>? to T* in C, so
the C header declaration is identical to any other nullable pointer.

Swift thunk

optionalReturningThunk in KotlinNativeSwift2KotlinGenerator+SwiftThunkPrinting.swift generates
the heap-allocation pattern. The type name is derived directly from the nominal type declaration
(nom.nominalTypeDecl.name) — no hardcoded mapping table is needed:

@_cdecl("swiftjava_Module_maybeInt")
public func swiftjava_Module_maybeInt() -> UnsafeMutablePointer<Int>? {
    guard let _result: Int = maybeInt() else { return nil }
    let _ptr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
    _ptr.initialize(to: _result)
    return _ptr
}

CDeclLowering signature stripping

Because lowerFunctionSignature throws on optional return types, resolve() and
writeSwiftThunkSources both detect an optional return and lower a stripped signature (with the
wrapped, non-optional type as the return) before building the custom CFunction manually:

// In resolve() and writeSwiftThunkSources():
if let wrapped = optionalReturnWrapped, case .optional = ktReturn {
  strippedResult = SwiftResult(convention: ..., type: wrapped)  // e.g. Int, not Int?
}
let sigForLowering = SwiftFunctionSignature(... result: strippedResult ...)
let lowered = try CdeclLowering(...).lowerFunctionSignature(sigForLowering)
// Then build custom CFunction with UnsafeMutablePointer<T>? return type.

Kotlin wrapper

fun maybeInt(): Long? {
  val ptr = swiftjava_Module_maybeInt() ?: return null
  val result = ptr.pointed.value
  free(ptr)
  return result
}

ptr.pointed.value dereferences the heap pointer to obtain the scalar value. return null and
return result are non-local returns, valid here because any enclosing usePinned or memScoped
lambdas are inline.


String Optionals

Why String needs separate handling

String is not a C-compatible type. The existing non-optional String ABI passes strings as
heap-allocated null-terminated C strings (char*):

  • Parameter: UnsafePointer<Int8> in the thunk; Kotlin passes .cstr.
  • Return: UnsafeMutablePointer<Int8> / UnsafeMutablePointer<CChar> in the thunk; the
    caller owns the allocation and must free it.

lowerOptionalParameter in FFMSwift2JavaGenerator+FunctionLowering.swift explicitly throws for
String?:

case .void, .string:
  throw LoweringError.unhandledType(knownTypes.optionalSugar(wrappedType))

And the general optional parameter path would produce UnsafeRawPointer? for non-C types (not
UnsafePointer<Int8>?). Both make the standard lowering path unusable for String?.

Signature stripping for String? parameters

Like optional returns, String? parameters are stripped (replaced with non-optional String)
before CDeclLowering is called. This makes the lowering succeed and produces
UnsafePointer<Int8> (non-optional) in the lowered signature:

let strippedParameters = decl.functionSignature.parameters.map { p -> SwiftParameter in
  guard swiftTypeToKotlin(p.type) == .optional(.string) else { return p }
  return SwiftParameter(convention: p.convention, argumentLabel: p.argumentLabel,
                        parameterName: p.parameterName, type: knownTypes.string)
}

The C header then shows const int8_t* — which Kotlin/Native cinterop treats as a nullable
CValuesRef<ByteVar>?, allowing null to be passed.

String? as a parameter — Kotlin wrapper

The call-site uses Kotlin's safe-call operator on .cstr. When s is null, s?.cstr evaluates
to null, which the cinterop layer maps to a null pointer:

fun acceptOptString(s: String?): Unit {
  swiftjava_Module_acceptOptString_s(s?.cstr)
}

String? as a parameter — Swift thunk

stringOptionalAwareThunk generates the thunk. The parameter is declared as
UnsafePointer<Int8>? (optional pointer) and converted via .map { String(cString: $0) }:

@_cdecl("swiftjava_Module_acceptOptString_s")
public func swiftjava_Module_acceptOptString_s(_ s: UnsafePointer<Int8>?) {
    acceptOptString(s: s.map { String(cString: $0) })
}

String? as a return type — ABI

The ABI mirrors non-optional String: the thunk returns a heap-allocated null-terminated C string
(char*), with null meaning the Swift function returned nil. The C declaration is:

int8_t* swiftjava_Module_maybeName(void);

For String? returns, the stripped signature (with String as the return type) already produces
the right int8_t* C type via CDeclLowering, so no custom CFunction construction is needed —
the lowered.cdeclSignature is used directly (same as the default branch).

String? as a return type — Swift thunk

stringOptionalAwareThunk uses _swiftjava_stringToCString (from SwiftRuntimeFunctions) for
the heap allocation, avoiding a duplicated allocation loop:

@_cdecl("swiftjava_Module_maybeName")
public func swiftjava_Module_maybeName() -> UnsafeMutablePointer<CChar>? {
    guard let _result: String = maybeName() else { return nil }
    return _swiftjava_stringToCString(_result)
}

String? as a return type — Kotlin wrapper

fun maybeName(): String? {
  val ptr = swiftjava_Module_maybeName() ?: return null
  val result = ptr.toKString()
  free(ptr)
  return result
}

This is identical to a non-optional String return except ?: return null replaces ?: return "".

Combined: String? param + String? return

stringOptionalAwareThunk handles this combination in a single pass:

@_cdecl("swiftjava_Module_maybeUpper_s")
public func swiftjava_Module_maybeUpper_s(_ s: UnsafePointer<Int8>?) -> UnsafeMutablePointer<CChar>? {
    guard let _result: String = maybeUpper(s: s.map { String(cString: $0) }) else { return nil }
    return _swiftjava_stringToCString(_result)
}
fun maybeUpper(s: String?): String? {
  val ptr = swiftjava_Module_maybeUpper_s(s?.cstr) ?: return null
  val result = ptr.toKString()
  free(ptr)
  return result
}

Ilya Plisko 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.
@@ -0,0 +1,143 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once again :)

Ilya Plisko added 4 commits June 18, 2026 13:49
- Extend Kotlin/Native generator to handle optional parameters (`T?`) and returns, including memory management for optional pointer values.
- Add support for mapping optional types in `KotlinType` and logic for parameter and return thunk generation.
- Update integration and unit tests for functions with optional parameters, returns, and combinations of both.
- Refactor `CDeclLowering` to correctly handle stripped (non-optional) signatures for optional return types.
…lin/Native

- Extend generator to handle `String?` parameters and returns, including memory management for nullable C string pointers.
- Add thunk generation logic for `UnsafePointer<Int8>?` parameters and `String?` return conversions.
- Update integration and unit tests to cover optional `String` interop cases (`String?` params, returns, and combinations).
- Refactor existing lowering logic to strip optional `String?` types for correct C declaration processing.
…nal `String?` support

- Consolidate handling of optional `String?` parameters and return types.
- Introduce utility functions (`isStringType`, `isVoidType`, `isOptionalString`, etc.) for streamlined type checks.
- Simplify thunk generation by removing redundant logic and employing new utility functions.
- Update tests to reflect changes in import management and `String?` interop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants