Skip to content

JNI: Java implementations of async and cross-module Swift protocols - #7

Open
piercifani wants to merge 2 commits into
jni/05-codegen-fixesfrom
jni/06-java-implemented-protocols
Open

piercifani wants to merge 2 commits into
jni/05-codegen-fixesfrom
jni/06-java-implemented-protocols

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 6/21 · base: jni/05-codegen-fixes · previous: #6 · next: #8

With enableJavaCallbacks, Java classes can implement Swift protocols and be passed into Swift. This PR makes that work for async requirements, for protocols declared in another module, and in Swift 6 language mode.

Problems

  1. async requirements didn't compile. jextract gives the Java interface CompletableFuture<T> fetch(...), but the Swift wrapper called it synchronously as if it returned T, and wrap-java doesn't bind the generic CompletableFuture<T> method at all (value of type 'JavaProfileClient' has no member 'fetch').
  2. Protocols from another module couldn't be implemented in Java. In the consuming module, a parameter like any ProfileClient only accepted Swift instances (a Java implementation hit fatalError("…typeMetadata$ memory address was null")), and the Java generic bound was printed unqualified (<_T0 extends ProfileClient>), so the Java didn't compile.
  3. Swift 6 language mode: JavaSwiftArena.defaultAutoArena was a mutable static (not concurrency-safe), and wrap-java's classes for Swift error enums (open class JavaClientError: Exception) failed non-final class cannot conform to 'Sendable', because Throwable: Error implies Sendable.
  4. Thread confinement: the wrapper's Java interface keeps the JNIEnv of the thread that created it, but Swift calls requirements from any thread, always so for async ones.

Changes

Awaiting a Java future without blocking

  • SwiftKitCore: CompletableFutureContinuation.resumeWhenComplete(future, continuation) resumes a Swift continuation from whenComplete, unwrapping CompletionException.
  • SwiftJavaRuntimeSupport: _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.
  • Generated wrapper for an async requirement: call the Java method dynamically (wrap-java doesn't bind it) to get the CompletableFuture, await it, then convert the value. Supported results are Void, directly translated types (unboxed through JavaBoxable) and jextracted types. Other results, or asyncFuncMode: legacyFuture (a Future can't notify), skip the wrapper with a warning.

Protocols from other modules

  • The SwiftJava…Wrapper protocol and its default implementations are public.
  • The consuming module accepts Java implementations of a protocol from a dependency extracted with enableJavaCallbacks (from its --depends-on config). It builds the per-parameter wrapper class from the protocol's names.
  • Generic bounds are printed fully qualified.

Swift 6 and threading

  • defaultAutoArena is a nonisolated(unsafe) static let; it was never reassigned.
  • extension Throwable: @unchecked Sendable {} at the root, so generated exception subclasses compile in Swift 6.
  • Each wrapper method re-binds the Java interface to the calling thread's JNIEnv. Wrapper classes are @unchecked Sendable, since the protocols may require Sendable.

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 blocking get().

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 implementing ProfileClient.swift, which is declared in KotlinSampleModels (extracted with enableJavaCallbacks), and passed to ProfileService.swift in the other module. The async requirement returns a CompletableFuture. 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 an Error enum, which needs the Throwable Sendable fix to build in Swift 6.

Testing

  • Kotlin sample: ./gradlew :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes at this point in the stack.
  • New JNIJavaImplementedProtocolTests (7 tests): async wrapper shape, primitive unboxing and Void, legacy-future and unsupported-result rejection, cross-module wrapper with and without the dependency's callbacks, and the qualified generic bound. Existing JNIProtocolTests expectations updated for public, the per-call interface binding and @unchecked Sendable.
  • JNI sample: AsyncCallbackProtocol implemented in Java with CompletableFutures, awaited from Swift (success and failure).
  • A standalone Swift 6 package with the protocol in one module and the consumer in another passes Kotlin tests on the JVM. They cover a sync requirement, a struct-returning requirement, an async requirement (success and exception), and cancelling the Swift task cancelling the Java future.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test pass.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • Implementing Swift protocols in Java still needs the SwiftPM sandbox disabled (the plugin runs Gradle and wrap-java).
  • async requirements returning optionals, arrays or other containers.
  • JavaCompletableFuture is 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

@cla-bot cla-bot Bot added the cla-signed label Nov 3, 2026
@piercifani
piercifani force-pushed the jni/06-java-implemented-protocols branch from 5306df3 to a5d5420 Compare November 3, 2026 14:41
@piercifani
piercifani added this pull request to stack #4 November 3, 2026 14:53
@piercifani
piercifani force-pushed the jni/06-java-implemented-protocols branch from a5d5420 to fb8294c Compare November 3, 2026 15:19
@piercifani
piercifani removed this pull request from stack #4 November 3, 2026 15:24
@piercifani
piercifani added this pull request to stack #12 November 3, 2026 15:24
@piercifani
piercifani force-pushed the jni/06-java-implemented-protocols branch from fb8294c to 9272c4d Compare November 3, 2026 15:56
piercifani and others added 2 commits November 3, 2026 17:35
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>
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