Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initializing Android JNI Context #1778

Open
ajoklar opened this issue Oct 4, 2023 · 4 comments
Open

Initializing Android JNI Context #1778

ajoklar opened this issue Oct 4, 2023 · 4 comments

Comments

@ajoklar
Copy link

ajoklar commented Oct 4, 2023

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
Copy link
Member

badboy commented Oct 5, 2023

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
Copy link
Author

ajoklar commented Oct 7, 2023

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
Copy link
Author

ajoklar commented Nov 13, 2023

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.

@mhammond mhammond changed the title Initializing Android Context Initializing Android JNI Context Dec 13, 2023
@Tuurlijk
Copy link
Contributor

Tuurlijk commented Oct 26, 2024

@ajoklar I am trying to set up a cpal test sound with uniffi. Can you see what's wrong with it? https://github.com/Tuurlijk/android-cpal-test

Ok, just discovered that the example code works on native device, but not on the emulator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants