mozilla / uniffi-rs

a multi-language bindings generator for rust
https://mozilla.github.io/uniffi-rs/
Mozilla Public License 2.0
2.64k stars 218 forks source link

Initializing Android JNI Context #1778

Open ajoklar opened 11 months ago

ajoklar commented 11 months ago

I'm trying to add audio functionality via CPAL to my library that is used in iOS and Android applications. Everything works on iOS, but on Android an error is thrown: uniffi.fundsptest.InternalException: android context was not initialized.

There is an issue describing this behavior and it seems like it can be fixed implementing JNI_OnLoad. That function is part of JNI and as UniFFI depends on JNA, it is not called (right?). There doesn't seem to be an equivalent to JNI_OnLoad - I even found this JNA issue opened by a mozilla developer 5 years ago.

Does it mean that CPAL (or any library that needs an initialized Android context) is incompatible with UniFFI or is there any way to make it work?

badboy commented 11 months ago

From what I understand JNI_OnLoad is simply a function JNI calls by default after loading. And that function happens to do some setup work. JNA doesn't have that and UniFFI doesn't default-call anything after load (well, it calls some of its own code, but nothing generated from the user definitions).

Have you tried manually initializing the Android context before calling any CPAL code? You can expose a function that initializes the Android context for you and make your app/library/kotlin wrapper call that as the first thing.

ajoklar commented 10 months ago

I tried a lot since I opened this issue, but did not succeed, yet. Here is what I found out:

ndk_context::initialize_android_context needs pointers to the JVM and to the context (as documented here)

How do you get a pointer to the VM?

ajoklar commented 9 months ago

Based on this StackOverflow answer I found a solution that probably can be improved, but it works for now:

Added this function to the Rust library ```rust use jni::{ signature::ReturnType, sys::{jint, jsize, JavaVM}, }; use std::{ffi::c_void, ptr::null_mut}; pub type JniGetCreatedJavaVms = unsafe extern "system" fn(vmBuf: *mut *mut JavaVM, bufLen: jsize, nVMs: *mut jsize) -> jint; pub const JNI_GET_JAVA_VMS_NAME: &[u8] = b"JNI_GetCreatedJavaVMs"; #[no_mangle] pub unsafe extern "system" fn initialize_android_context() { let lib = libloading::os::unix::Library::this(); let get_created_java_vms: JniGetCreatedJavaVms = unsafe { *lib.get(JNI_GET_JAVA_VMS_NAME).unwrap() }; let mut created_java_vms: [*mut JavaVM; 1] = [null_mut() as *mut JavaVM]; let mut java_vms_count: i32 = 0; unsafe { get_created_java_vms(created_java_vms.as_mut_ptr(), 1, &mut java_vms_count); } let jvm_ptr = *created_java_vms.first().unwrap(); let jvm = unsafe { jni::JavaVM::from_raw(jvm_ptr) }.unwrap(); let mut env = jvm.get_env().unwrap(); let activity_thread = env.find_class("android/app/ActivityThread").unwrap(); let current_activity_thread = env .get_static_method_id( &activity_thread, "currentActivityThread", "()Landroid/app/ActivityThread;", ) .unwrap(); let at = env .call_static_method_unchecked( &activity_thread, current_activity_thread, ReturnType::Object, &[], ) .unwrap(); let get_application = env .get_method_id( activity_thread, "getApplication", "()Landroid/app/Application;", ) .unwrap(); let context = env .call_method_unchecked(at.l().unwrap(), get_application, ReturnType::Object, &[]) .unwrap(); ndk_context::initialize_android_context( jvm.get_java_vm_pointer() as *mut c_void, context.l().unwrap().to_owned() as *mut c_void, ); } ```
Modifications to the autogenerated Kotlin file ```kotlin internal interface _UniFFILib : Library { companion object { internal val INSTANCE: _UniFFILib by lazy { // initialize android context once and as soon as possible val instance = loadIndirect<_UniFFILib>(componentName = "mylibname") instance.initialize_android_context() return@lazy instance } } // add function to FFI definition fun initialize_android_context() // … } ```

I didn't add the function to the UDL as it only makes sense on Android.