Skip to content

Commit d81b3b4

Browse files
qdotclaude
andcommitted
fix: Fix JNI signature mismatches caused by bind_java_type! JObject mapping
bind_java_type! maps JObject to Ljava/lang/Object; in JNI signatures, but Java methods using domain-specific types (UUID, Future, Stream, ScanResult, byte[], List, Map, etc.) require exact signature matches. This caused runtime "Method not found" errors and a SIGSEGV in the scan callback. Changes: - objects.rs: Move all methods with domain-typed params/returns out of bind_java_type! into manual env.call_method() with correct JNI signatures. Keep bind_java_type! for class definitions and primitive-only methods. - future.rs: Move JFuture::poll to manual impl with correct Waker/PollResult sigs - stream.rs: Move JStream::poll_next to manual impl with correct Waker/PollResult sigs - mod.rs: Fix reportScanResult to use extern "C" + EnvUnowned + with_env for raw JNI ABI compatibility (from_raw_parts requires raw C calling convention). Add env.get_java_vm() to seed JavaVM singleton during init. - peripheral_finder.rs: Add catch_unwind and logging to adapter background thread for Android debugging (silent thread death caused confusing RecvError) Verified: 28/29 Android integration tests pass on Pixel 9a. The single failure (testPropertiesContainPeripheralInfo TX power) is a test-peripheral issue. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c3ac72b commit d81b3b4

5 files changed

Lines changed: 167 additions & 99 deletions

File tree

src/droidplug/jni/mod.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub mod objects;
22

3-
use ::jni::{Env, NativeMethod, jni_str, native_method, objects::{JObject, Reference}};
3+
use ::jni::{Env, EnvUnowned, NativeMethod, jni_str, native_method, objects::{JObject, Reference}};
44
use jni::{objects::JString, sys::jboolean};
55
use std::ffi::c_void;
66
use std::sync::Once;
@@ -18,17 +18,21 @@ pub fn init(env: &mut Env) -> crate::Result<()> {
1818
}
1919

2020
fn init_inner(env: &mut Env) -> crate::Result<()> {
21+
// Seed the JavaVM singleton so JavaVM::singleton() works from any thread.
22+
env.get_java_vm()?;
2123
{
2224
let adapter_class =
2325
env.find_class(jni_str!("com/nonpolynomial/btleplug/android/impl/Adapter"))?;
2426
unsafe { env.register_native_methods(
2527
&adapter_class,
2628
&[
27-
native_method! {
28-
name = "reportScanResult",
29-
sig = (scan_result: JObject) -> (),
30-
fn = adapter_report_scan_result,
31-
},
29+
// Can't use native_method! here — JObject maps to Ljava/lang/Object; but the
30+
// Java side declares the parameter as ScanResult. JNI requires exact signature match.
31+
NativeMethod::from_raw_parts(
32+
jni_str!("reportScanResult"),
33+
jni_str!("(Landroid/bluetooth/le/ScanResult;)V"),
34+
adapter_report_scan_result as *mut c_void,
35+
),
3236
native_method! {
3337
name = "onConnectionStateChanged",
3438
sig = (addr: JString, connected: jboolean) -> (),
@@ -96,13 +100,16 @@ impl From<::jni::errors::Error> for crate::Error {
96100
}
97101
}
98102

99-
fn adapter_report_scan_result<'local>(
100-
env: &mut Env<'local>,
103+
extern "C" fn adapter_report_scan_result<'local>(
104+
mut env: EnvUnowned<'local>,
101105
obj: JObject<'local>,
102106
scan_result: JObject<'local>,
103-
) -> jni::errors::Result<()> {
104-
let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result);
105-
Ok(())
107+
) {
108+
let outcome = env.with_env(|env| {
109+
let _ = super::adapter::adapter_report_scan_result_internal(env, &obj, scan_result);
110+
Ok::<_, jni::errors::Error>(())
111+
});
112+
let _ = outcome.into_outcome();
106113
}
107114

108115
fn adapter_on_connection_state_changed<'local>(

0 commit comments

Comments
 (0)