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.
jextract/JNI: generated protocol-wrapper thunks for
asyncrequirements reference undeclaredselfPointer$/selfTypePointer$Summary
With
mode: "jni"andenableJavaCallbacks: true, a protocol that has anasyncrequirementproduces a Swift thunk that does not compile. The generated
@_cdeclentry point declares itsparameters as
selfPointer/selfTypePointer, but the async capture preamble refers toselfPointer$/selfTypePointer$(trailing$), which are never declared:Synchronous requirements on the same protocol are generated correctly — they consistently use the
undecorated parameter names. Only the
asyncpath 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-6.3.3-RELEASE), macOS 26 (Apple Silicon)mode: "jni",enableJavaCallbacks: trueaarch64-unknown-linux-android28,Swift Android SDK 6.3.3), but the faulty Swift is emitted by
jextractitself and reproducesby running the tool directly on macOS.
Reproducer
Sources/Demo/Greeter.swift:Sources/Demo/swift-java.config:{ "javaPackage": "com.example.demo", "mode": "jni", "enableJavaCallbacks": true }Generated output
out-swift/Greeter+SwiftJava.swift, theasyncrequirement (comments trimmed):For contrast, the synchronous requirement in the same file uses the parameters directly and
compiles:
Note also that the
asyncthunk eagerly derivesselfPointerExistential$before theTask,and then re-derives the pointers from the captured values inside it — so the pre-
Taskload looksredundant on this path, though that is cosmetic next to the naming problem.
Expected
The two
nonisolated(unsafe) let ...Sendable$bindings should capture the@_cdeclparametersselfPointer/selfTypePointer(no$suffix), matching how the synchronous path and the restof the same function refer to them.
Impact
Any protocol with at least one
asyncrequirement is unusable withenableJavaCallbacksin JNImode: the emitted module does not compile, so the whole target fails to build. In our case a
BLE device protocol with ten
asyncmethods produced ~1200 errors, all of exactly these twokinds.