Repository navigation
JNI: closures returned to Java, and optional closure parameters - #21
Open
piercifani wants to merge 3 commits into
Open
piercifani wants to merge 3 commits into
piercifani wants to merge 3 commits into
Conversation
A function returning a closure, or a property of closure type, was skipped. The native method now returns the handle of a Swift closure box, and the Java method wraps it in a `SwiftClosure` (new in SwiftKitCore, released by a cleaner when unreachable) and returns a lambda of the closure's functional interface, `<Type>.<method>.result`. The lambda calls `SwiftClosure.call` with boxed arguments; the box converts them, and the result, with the `JobjectBridge`s collections already use. Async, throwing and `@MainActor` closures stay skipped: Java calls the closure synchronously, on any thread. An optional closure parameter is `Optional<Interface>` (or the nullable interface with `optionalsAsNullable`) and lowers to `nil` or the escaping closure. A closure property's getter returns its setter's interface, `<Type>.set<Property>.newValue`, so Kotlin sees one property and can assign it a lambda. Supporting changes: - `(T)` is parsed as `T`, not a one-element tuple, so `((Item) -> Void)?` is an optional closure. - wrap-java's `…Optional` convenience overloads call `self.method(…)`: jextract's helper class of the same name made the bare call ambiguous. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
…oads Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
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.
Stack 17/21 · base:
jni/16-closure-struct-arguments· previous: #20 · next: #22 (jni/18-async-closures)Problem
jextract skipped closures going from Swift to Java:
func makeFormatter(prefix: String) -> (Item) -> Stringhad no Java method.public var onItem: ((Item) -> Void)?had neither getter nor setter. Optional closure parameters weren't supported, and the parentheses in((Item) -> Void)?made it a one-element tuple.Changes
Returned closures. The native method returns the handle of a Swift box holding the closure. The Java method wraps the handle in a
SwiftClosureand returns a lambda of the closure's functional interface:<Type>.<method>.result, in the method's helper class.SwiftClosure.call(Object...)passes boxed arguments to the box. The box converts them, and the closure's result, with theJobjectBridges that arrays and dictionaries use. So the closure's types can be anything an array element can be: primitives, strings, jextracted types, optionals and collections.SwiftClosure(new in SwiftKitCore) releases the Swift closure when it becomes unreachable. It uses a cleaner thread, like the automatic arena, so it works on Android.@MainActorclosures stay skipped. Java calls the closure synchronously, on any thread.Optional closure parameters. They become
Optional<Interface>, or the nullable interface withoptionalsAsNullable. In Swift they lower tonilor the escaping closure.Closure properties. A getter returns its setter's interface,
<Type>.set<Property>.newValue, so Kotlin sees a single property. A read-only property's getter gets its own<Type>.get<Property>.result.Supporting changes:
(T)is now parsed asT, as Swift does, instead of a one-element tuple.…Optionalconvenience overloads callself.method(…). jextract's helper classsetOnItembecame a Swift nested type of the same name, which made the bare callsetOnItem(…)ambiguous.Kotlin in this PR
ClosuresTest.ktwithClosures.swift:Why Skip needs it
Skip bridges closure properties and closure results as Kotlin function types, which Kotlin code assigns lambdas to and calls. A production Skip Fuse app (16 Skip modules) has about 420 closure properties and closure results.
Testing
./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:testpasses (45 tests).JNIClosureResultsTests(7 tests):resultinterface;Optionaland withoptionalsAsNullable, and its helper class printed once;nilor the closure for an optional closure parameter;@MainActorclosure result being skipped.ClosureResultsTestcalls a returned(Int64) -> Int641,000 times and a returned struct closure. It sets, reads, calls and clears an optional closure property. It drops 100 returned closures and waits for their Swift captures to be released.swift test --disable-experimental-prebuilts --disable-sandbox(682 tests),./gradlew :Samples:SwiftJavaExtractJNISampleApp:test(298 tests) and./gradlew :SwiftKitCore:testpass.swift format lint --stricton changed files;git diff --check.Not in this PR
@MainActorclosures returned to Java.CompletableFuturethat Swift awaits.🤖 Generated with Claude Code