Files
chanora/crates/chanora_bridge/src/android_init.rs
T

84 lines
2.8 KiB
Rust

//! Android-specific JNI lifecycle helpers.
//!
//! On Android the Flutter engine starts our cdylib but does not push
//! anything into `ndk_context` (the global the Android Oboe backend
//! reads to find Android audio services). Without that, the first
//! audio call hangs / fails.
//!
//! We solve this two ways:
//!
//! 1. `JNI_OnLoad` captures the `JavaVM*` as soon as the library is
//! loaded.
//! 2. We expose a `Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext`
//! JNI function the Kotlin `MainActivity.onCreate` calls with its
//! application Context. That JNI function pushes both the
//! `JavaVM*` and a `Context` global ref into `ndk_context`.
//!
//! After that, the Android Oboe backend can open the default
//! input/output devices.
#![cfg(target_os = "android")]
use std::sync::Once;
use jni::objects::{JClass, JObject};
use jni::sys::{jint, JNI_VERSION_1_6};
use jni::JNIEnv;
use log::{error, info};
/// Stash the JavaVM pointer between JNI_OnLoad and initContext.
static mut JAVA_VM: *mut std::ffi::c_void = std::ptr::null_mut();
static INIT_ONCE: Once = Once::new();
/// JNI entry point invoked by the Android runtime when the cdylib is
/// loaded via `System.loadLibrary` (which `flutter_rust_bridge` does
/// at `RustLib.init`).
#[no_mangle]
pub extern "system" fn JNI_OnLoad(
vm: *mut std::ffi::c_void,
_reserved: *mut std::ffi::c_void,
) -> jint {
// Safety: Android guarantees `vm` is a valid JavaVM* for the
// lifetime of the library.
unsafe {
JAVA_VM = vm;
}
JNI_VERSION_1_6
}
/// Called by `MainActivity.onCreate` with the application Context.
/// Initialises the `ndk_context` so the Oboe backend can locate
/// the Android audio services.
///
/// Symbol mangling note: Kotlin / JNI mangles the underscore in
/// `chanora_flutter` to `chanora_1flutter` because the literal `_`
/// in a JNI symbol means package separator.
#[no_mangle]
pub extern "system" fn Java_app_chanora_chanora_1flutter_MainActivity_initChanoraContext<'local>(
env: JNIEnv<'local>,
_class: JClass<'local>,
context: JObject<'local>,
) {
INIT_ONCE.call_once(|| {
let global = match env.new_global_ref(&context) {
Ok(g) => g,
Err(e) => {
error!("initChanoraContext: new_global_ref failed: {e}");
return;
}
};
let raw_ctx = global.as_obj().as_raw() as *mut std::ffi::c_void;
// Leak the global ref so it survives for the process lifetime
// (ndk_context borrows the pointer).
std::mem::forget(global);
unsafe {
ndk_context::initialize_android_context(JAVA_VM, raw_ctx);
}
info!(
"initChanoraContext: ndk_context initialised (vm={:p}, ctx={:p})",
unsafe { JAVA_VM },
raw_ctx
);
});
}