Skip to content

Adopt jextract JNI callbacks and Compose demo UI - #5

Merged
colemancda merged 8 commits into
masterfrom
feature/jextract-jni-callbacks
Jul 25, 2026
Merged

Adopt jextract JNI callbacks and Compose demo UI#5
colemancda merged 8 commits into
masterfrom
feature/jextract-jni-callbacks

Conversation

@colemancda

Copy link
Copy Markdown
Member

Summary

Replaces the legacy @JavaImplementation/swiftPeer callback bridging with swift-java's jextract JNI pattern (following PureSwift/Android), and rewrites the demo app UI in Kotlin Jetpack Compose.

Library

  • New AndroidBluetoothBridge target processed by JExtractSwiftPlugin (JNI mode): one generated static method per Bluetooth callback event (org.pureswift.bluetooth.bridge.AndroidBluetoothBridge); byte payloads cross as byte[].
  • AndroidCentralRegistry maps stable Int64 identifiers to weakly-held centrals, replacing the unsafe raw-pointer swiftPeer round-trip.
  • Callback bodies move to handle* event methods on AndroidCentral taking primitive values; peripherals resolve by address (getRemoteDevice) and characteristics match by UUID + instance id, removing the need for Java object identity in events.
  • @JavaImplementation is gone from the package (it also crashed the current compiler when applied to nested types).

Demo

  • ScanCallback.kt / BluetoothGattCallback.kt are pure Kotlin data-forwarders — no external fun, no pointer field. They remain only because the Android callback types are abstract classes requiring JVM subclasses.
  • Swift View-driven UI replaced by a Compose scanner screen (scan toggle, RSSI-sorted list, tap-to-connect showing service UUIDs) backed by a new BluetoothDemoBridge jextract target exposing startScan/stopScan/connect/disconnect with closure callbacks (generated Java functional interfaces).
  • Gradle now drives the Swift cross-compilation (jextract task, --disable-sandbox for the swift-java callbacks sub-build), consumes the generated Java + SwiftKitCore as source, and stages all .sos into jniLibs; the shell build scripts are removed. An Android-compatible Unsigned annotation replaces the upstream one that depends on jdk.jfr.

Testing

  • gradle assembleDebug (JDK 21; callbacks sub-build on JDK 25) builds the APK end-to-end, including jextract generation for both bridge targets.
  • Verified JNI thunks for both bridges are exported from libSwiftAndroidApp.so.
  • Repo-wide checks: zero @JavaImplementation, external fun, or swiftPeer remaining.
  • On-device BLE scan/connect verification still pending (requires physical hardware).

Add AndroidBluetoothBridge target (JExtractSwiftPlugin, JNI mode) exposing
one function per callback event, an AndroidCentralRegistry mapping Int64
identifiers to centrals (replacing the unsafe swiftPeer raw pointer), and
handle* event methods on AndroidCentral taking primitive values. Peripherals
are resolved by address via getRemoteDevice and characteristics matched by
UUID and instance id, so callback events no longer need Java object identity.
ScanCallback and BluetoothGattCallback no longer declare external JNI
functions or hold a swiftPeer pointer; they extract primitive values from
the Android callback objects and forward them to the generated
AndroidBluetoothBridge statics, keyed by central identifier and address.
The SwiftAndroidApp dynamic library is now built from BluetoothDemoBridge,
a jextract JNI target exposing startScan/stopScan/connect/disconnect with
closure parameters that surface as Java functional interfaces. DemoCentral
owns the AndroidCentral instance, bootstrapped from the application context
via AndroidContext. The Swift-driven View UI, example sources and shell
build scripts are removed.
MainActivity renders a Compose scanner screen backed by the generated
BluetoothDemoBridge bindings: scan toggle, device list sorted by RSSI, and
tap-to-connect with discovered service UUIDs. The SwiftObject/external-fun
Kotlin scaffolding and the unused View-based demo classes are removed.
The jextract task cross-compiles the package graph with the Swift Android
SDK (--disable-sandbox so the swift-java callbacks sub-build can run
Gradle), generated Java bindings and a patched SwiftKitCore source tree are
added to the source set, and all required .so files are staged into
jniLibs. The Unsigned annotation is replaced with an Android-compatible
copy since the upstream source depends on jdk.jfr.
Install a Temurin JDK before the Android build (required by
android-commandlinetools and the swift-java jextract plugin) and build with
Swift 6.3.3, matching the swift-tools 6.3 requirement of the manifests.
The AndroidBluetoothBridge target now exposes BluetoothScanEventSink and
BluetoothGattEventSink as public Swift classes; jextract surfaces each as a
Java class with native instance methods, so the Kotlin adapters hold and
invoke a Swift instance directly instead of routing events through Int64
identifiers and a global registry. Each sink weakly references its central,
preserving the no-dangling-events behavior. Sink identity crosses the JNI
boundary once, during adapter construction, via the transient
AndroidCentralHandoff window; lifetime is managed by the swift-java arena.
ScanCallback and BluetoothGattCallback now hold a jextract-generated
BluetoothScanEventSink/BluetoothGattEventSink and forward events to it; the
no-argument constructors obtain the Swift sink for the connection under
construction via AndroidBluetoothBridge.takePending*EventSink, and the
primary constructors accept any sink instance. Java compatibility moves to
17, which the generated bindings require.
@colemancda
colemancda force-pushed the feature/jextract-jni-callbacks branch from 235c656 to 9ef650f Compare July 25, 2026 22:04
@colemancda
colemancda merged commit 357635c into master Jul 25, 2026
12 checks passed
@colemancda
colemancda deleted the feature/jextract-jni-callbacks branch July 25, 2026 23:09
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