android-rs-glue icon indicating copy to clipboard operation
android-rs-glue copied to clipboard

Expose nicer JNI/NDK interface

Open OptimisticPeach opened this issue 4 years ago • 9 comments

Currently it is difficult to interact with the JNI and NDK through rust. This issue is asking for:

  • A way to insert code called on the main thread at startup (In java, or preferably rust)
  • A more exposed java interface for more complex calls to java code

This can be mostly covered by android-ndk-rs but a portion of it should probably be done here.

This is closer to a goal than an issue.

OptimisticPeach avatar Sep 01 '19 19:09 OptimisticPeach

I definitely think that exposing the JNI would be a good thing. In terms of api, android_glueshould probably provide the JNIEnv *. It could do this in a few ways:

  • Have a function that returns jni::JNIEnv – this is bad because it requires the JNI crate as a public dependency
  • Have a function that returns *mut c_void – this is bad because it requires users to write unsafe code to use the JNI
  • Have a jni feature that enables a function returning jni::JNIEnv – this is bad because it makes it slightly less convenient, and still has jni as a public dependency when the feature is enabled.

I have no strong opinions here, but my preference is either the first or third ways.

A way to insert code called on the main thread at startup (In java, or preferably rust)

Why particularly do you need this? (As opposed to just writing it at the start of main())

mb64 avatar Sep 02 '19 19:09 mb64

Why particularly do you need this? (As opposed to just writing it at the start of main()) (Specifically) I use this to remove the navigation bars and decorations on the screen which can only be done from the main thread which NativeActivity is created on. I currently have a modified version of cargo-apk locally which I've added the following function in android_native_app_glue:

// android_native_app_glue.h

/**
 * This is the function that is run on startup in the main thread
 * For example, to hide the navigation bars. 
 */
extern void android_startup(ANativeActivity* activity, int focused);

// android_native_app_glue.c

static void onWindowFocusChanged(ANativeActivity* activity, int focused) {
    LOGV("WindowFocusChanged: %p -- %d\n", activity, focused);
    android_app_write_cmd((struct android_app*)activity->instance,
            focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS);
    android_startup(activity, focused);
}

which I then define in my rust code as follows:

use cargo_apk_injected_glue::ffi;

#[no_mangle]
pub extern fn android_startup(activity: *const ffi::ANativeActivity, _focused: i32) {
    let env = unsafe { &mut *(*activity).env };

    let functions = unsafe { &*env.functions };

    let activity_class = (functions.FindClass)(env, "android/app/NativeActivity\0".as_ptr() as *const _);
    let get_window = (functions.GetMethodID)(env, activity_class, "getWindow\0".as_ptr() as *const _, "()Landroid/view/Window;\0".as_ptr() as *const _);

    let window_class = (functions.FindClass)(env, "android/view/Window\0".as_ptr() as *const _);
    let get_decor_view = (functions.GetMethodID)(env, window_class, "getDecorView\0".as_ptr() as *const _, "()Landroid/view/View;\0".as_ptr() as *const _);

    let view_class = (functions.FindClass)(env, "android/view/View\0".as_ptr() as *const _);
    let set_system_ui_visibility = (functions.GetMethodID)(env, view_class, "setSystemUiVisibility\0".as_ptr() as *const _, "(I)V\0".as_ptr() as *const _);

    let window = (functions.CallObjectMethod)(env, unsafe { (*activity).clazz }, get_window);

    let decor_view = (functions.CallObjectMethod)(env, window, get_decor_view);

    let flag_fullscreen_id = (functions.GetStaticFieldID)(env, view_class, "SYSTEM_UI_FLAG_FULLSCREEN\0".as_ptr() as *const _, "I\0".as_ptr() as *const _);
    let flag_hide_navigation_id = (functions.GetStaticFieldID)(env, view_class, "SYSTEM_UI_FLAG_HIDE_NAVIGATION\0".as_ptr() as *const _, "I\0".as_ptr() as *const _);
    let flag_immersive_sticky_id = (functions.GetStaticFieldID)(env, view_class, "SYSTEM_UI_FLAG_IMMERSIVE_STICKY\0".as_ptr() as *const _, "I\0".as_ptr() as *const _);

    let flag_fullscreen = (functions.GetStaticIntField)(env, view_class, flag_fullscreen_id);
    let flag_hide_navigation = (functions.GetStaticIntField)(env, view_class, flag_hide_navigation_id);
    let flag_immersive_sticky = (functions.GetStaticIntField)(env, view_class, flag_immersive_sticky_id);
    let flag = flag_fullscreen | flag_hide_navigation | flag_immersive_sticky;

    (functions.CallVoidMethod)(env, decor_view, set_system_ui_visibility, flag);
}

(The code already there is a rust translation of what @philip-alldredge suggested in #234) Obviously if I were to not include this it would crash the application with a linker error, so this is definitely not user-friendly, but perhaps a subscriber-style system would work well? Or even a Mutex<Option<fn(*const NativeActivity, i32)>> globally for a single one.

OptimisticPeach avatar Sep 02 '19 19:09 OptimisticPeach

We'll need to keep in mind that the JNIEnv provided by ANativeActivity cannot be shared with other threads. To create one for another thread requires using JavaVM and attaching a thread to it.

In @OptimisticPeach's case, code will need to be ran at startup and on subsequent window focus changes. That should be doable in main as long as something provides a way to get or crate a valid JNIEnv for the current thread.

I think an high-level set of APIs that calls a closure with a a valid jni::JNIEnv would work well for most cases. Assuming that cost must be ran on the UI thread, there should be a way to run the closure on the UI thread using Activity.runOnUiThread.

My feeling is that most of this should eventually be in android-ndk/android-ndk-sys.

philip-alldredge avatar Sep 02 '19 19:09 philip-alldredge

I've been working on android-ndk. Although I haven't pushed it to Crates.io yet, I currently have the following:

extern crate jni;

/// An `ANativeActivity *`
struct NativeActivity { ... }

impl NativeActivity {
    // other methods ...

    pub fn vm(&self) -> jni::JavaVM { ... }
}

It's usable like let env = native_activity.vm().attach_current_thread(); .... Would this (together with an interface to android-ndk from android-rs-glue) solve what you're looking for?

mb64 avatar Sep 15 '19 19:09 mb64

Yes, this would most likely solve what I'm looking for. I'll try this as soon as possible.

OptimisticPeach avatar Sep 17 '19 01:09 OptimisticPeach

I believe this should have a simple answer and I'm just missing it, but how might I acquire a *const ANativeActivity to create a NativeActivity? I'm currently using my injected function talked about earlier, but even then I'm running into some errors due to threading.

Trying to use some kind of lazy_static doesn't work because NonNull isn't thread safe, and using a thread_local doesn't work either because we call onWindowFocusChanged on a separate thread in the C++ code.

OptimisticPeach avatar Sep 18 '19 20:09 OptimisticPeach

Yes, this does require changes to android-rs-glue to provide access to these things. That's part of the plan of #228, which I'll rebase/fix once I feel like android-ndk is in a usable enough state.

I think that ideally, you would get a struct android_app * from android_glue, and android-ndk would provide a nice wrapper for it that gives access to the ANativeActivity, AConfiguration, ANativeWindow, saved state, etc.

mb64 avatar Sep 20 '19 01:09 mb64

That sounds wonderful, meaning that I can get a pointer to it from any thread!

OptimisticPeach avatar Sep 20 '19 01:09 OptimisticPeach

Yes, this does require changes to android-rs-glue to provide access to these things. That's part of the plan of #228, which I'll get to rebasing/fixing soon.

The plan is that android_glue gives you a struct android_app *, and android-ndk provides a nice wrapper for it that gives access to the ANativeActivity, AConfiguration, ANativeWindow, saved state, etc. I recently pushed a new version of android-ndk that has most of this; the docs are here.

mb64 avatar Sep 30 '19 02:09 mb64