rust-mobile / android-rs-glue

Glue between Rust and Android
Apache License 2.0
904 stars 109 forks source link

Expose nicer JNI/NDK interface #238

Open OptimisticPeach opened 5 years ago

OptimisticPeach commented 5 years ago

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

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.

mb64 commented 5 years ago

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:

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())

OptimisticPeach commented 5 years ago

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.

philip-alldredge commented 5 years ago

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.

mb64 commented 4 years ago

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?

OptimisticPeach commented 4 years ago

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

OptimisticPeach commented 4 years ago

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.

mb64 commented 4 years ago

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.

OptimisticPeach commented 4 years ago

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

mb64 commented 4 years ago

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.