Skip to content

JNI: arrays of optionals, dictionaries and sets - #16

Open
piercifani wants to merge 2 commits into
jni/11-async-throwing-streamfrom
jni/12-optional-nested-arrays
Open

piercifani wants to merge 2 commits into
jni/11-async-throwing-streamfrom
jni/12-optional-nested-arrays

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 12/21 · base: jni/11-async-throwing-stream · previous: #15 · next: #17 (jni/13-java-enums)

Problem

JNI arrays are converted eagerly, and only primitive, String, jextracted-class and nested-array elements were handled. Any other element type made jextract skip the declaration, including:

  • [Tag?], an array of optionals, which failed with unsupportedSwiftType(Tag?);
  • [[String: Int64]], an array of dictionaries, which failed with unsupportedSwiftType([String: Int64]).

Dictionaries and sets didn't have this problem, because they convert their keys and values through bridges. [String: [Tag]] and [Tag]? already worked.

Changes

Arrays whose elements are optionals, dictionaries or sets now go through the existing ArrayBridge, which converts element by element with the same bridges dictionaries use.

  • Swift thunk: ArrayBridge<…>.toJavaObject(result, in: environment) for results and ArrayBridge<…>.fromJavaObject(array, in: environment) for parameters.
  • Java: the public method is typed: Optional<Tag>[], SwiftDictionaryMap<String, Long>[], SwiftSet<String>[]. The native method takes and returns Object[], the array's static type on the Swift side. The wrapper casts the result with a new .cast conversion step, which renders (T[]) $f().
  • Element types: OptionalBridge always creates a java.util.Optional, so an optional primitive element is Optional<Long>, not OptionalLong. bridgedArrayElementJavaType computes that type.

Kotlin in this PR

CollectionsTest.kt with Collections.swift:

Watchlist.quotes().map { it.getOrNull()?.id }   // [Item?] → listOf("a", null, "c")
Watchlist.pages()[1]["c"]                        // [[String: Int64]] → 3
Watchlist.total(arrayOf(Optional.of(1L), Optional.empty(), Optional.of(3L)))  // [Int64?] parameter

Why Skip needs it

A production Skip Fuse app (16 Skip modules) bridges 2 signatures with [T?] and 19 with nested collections. This PR fixes those whose outer collection is an array; dictionaries and sets of collections, and [T]? (85 signatures), already work on this branch. With strict skipped-declaration checks, each unsupported one fails the build. Without the checks, Kotlin silently loses those members.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (33 tests).
  • New JNIBridgedArrayElementsTests (4 tests): the Java signature, cast and native Object[] signature; the Swift ArrayBridge calls for results and parameters; dictionary elements; jextracted optional elements.
  • JNI sample ArraysTest: [Int64?] and [MySwiftClass?] round trips, [[String: Int64]] and [Set<String>] results.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox (658 tests) and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test (292 tests) pass.
  • JNISkippedDeclarationsTests (from JExtract: report every skipped declaration; add failOnSkippedDeclarations #3) used [Tag?] and [[String: Int64]] as its unsupported examples; it now uses arrays of tuples and of closures.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • Arrays of other element types without a direct mapping (tuples, closures, Date, UUID).
  • Kotlin-friendly nullability (Item? instead of Optional<Item>) is a separate gap.
  • Arrays returned from Java-implemented protocol requirements still use the earlier array rules.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 17:35
Arrays were only converted for primitive, String, jextracted-class and
nested-array elements; `[Tag?]` and `[[String: Int64]]` were skipped.

Such arrays now use the existing `ArrayBridge`, which converts element by
element with the bridges dictionaries already use. The public Java method
is typed (`Optional<Tag>[]`, `SwiftDictionaryMap<String, Long>[]`); the
native method passes `Object[]` and the wrapper casts with a new `.cast`
conversion step. Optional primitive elements are `Optional<Long>`, the
class `OptionalBridge` creates.

The skipped-declarations test used these two shapes as its unsupported
examples; it now uses arrays of tuples and of closures.

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