Repository navigation
JNI: Java implementations of async and cross-module Swift protocols - #7
Open
piercifani wants to merge 2 commits into
Open
piercifani wants to merge 2 commits into
piercifani wants to merge 2 commits into
Conversation
This was referenced Nov 3, 2026
piercifani
force-pushed
the
jni/06-java-implemented-protocols
branch
from
November 3, 2026 14:41
5306df3 to
a5d5420
Compare
piercifani
force-pushed
the
jni/06-java-implemented-protocols
branch
from
November 3, 2026 15:19
a5d5420 to
fb8294c
Compare
piercifani
force-pushed
the
jni/06-java-implemented-protocols
branch
from
November 3, 2026 15:56
fb8294c to
9272c4d
Compare
With `enableJavaCallbacks`, Java classes can implement Swift protocols. Make that work for `async` requirements, protocols declared in another module, and Swift 6 language mode. - Await Java futures without blocking: SwiftKitCore's `CompletableFutureContinuation` resumes a Swift continuation from `whenComplete`; `_awaitCompletableFuture` suspends on it, cancels the future when the task is cancelled, and re-wraps results with the awaiting thread's environment. Generated wrappers for `async` requirements call the Java method dynamically (wrap-java doesn't bind generic `CompletableFuture<T>` methods) and await the future. Unsupported results and `legacyFuture` mode skip the wrapper with a warning. - Protocols from other modules: the `SwiftJava…Wrapper` protocol and its default implementations are public; a module accepts Java implementations of protocols from dependencies extracted with Java callbacks; generic bounds are printed fully qualified. - Swift 6: `defaultAutoArena` is a constant; `Throwable` is `@unchecked Sendable` at the root so generated exception subclasses compile. - Each wrapper method binds the Java interface to the calling thread's JNIEnv; wrapper classes are `@unchecked Sendable`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The protocol (with an async requirement) and an Error enum live in KotlinSampleModels, extracted with enableJavaCallbacks; KotlinSampleLibrary takes the protocol as a parameter. Kotlin tests cover sync, struct-returning and async requirements, an async failure, and cancellation of the Java future when the Swift task is cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
piercifani
force-pushed
the
jni/06-java-implemented-protocols
branch
from
November 3, 2026 16:46
9272c4d to
0bb9eb3
Compare
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 6/21 · base:
jni/05-codegen-fixes· previous: #6 · next: #8With
enableJavaCallbacks, Java classes can implement Swift protocols and be passed into Swift. This PR makes that work forasyncrequirements, for protocols declared in another module, and in Swift 6 language mode.Problems
asyncrequirements didn't compile. jextract gives the Java interfaceCompletableFuture<T> fetch(...), but the Swift wrapper called it synchronously as if it returnedT, and wrap-java doesn't bind the genericCompletableFuture<T>method at all (value of type 'JavaProfileClient' has no member 'fetch').any ProfileClientonly accepted Swift instances (a Java implementation hitfatalError("…typeMetadata$ memory address was null")), and the Java generic bound was printed unqualified (<_T0 extends ProfileClient>), so the Java didn't compile.JavaSwiftArena.defaultAutoArenawas a mutable static (not concurrency-safe), and wrap-java's classes for Swift error enums (open class JavaClientError: Exception) failednon-final class cannot conform to 'Sendable', becauseThrowable: ErrorimpliesSendable.JNIEnvof the thread that created it, but Swift calls requirements from any thread, always so forasyncones.Changes
Awaiting a Java future without blocking
CompletableFutureContinuation.resumeWhenComplete(future, continuation)resumes a Swift continuation fromwhenComplete, unwrappingCompletionException._awaitCompletableFuture(_:)suspends on a checked continuation, so no thread is blocked while Java works. Cancelling the Swift task cancels the future. Results and exceptions cross threads as global references and are re-wrapped with the awaiting thread's environment.asyncrequirement: call the Java method dynamically (wrap-java doesn't bind it) to get theCompletableFuture, await it, then convert the value. Supported results areVoid, directly translated types (unboxed throughJavaBoxable) and jextracted types. Other results, orasyncFuncMode: legacyFuture(aFuturecan't notify), skip the wrapper with a warning.Protocols from other modules
SwiftJava…Wrapperprotocol and its default implementations arepublic.enableJavaCallbacks(from its--depends-onconfig). It builds the per-parameter wrapper class from the protocol's names.Swift 6 and threading
defaultAutoArenais anonisolated(unsafe) static let; it was never reassigned.extension Throwable: @unchecked Sendable {}at the root, so generated exception subclasses compile in Swift 6.JNIEnv. Wrapper classes are@unchecked Sendable, since the protocols may requireSendable.Why Skip needs it
Skip Fuse apps inject platform SDKs (auth, payments, analytics, networking) into shared Swift code as protocols the Android app implements in Kotlin. In a production Skip Fuse app (16 Skip modules) that's 41 protocols with 191 requirements, 103 of them
async, declared in one module and consumed in others, in Swift 6 mode. Skip's bridge awaits those through a completion callback, not a blockingget().Related upstream: swiftlang#913 (async closures) awaits futures with a blocking
get(); the continuation approach here could serve async closures too.Kotlin in this PR
ProtocolsTest.kt: a Kotlin class implementingProfileClient.swift, which is declared inKotlinSampleModels(extracted withenableJavaCallbacks), and passed toProfileService.swiftin the other module. Theasyncrequirement returns aCompletableFuture. The tests cover a sync requirement, one returning a struct, async success and failure, and that cancelling the Swift task cancels the Java future. The models module also has anErrorenum, which needs theThrowableSendablefix to build in Swift 6.Testing
./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:testpasses at this point in the stack.JNIJavaImplementedProtocolTests(7 tests): async wrapper shape, primitive unboxing andVoid, legacy-future and unsupported-result rejection, cross-module wrapper with and without the dependency's callbacks, and the qualified generic bound. ExistingJNIProtocolTestsexpectations updated forpublic, the per-call interface binding and@unchecked Sendable.AsyncCallbackProtocolimplemented in Java withCompletableFutures, awaited from Swift (success and failure).swift test --disable-experimental-prebuilts --disable-sandboxand./gradlew :Samples:SwiftJavaExtractJNISampleApp:testpass.swift format lint --stricton changed files;git diff --check.Not in this PR
asyncrequirements returning optionals, arrays or other containers.JavaCompletableFutureis declared in SwiftJavaRuntimeSupport; jextract/jni: Support async closures (#834) swiftlang/swift-java#913 adds one in SwiftJava, so one would need to go if both land.🤖 Generated with Claude Code