Rust 使用uniffi-rs 在Android 上的一个崩溃问题

510 阅读3分钟

背景

在文章Rust移动端跨平台开发实践中提到

Android、iOS:中间层使用uniffi编写binding代码。使用uniffi-bindgen将binding代码生成kotlin、Swift代码,方便Android、iOS调用。

在实际开发中,遇到上述方案编译出来的Android so,在特定场景下会出现崩溃的问题。

崩溃问题描述

使用下面简化代码,详细说明崩溃的场景。

  1. Rust层定义一个trait,目的是为了让 Android 设置一个Callback给Rust调用。
// 方便把日志代理到外部
static LOGGER: OnceLock<Box<dyn Log>> = OnceLock::new();

pub trait Log: Send + Sync {
    fn log(&self, msg: &str);
}
// 设置全局的logger
pub fn set_logger(logger: Box<dyn Log>) -> anyhow::Result<()> {
    LOGGER
        .set(logger)
        .map_err(|e| anyhow!("set logger failed"))?;
    Ok(())
}
// rust 层调用kotlin 设置的logger
pub fn logger(msg: &str) {
    LOGGER.get().map(|log| {
        log.log(msg);
    });
}

pub fn hello_rust(config: MobileConfig) -> anyhow::Result<()> {
    // 调用log 打印日志
    log::logger(&config.username);
    Ok(())
}

// 在其他线程回调Log,实际此处应该是其他业务callback,为了简单,直接使用上述同一个trait Log
pub fn hello_rust_log(log: Box<dyn Log>) -> anyhow::Result<()> {
    // 如果直接在本线程运行,也不会出现崩溃
    let handle = thread::spawn(move || {
        log.log("hello_rust_log, sub thread");
    });
    // 待线程执行完
    handle.join();
    Ok(())
}
  1. uniffi-rs 对上述代码编写binding code
#[uniffi::export]
pub fn set_logger(logger: Box<dyn FFILog>) -> Result<(), MobileError> {
    mobile::set_logger(Box::new(LogImpl { logger }))
        .map_err(|e| MobileError::Generic(e.to_string()))
}

#[uniffi::export]
pub fn hello_rust(config: MobileConfig) -> Result<(), MobileError> {
    mobile::hello_rust(config.to_inner_config())
        .map_err(|e| MobileError::Generic(e.to_string()))
}

#[uniffi::export]
pub fn hello_rust_log(log: Box<dyn Log>) -> Result<(), MobileError> { 
    mobile::hello_rust_log(Box::new(LogImpl { logger }))
        .map_err(|e| MobileError::Generic(e.to_string()))
}
  1. Kotlin 侧调用uniffi-rs生成的Kotlin接口
private fun testCallback() { 
    val config = MobileConfig(id = "id", username = "username", pwd = "pwd", null)  
    // 调用helloRustLog,传入的callback会在rust层的子线程被调用执行
    helloRustLog(object : FfiLog {  
        override fun log(msg: String) {
            // 如果新建线程不会崩溃
            // thread {
                // helloRust在rust层的子线程执行
                // helloRust内部调用 rust层的hello_rust代码,hello_rust中调用之前设置的callback,此时出现崩溃
                helloRust(config)  
            // }  
        }  
    })  
}

如注释所示,出现崩溃的场景如下

  1. uniffi-rs生成的Kotlin接口,传入一个callback,传入的callback会在rust层的子线程执行
  2. callback在rust层的子线程执行时,调用了rust层的代码,rust层的代码内部又调用了之前设置的callback

此时出现崩溃,崩溃信息如下

12-14 07:29:06.423 **17629** 17858 F: runtime.cc:669] Runtime aborting...
12-14 07:29:06.423 **17629** 17858 F: runtime.cc:669] Dumping all threads without mutator lock held
12-14 07:29:06.423 **17629** 17858 F: runtime.cc:669] All threads:
12-14 07:29:06.423 **17629** 17858 F: runtime.cc:669] DALVIK THREADS (115):
12-14 07:29:06.423 **17629** 17858 F: runtime.cc:669] "Thread-33" prio=5 tid=54 Runnable

如果上述代码编译到mac平台的程序,会出现如下报错,但不会崩溃 JNA: could not detach thread

原因猜测

uniffi-rs 生成的Kotlin代码用到了 jna库,方便Kotlin调用so。 所以上述崩溃应该和Rust无关,而是jna库导致的。猜测崩溃的原因可能是Native侧的线程和jvm环境的Attach Detach有关系,而且不同的JVM有不同表现,具体原因有待深究。

暂时解决办法

切换到其他线程调用。