Repository navigation
JNI: return main-actor and async closures to Java - #28
Open
piercifani wants to merge 2 commits into
Open
piercifani wants to merge 2 commits into
piercifani wants to merge 2 commits into
Conversation
A closure Java reads back, such as a closure property's getter, was skipped when it was `@MainActor`, `async` or `throws`: Java calls it synchronously, from any thread. View models hand their UI callbacks of exactly these shapes. - A `@MainActor` closure is wrapped by `_makeMainActorSwiftClosure`, which calls it under `MainActor.assumeIsolated`. Java has to call it on the main thread, as Compose does with a view model's callbacks; any other thread traps, as calling a `@MainActor` function from Java does. - An `async` closure, main-actor isolated or not and throwing or not, returns a `CompletableFuture`, which its Java interface already declares for closures Java implements. `_swiftClosureFuture` converts the arguments on the calling thread, awaits the closure in a task and completes the future with the result, or with the error as a `Throwable`. A synchronous throwing closure is still skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 24/24 · base:
jni/23-foundation-java-types· previous: #27Problem
#21 lets Java read a closure back from Swift, such as the getter of a closure property, as a Java lambda over a
SwiftClosurehandle. Java calls that lambda synchronously and from any thread, so closures that can't be called that way were skipped:@MainActorclosures;asyncclosures;throwsclosures.These are exactly the callbacks a view model hands its UI:
var onTap: @MainActor () -> Void,var onSelect: @MainActor (Item) -> Void,var save: @MainActor (String) async throws -> Void.Changes
@MainActorclosures go through a new_makeMainActorSwiftClosure, which calls them underMainActor.assumeIsolated. Java has to call them on the main thread, as Compose calls a view model's callbacks. Any other thread traps, the same rule as calling a@MainActorfunction from Java (Samples: Swift 6 JNI sample used from Kotlin, with main-actor view models (on swiftlang#938) #11).asyncclosures, main-actor isolated or not, throwing or not, return aCompletableFuture. That's what their Java interface already declares for closures Java implements (JNI: async closures implemented in Java return a CompletableFuture #22). A new_swiftClosureFuture:Throwable(JNI: Swift Error values cross as Java Throwables #25).throwsclosures are still skipped. A synchronous Java call has no way to receive the error.Not included: cancelling the returned future doesn't cancel the task, unlike async functions (#10).
Kotlin in this PR
ScreenCallbacksTest.ktwithScreenCallbacks.swift:Why Skip needs it
Getters of closure properties were the largest remaining cause of skipped declarations in a production Skip Fuse app (9 Skip modules) run through this stack: 420 of 930, 405 of them in its view-model module. Compose calls these callbacks from the UI thread.
Testing
./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:testpasses (56 tests, including 2 inScreenCallbacksTest).JNIClosureResultsTests: the@MainActorandasync throwsclosure thunks. A synchronous throwing closure is still skipped; that test replaces the one asserting@MainActorclosures were skipped.swift testpasses.🤖 Generated with Claude Code