Skip to content

Async javaCallback selfPointer$ #865

Description

@Flatout73

jextract/JNI: generated protocol-wrapper thunks for async requirements reference undeclared selfPointer$ / selfTypePointer$

Summary

With mode: "jni" and enableJavaCallbacks: true, a protocol that has an async requirement
produces a Swift thunk that does not compile. The generated @_cdecl entry point declares its
parameters as selfPointer / selfTypePointer, but the async capture preamble refers to
selfPointer$ / selfTypePointer$ (trailing $), which are never declared:

Greeter+SwiftJava.swift:89:50: error: cannot find 'selfPointer$' in scope
Greeter+SwiftJava.swift:90:54: error: cannot find 'selfTypePointer$' in scope

Synchronous requirements on the same protocol are generated correctly — they consistently use the
undecorated parameter names. Only the async path is affected.

This appears to be new in 0.5.0, which added async support to protocol wrappers. On 0.4.2 the
wrapper for the same protocol failed differently (it emitted a call to a Java member that the
Java-side interface did not have, since the async member is mapped to CompletableFuture).

Environment

  • swift-java 0.5.0
  • Swift 6.3.3 (swift-6.3.3-RELEASE), macOS 26 (Apple Silicon)
  • mode: "jni", enableJavaCallbacks: true
  • Originally hit when cross-compiling for Android (aarch64-unknown-linux-android28,
    Swift Android SDK 6.3.3), but the faulty Swift is emitted by jextract itself and reproduces
    by running the tool directly on macOS.

Reproducer

Sources/Demo/Greeter.swift:

public protocol Greeter: AnyObject {
    func greetSync(name: String)
    func greetAsync(name: String) async
}

Sources/Demo/swift-java.config:

{
  "javaPackage": "com.example.demo",
  "mode": "jni",
  "enableJavaCallbacks": true
}
swift-java-tool jextract \
  --config Sources/Demo/swift-java.config \
  --swift-module Demo \
  --input-swift Sources/Demo \
  --output-java out-java \
  --output-swift out-swift

Generated output

out-swift/Greeter+SwiftJava.swift, the async requirement (comments trimmed):

public func Java_com_example_demo_GreeterBox__00024greetAsync__Ljava_lang_String_2JJLjava_util_concurrent_CompletableFuture_2(
  environment: UnsafeMutablePointer<JNIEnv?>!, thisClass: jclass,
  name: jstring?, selfPointer: jlong, selfTypePointer: jlong, result_future: jobject?
) {
  guard let selfPointerTypeMetadataPointer$ = UnsafeRawPointer(bitPattern: Int(Int64(fromJNI: selfTypePointer, in: environment))) else { ... }
  ...
  nonisolated(unsafe) let name = environment.interface.NewGlobalRef(environment, name)
  nonisolated(unsafe) let globalFuture = environment.interface.NewGlobalRef(environment, result_future)
  nonisolated(unsafe) let selfPointerSendable$ = selfPointer$          // ❌ 'selfPointer$' undeclared
  nonisolated(unsafe) let selfTypePointerSendable$ = selfTypePointer$  // ❌ 'selfTypePointer$' undeclared
  var task: Task<Void, Never>? = nil
  #if swift(>=6.2)
    if #available(macOS 26.0, iOS 26.0, watchOS 26.0, tvOS 26.0, *) {
      task = Task.immediate {
        ...
        let selfPointer$ = selfPointerSendable$
        let selfTypePointer$ = selfTypePointerSendable$

For contrast, the synchronous requirement in the same file uses the parameters directly and
compiles:

guard let selfPointerRawPointer$ = UnsafeMutableRawPointer(bitPattern: Int(Int64(fromJNI: selfPointer, in: environment))) else { ... }

Note also that the async thunk eagerly derives selfPointerExistential$ before the Task,
and then re-derives the pointers from the captured values inside it — so the pre-Task load looks
redundant on this path, though that is cosmetic next to the naming problem.

Expected

The two nonisolated(unsafe) let ...Sendable$ bindings should capture the @_cdecl parameters
selfPointer / selfTypePointer (no $ suffix), matching how the synchronous path and the rest
of the same function refer to them.

Impact

Any protocol with at least one async requirement is unusable with enableJavaCallbacks in JNI
mode: the emitted module does not compile, so the whole target fails to build. In our case a
BLE device protocol with ten async methods produced ~1200 errors, all of exactly these two
kinds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions