Skip to content

JNI: preserve MainActor isolation in synchronous thunks - #938

Open
teamhimeh wants to merge 1 commit into
swiftlang:mainfrom
qoncept:codex/fix-jni-main-actor-thunks
Open

teamhimeh wants to merge 1 commit into
swiftlang:mainfrom
qoncept:codex/fix-jni-main-actor-thunks

Conversation

@teamhimeh

Copy link
Copy Markdown

Problem

JNI extraction preserves synchronous Java signatures for @MainActor APIs, but emits nonisolated Swift thunks. As a result, the generated bindings fail to compile when they call isolated initializers or methods, or access isolated properties and subscripts.

For example, extracting this declaration generates a nonisolated thunk that directly calls Counter.init, producing an actor-isolation diagnostic:

@MainActor
public final class Counter {
  public var value: Int64

  public init(value: Int64) {
    self.value = value
  }
}

Changes

  • Annotate synchronous JNI entry points with @MainActor when isolation is explicitly specified on the declaration, its enclosing nominal type, or its extension. Recognize both @MainActor and @_Concurrency.MainActor.
  • Emit MainActor.preconditionIsolated() before the thunk converts arguments or accesses Swift state. Java callers must already execute on the main actor's executor; the thunk remains synchronous and does not schedule an executor hop.
  • Preserve isolation on generic opener requirements and their implementations, including methods that return JNI references.
  • Keep explicitly nonisolated members and existing asynchronous downcall paths unchanged.
  • Add generator regression tests, compilation fixtures in the JNI sample, and documentation of the caller's executor requirement.

Annotating the thunk itself allows reference-valued results such as jstring? to remain in the synchronous call without requiring JNI pointers to conform to Sendable.

Validation

Tested on macOS 27.0.1 with Apple Swift 6.4 and JetBrains Runtime 25.0.2.

  • Confirmed the new regression tests fail against the unmodified generator at 301e3da710ede7af93e798f1ce1c7950715a5748 and pass with this change. Coverage includes initializers, instance/static methods, properties, subscripts, extensions, qualified attributes, generic openers, throwing calls, reference results, nonisolated members, and async signatures.
  • Full Swift test suite: swift test --disable-experimental-prebuilts --disable-sandbox.
  • JNI sample generation, native/Java compilation, and all 273 Java tests: ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test --no-daemon.
  • Compiled a standalone JNI package containing MainActorCounter and MainActorBox in Swift 6 language mode, including string results and generic opener dispatch. The existing JNI sample also compiles these fixtures in Swift 5 language mode.
  • swift-format lint --strict on all changed Swift files and git diff --check.

Synchronous MainActor APIs currently produce nonisolated JNI entry points,
so their generated bindings fail to compile when calling isolated Swift
initializers or members.

Annotate these entry points with MainActor and check the caller's executor
before converting arguments or accessing Swift state. Keep the Java API
synchronous, preserve isolation through generic openers, and leave
nonisolated members and asynchronous downcalls on their existing paths.
Isolating the thunk itself also supports reference-valued JNI results in
Swift 6 without requiring JNI pointers to conform to Sendable.

Add regression tests, JNI sample compilation fixtures, and documentation
of the executor requirement for Java callers. Verify the regressions
against the original generator, run the full Swift and JNI sample test
suites, and compile the fixtures in Swift 6 language mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant