btleplug icon indicating copy to clipboard operation
btleplug copied to clipboard

No Thread Safety on Android (detached thread error)

Open Erik1000 opened this issue 8 months ago • 0 comments

On android, since JNIEnv is used, one must ensure that calls to btleplug which themselves use JNIEnv to call into the JVM stay on the same thread (or at least on a thread "attached" to the JVM), because otherwise Err(btleplug::Other(JniCall(ThreadDetached))) is returned. JNIEnv ensures it stays on the same thread by being !Send and !Sync. But Adapter is Send and Sync and can therefore be moved to another thread in rust which won't be "attached" to the VM. This is a problem, because moves to other threads are not always visible. For example, when using tokio with a multi threaded runtime, futures can be moved to another thread which will lead to hard to debug errors (especially since panics/stderr is not logged to logcat by default). I think a possible fix would be to follow the docs on jni::JavaVM and surrounding each call into the JVM with an jni::AttachGuard

This will log the error described above:

/// This will be called from the android app in a new thread because it will block and otherwise android kills the app
#[no_mangle]
pub extern "system" fn Java_com_example_Rust_start(env: JNIEnv, _this: JClass) {
    android_logger::init_once(
        android_logger::Config::default()
            .with_max_level(log::LevelFilter::Trace)
            .with_tag("Rust"),
    );

    info!("Launching tokio...");
    match launch(env) {
        Ok(_) => log::info!("Finished ok"),
        Err(e) => log::error!("Return error: {e:#?}",),
    };
}

#[tokio::main(flavor = "multi_thread")]
async fn launch(env: JNIEnv) -> color_eyre::Result<()> {
    btleplug::platform::init(&env)?;
    let manager = Manager::new().await?;

    // get the first (and usually only) ble adapter
    let adapter = manager
        .adapters()
        .await?
        .into_iter()
        .next()
        .ok_or(eyre!("No bluetooth adapter found"))?;
    adapter.start_scan(ScanFilter::default()).await?;

    let handle = tokio::spawn(async move {
        warn!("in spawn");
        match adapter.stop_scan().await {
            Ok(_) => info!("works on other thread"),
            Err(e) => error!("Not working on other thread: {e:#?}"),
        }
    });
    info!("waiting for handle");
    handle.await?;
   Ok(())
}

For now, this error should be avoidable by using a single threaded tokio runtime.

Erik1000 avatar Apr 21 '25 13:04 Erik1000