uniffi-rs icon indicating copy to clipboard operation
uniffi-rs copied to clipboard

Initializing Android JNI Context

Open ajoklar opened this issue 2 years ago • 3 comments

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?

ajoklar avatar Oct 04 '23 12:10 ajoklar

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.

badboy avatar Oct 05 '23 10:10 badboy

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?

  • There is a solution that uses JNI_GetCreatedJavaVMs (StackOverflow: How can I invoke a Java method from Rust via JNI?), but that leads to an UnsatisfiedLinkError on Android:

    dlopen failed: cannot locate symbol "JNI_GetCreatedJavaVMs"

  • Pass JNIEnv from Kotlin side to Rust: JNA exposes the class JNIEnv. Description:

    Marker type for the JNIEnv pointer. Use this to wrap native methods that take a JNIEnv* parameter. Pass CURRENT as the argument.

    On the Rust side, you could then get the VM via env.get_java_vm(), but I already have problems passing the object:

    In the generated Kotlin file I manually added the function signature fun initialize_android_context(env: JNIEnv) to the interface _UniFFILib. I will automate this step in a script, if everything works. If the Rust function needs a pointer, it can't be defined in the UDL and also it should only exist for Android, not for all platforms.

    It is called, when the instance gets initialized:

    companion object {
      internal val INSTANCE: _UniFFILib by lazy {
        val instance = loadIndirect<_UniFFILib>(componentName = "mylibname")
        instance.initialize_android_context(JNIEnv.CURRENT)
        return@lazy instance
      }
    }
    

    In the Rust library I added a function with this signature:

    #[no_mangle]
    pub extern "C" fn initialize_android_context(mut env: jni::JNIEnv)
    

    An exception is thrown:

    java.lang.IllegalArgumentException: Unsupported argument type com.sun.jna.JNIEnv at parameter 0 of function initialize_android_context

    I tried to change the argument's type to:

    • env: *mut jni::sys::JNIEnv
    • env: *mut c_void
    • env: jobject
    • env: jclass

    Always getting the same exception. The only example usage of JNIEnv.CURRENT I found is a test in the JNA repo. It consists of this Java definition and this C implementation.

    At this point, I'm out of ideas. I might be missing something obvious after messing with it for so long. It would be highly appreciated if anyone can point me in the right direction.

ajoklar avatar Oct 07 '23 11:10 ajoklar

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
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
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.

ajoklar avatar Nov 13 '23 11:11 ajoklar