背景
在文章Rust移动端跨平台开发实践中提到
Android、iOS:中间层使用uniffi编写binding代码。使用uniffi-bindgen将binding代码生成kotlin、Swift代码,方便Android、iOS调用。
在实际开发中,遇到上述方案编译出来的Android so,在特定场景下会出现崩溃的问题。
崩溃问题描述
使用下面简化代码,详细说明崩溃的场景。
- 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(())
}
- 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()))
}
- 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)
// }
}
})
}
如注释所示,出现崩溃的场景如下
- uniffi-rs生成的Kotlin接口,传入一个callback,传入的callback会在rust层的子线程执行
- 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有不同表现,具体原因有待深究。
暂时解决办法
切换到其他线程调用。