Skip to content

JNI: optionalsAsNullable passes optionals as nullable Java types - #19

Open
piercifani wants to merge 3 commits into
jni/14-default-argumentsfrom
jni/15-nullable-optionals
Open

piercifani wants to merge 3 commits into
jni/14-default-argumentsfrom
jni/15-nullable-optionals

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 15/21 · base: jni/14-default-arguments · previous: #18 · next: #20 (jni/16-closure-struct-arguments)

Problem

Swift optionals reach Java as Optional<T>, OptionalLong, OptionalInt or OptionalDouble. Kotlin can't use them as T?:

  • val nick: String? = Greeter.nickname(id) doesn't compile.
  • Passing null for nil isn't possible.
  • Every call site has to wrap and unwrap the value.

Changes

A new configuration option, optionalsAsNullable (default false), gives a declaration's optional parameters and results the wrapped Java type, boxed, with null meaning nil: String, java.lang.Long, Tag. It's opt-in because it changes the Java API of every optional.

It's a thin Java-side layer over the existing optional conversions, so native signatures and the Swift thunks don't change:

  • Parameter: the existing conversion gets the Optional it expects, built from the nullable argument: Optional.ofNullable(tag) or (count == null ? OptionalLong.empty() : OptionalLong.of(count)).
  • Result: the existing conversion's Optional goes into a typed local, Optional<String> result$optional = …, and the method returns result$optional.isPresent() ? result$optional.get() : null, or getAsLong() and friends for primitives.

It's applied once per declaration, in translate(_ decl:), to that declaration's own parameters and result. This covers functions, initializers, and property getters and setters.

Nested signatures keep Optional, because their Swift side passes an Optional through JNI. That applies to closure parameters and results, and to the element types of arrays and other collections. Enum getAsX() accessors keep returning Optional<Case.X>; their printer now wraps the nullable associated values with Optional.ofNullable before mapping them to the case record.

Kotlin sees these as platform types (String!), which can be used as String?. Real String? would need a nullability annotation Kotlin recognizes, which needs a dependency; that's left for later.

Kotlin in this PR

NullablesTest.kt with Quotes.swift; the library module turns the option on:

val quote: Quote? = Quotes.last("a")
assertEquals(1.5, quote?.price)              // Double? property
assertNull(Quotes.volume("b"))               // Int64? result
assertEquals("- 3", Quotes.describe(null, 3L))   // Double? and Int64? parameters
quote.note = null                            // String? setter

Turning the option on in the module changes one earlier test: JavaEnumsTest now reads Orders.status("a") instead of Orders.status("a").get(). ClosuresTest is unchanged, since a closure's optional argument is still an Optional.

Why Skip needs it

Skip turns Swift optionals into Kotlin nullable types, and the app's Kotlin code uses ?., ?: and null with them throughout. With Optional, every one of those call sites would change.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (42 tests).
  • New JNINullableOptionalsTests (5 tests): a primitive parameter and a String result, with the unchanged native method; a primitive result; jextracted parameter and result; an escaping closure parameter keeps OptionalLong; the default output without the option.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox (673 tests) and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test (293 tests) pass. The JNI sample doesn't turn the option on.
  • swift run generate-config-docs --check (the option is documented); swift format lint --strict on changed files; git diff --check.

Not in this PR

  • Nullability annotations, so Kotlin sees String? instead of String!.
  • Nullable closure parameters and results, and nullable collection elements.
  • FFM mode.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 18:35
With the new `optionalsAsNullable` option, a declaration's optional
parameters and results use the wrapped Java type, boxed, with `null` for
`nil`, instead of `Optional`/`OptionalLong`/`OptionalInt`/`OptionalDouble`.

It's a Java-side layer over the existing conversions: a parameter builds
the `Optional` they expect from the nullable argument, and a result
unwraps their `Optional` once into a typed local. Native methods and
Swift thunks are unchanged. Applied in `translate(_ decl:)` to the
declaration's own optionals only; closure and collection element types
keep `Optional`, and enum `getAsX()` wraps nullable associated values
before mapping them. Off by default: it changes the Java API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant