Skip to content

No Thread Safety on Android (detached thread error) #427

Open
@Erik1000

Description

@Erik1000

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions