Two follow-ups split out of the PR #3999 review so they don't block the Kotlin SDK, both verified against the code.
1. JNI panic-safety across the FFI boundary (from support.rs review)
rs-unified-sdk-jni's guard (support.rs) wraps each JNI body in catch_unwind and rethrows as a Java exception. But it only catches panics that unwind back to its own closure. The guarded bodies call rs-platform-wallet-ffi / rs-sdk-ffi exports that are extern "C" (non-unwinding; no C-unwind, no internal catch_unwind). Android builds panic="unwind", and block_on_worker (runtime.rs) does rt.spawn(future).await.expect("tokio worker panicked") — a spawned-future panic becomes a JoinError, and .expect re-panics on the calling thread inside the extern "C" frame. Since Rust 1.81 a panic leaving a non-unwinding extern "C" frame aborts the process — before guard's catch_unwind can observe it. So any unwrap/expect deep in an async FFI callee (proof verification, grovedb, serde) can terminate the Android app, breaking the "a JNI library must never abort the app process" contract in the android profile comment.
Scope: ~539 extern "C" exports across two crates that are also shared with the iOS SDK, which builds panic="abort" (where catch_unwind is inert). So the two mechanical fixes floated in review don't fit as-is: there are no non-extern internal impls to redirect JNI to, and blanket per-export catches would be dead code on iOS.
Proposed work:
- Make the
Result-returning block_on_worker sites catch_unwind the future and surface a caught panic as a PlatformWalletError (normal PlatformWalletFFIResult error) instead of .expect-aborting — this neutralizes the dominant abort path.
- A uniform
#[catch_panic]-style attribute (or impl/shim split) across the FFI exports, cfg-gated so it's inert under panic="abort" (iOS).
- Either soften the android profile "never aborts" comment or point it at this issue until the guarantee actually holds.
2. Behavioral regression test for Sdk teardown / query-gate lifecycle (from Sdk.kt review)
The SDK teardown is fenced correctly, but GateCoverageLintTest only scans parameter names + checks for a gate-shaped source opener — it doesn't exercise the runtime semantics. Add a behavioral test that constructs an Sdk, holds an SDK lease, and drives both close paths to prove:
closeSuspending() waits for an active lease to drain,
- operations started after close fail before reading the native handle,
- cancellation can't strand cleanup,
AutoCloseable.close() follows the same fence.
References
Two follow-ups split out of the PR #3999 review so they don't block the Kotlin SDK, both verified against the code.
1. JNI panic-safety across the FFI boundary (from
support.rsreview)rs-unified-sdk-jni'sguard(support.rs) wraps each JNI body incatch_unwindand rethrows as a Java exception. But it only catches panics that unwind back to its own closure. The guarded bodies callrs-platform-wallet-ffi/rs-sdk-ffiexports that areextern "C"(non-unwinding; noC-unwind, no internalcatch_unwind). Android buildspanic="unwind", andblock_on_worker(runtime.rs) doesrt.spawn(future).await.expect("tokio worker panicked")— a spawned-future panic becomes aJoinError, and.expectre-panics on the calling thread inside theextern "C"frame. Since Rust 1.81 a panic leaving a non-unwindingextern "C"frame aborts the process — beforeguard'scatch_unwindcan observe it. So anyunwrap/expectdeep in an async FFI callee (proof verification, grovedb, serde) can terminate the Android app, breaking the "a JNI library must never abort the app process" contract in the android profile comment.Scope: ~539
extern "C"exports across two crates that are also shared with the iOS SDK, which buildspanic="abort"(wherecatch_unwindis inert). So the two mechanical fixes floated in review don't fit as-is: there are no non-externinternal impls to redirect JNI to, and blanket per-export catches would be dead code on iOS.Proposed work:
Result-returningblock_on_workersitescatch_unwindthe future and surface a caught panic as aPlatformWalletError(normalPlatformWalletFFIResulterror) instead of.expect-aborting — this neutralizes the dominant abort path.#[catch_panic]-style attribute (or impl/shim split) across the FFI exports,cfg-gated so it's inert underpanic="abort"(iOS).2. Behavioral regression test for
Sdkteardown / query-gate lifecycle (fromSdk.ktreview)The SDK teardown is fenced correctly, but
GateCoverageLintTestonly scans parameter names + checks for a gate-shaped source opener — it doesn't exercise the runtime semantics. Add a behavioral test that constructs anSdk, holds an SDK lease, and drives both close paths to prove:closeSuspending()waits for an active lease to drain,AutoCloseable.close()follows the same fence.References
packages/rs-unified-sdk-jni/src/support.rs(guard),runtime.rs(block_on_worker)packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/Sdk.kt