Expose Java main/UI Looper via AndroidApp::java_main_looper

This makes it possible to register file descriptors that can wake up the
Java main / UI thread as well as callbacks that will run on the Java
main / UI thread.

Although it can be common to refer to this thread as the "main" thread,
we choose to explicitly refer to it as the "java main" thread thread in
the API to avoid confusion with the Rust thread that runs
"android_main".

Co-authored-by: Robert Bragg <robert@sixbynine.org>
This commit is contained in:
Mark Kimsal
2025-05-06 18:30:46 -04:00
committed by Robert Bragg
parent 0062cfc7a0
commit 2a05cd2763
4 changed files with 83 additions and 30 deletions
+25 -4
View File
@@ -107,7 +107,11 @@ impl StateLoader<'_> {
}
impl AndroidApp {
pub(crate) unsafe fn from_ptr(ptr: NonNull<ffi::android_app>, jvm: jni::JavaVM) -> Self {
pub(crate) fn new(
ptr: NonNull<ffi::android_app>,
jvm: jni::JavaVM,
main_looper: ndk::looper::ForeignLooper,
) -> Self {
// We attach to the thread before creating the AndroidApp
jvm.with_local_frame(10, |env| -> jni::errors::Result<_> {
if let Err(err) = crate::input::jni_init(env) {
@@ -117,8 +121,9 @@ impl AndroidApp {
// Note: we don't use from_ptr since we don't own the android_app.config
// and need to keep in mind that the Drop handler is going to call
// AConfiguration_delete()
let config =
Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config));
let config = unsafe {
Configuration::clone_from_ptr(NonNull::new_unchecked((*ptr.as_ptr()).config))
};
Ok(Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
@@ -126,6 +131,7 @@ impl AndroidApp {
native_app: NativeAppGlue { ptr },
config: ConfigurationRef::new(config),
native_window: Default::default(),
main_looper,
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
@@ -279,6 +285,10 @@ pub struct AndroidAppInner {
config: ConfigurationRef,
native_window: RwLock<Option<NativeWindow>>,
/// Looper associated with the activity's Java main thread, sometimes called
/// the UI thread.
main_looper: ndk::looper::ForeignLooper,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
/// characters
@@ -305,6 +315,10 @@ impl AndroidAppInner {
self.native_window.read().unwrap().clone()
}
pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
self.main_looper.clone()
}
pub fn poll_events<F>(&self, timeout: Option<Duration>, mut callback: F)
where
F: FnMut(PollEvent),
@@ -981,6 +995,12 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
};
// Note: At this point we can assume jni::JavaVM::singleton is initialized
let main_looper = unsafe {
ndk::looper::ForeignLooper::from_ptr(
std::ptr::NonNull::new((*native_app).mainLooper).unwrap(),
)
};
// Note: the GameActivity implementation will have already attached the main thread to the
// JVM before calling _rust_glue_entry so we don't to set the thread name via
// attach_current_thread_with_config since that won't actually create a new attachment.
@@ -996,7 +1016,8 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) {
}
unsafe {
let app = AndroidApp::from_ptr(NonNull::new(native_app).unwrap(), jvm.clone());
let app =
AndroidApp::new(NonNull::new(native_app).unwrap(), jvm.clone(), main_looper);
// We want to specifically catch any panic from the application's android_main
// so we can finish + destroy the Activity gracefully via the JVM
catch_unwind(|| {
+16
View File
@@ -562,6 +562,22 @@ impl AndroidApp {
self.inner.read().unwrap().native_window()
}
/// Returns a [`ndk::looper::ForeignLooper`] associated with the Java
/// main / UI thread.
///
/// This can be used to register file descriptors that may wake up the
/// Java main / UI thread and optionally run callbacks on that thread.
///
/// ```ignore
/// # use ndk;
/// # let app: AndroidApp = todo!();
/// let looper = app.java_main_looper();
/// looper.add_fd_with_callback(todo!(), ndk::looper::FdEvent::INPUT, todo!()).unwrap();
/// ```
pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
self.inner.read().unwrap().java_main_looper().clone()
}
/// Returns a pointer to the Java Virtual Machine, for making JNI calls
///
/// This returns a pointer to the Java Virtual Machine which can be used
+10 -3
View File
@@ -845,6 +845,9 @@ extern "C" fn ANativeActivity_onCreate(
activity, saved_state, saved_state_size
);
let main_looper =
ndk::looper::ForeignLooper::for_thread().expect("Failed to get Java main looper");
// Conceptually we associate a glue reference with the JVM main thread, and another
// reference with the Rust main thread
let jvm_glue = NativeActivityGlue::new(activity, saved_state, saved_state_size);
@@ -856,7 +859,7 @@ extern "C" fn ANativeActivity_onCreate(
// Note: we drop the thread handle which will detach the thread
std::thread::spawn(move || {
let activity: *mut ndk_sys::ANativeActivity = activity_ptr as *mut _;
rust_glue_entry(rust_glue, activity);
rust_glue_entry(rust_glue, activity, main_looper);
});
// Wait for thread to start.
@@ -870,7 +873,11 @@ extern "C" fn ANativeActivity_onCreate(
})
}
fn rust_glue_entry(rust_glue: NativeActivityGlue, activity: *mut ndk_sys::ANativeActivity) {
fn rust_glue_entry(
rust_glue: NativeActivityGlue,
activity: *mut ndk_sys::ANativeActivity,
main_looper: ndk::looper::ForeignLooper,
) {
abort_on_panic(|| {
let (jvm, jni_activity) = unsafe {
let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast();
@@ -897,7 +904,7 @@ fn rust_glue_entry(rust_glue: NativeActivityGlue, activity: *mut ndk_sys::ANativ
);
}
let app = AndroidApp::new(rust_glue.clone(), jvm.clone());
let app = AndroidApp::new(rust_glue.clone(), jvm.clone(), main_looper);
rust_glue.notify_main_thread_running();
+32 -23
View File
@@ -62,34 +62,40 @@ impl StateLoader<'_> {
}
impl AndroidApp {
pub(crate) fn new(native_activity: NativeActivityGlue, jvm: JavaVM) -> Self {
pub(crate) fn new(
native_activity: NativeActivityGlue,
jvm: JavaVM,
main_looper: ndk::looper::ForeignLooper,
) -> Self {
jvm.with_local_frame(10, |env| -> jni::errors::Result<_> {
if let Err(err) = crate::input::jni_init(env) {
panic!("Failed to init JNI bindings: {err:?}");
};
let looper = unsafe {
let ptr = ndk_sys::ALooper_prepare(
ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
);
ndk::looper::ForeignLooper::from_ptr(ptr::NonNull::new(ptr).unwrap())
};
let app = Self {
inner: Arc::new(RwLock::new(AndroidAppInner {
jvm: jvm.clone(),
native_activity,
looper: Looper {
ptr: ptr::null_mut(),
},
looper,
main_looper,
key_maps: Mutex::new(HashMap::new()),
input_receiver: Mutex::new(None),
})),
};
{
let mut guard = app.inner.write().unwrap();
let guard = app.inner.write().unwrap();
let main_fd = guard.native_activity.cmd_read_fd();
unsafe {
guard.looper.ptr = ndk_sys::ALooper_prepare(
ndk_sys::ALOOPER_PREPARE_ALLOW_NON_CALLBACKS as libc::c_int,
);
ndk_sys::ALooper_addFd(
guard.looper.ptr,
guard.looper.ptr().as_ptr(),
main_fd,
LOOPER_ID_MAIN,
ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int,
@@ -106,19 +112,18 @@ impl AndroidApp {
}
}
#[derive(Debug)]
struct Looper {
pub ptr: *mut ndk_sys::ALooper,
}
unsafe impl Send for Looper {}
unsafe impl Sync for Looper {}
#[derive(Debug)]
pub(crate) struct AndroidAppInner {
pub(crate) jvm: JavaVM,
pub(crate) native_activity: NativeActivityGlue,
looper: Looper,
/// Looper associated with the Rust `android_main` thread
looper: ndk::looper::ForeignLooper,
/// Looper associated with the activity's Java main thread, sometimes called
/// the UI thread.
main_looper: ndk::looper::ForeignLooper,
/// A table of `KeyCharacterMap`s per `InputDevice` ID
/// these are used to be able to map key presses to unicode
@@ -145,8 +150,12 @@ impl AndroidAppInner {
self.native_activity.activity
}
pub(crate) fn looper(&self) -> *mut ndk_sys::ALooper {
self.looper.ptr
pub(crate) fn looper_as_ptr(&self) -> *mut ndk_sys::ALooper {
self.looper.ptr().as_ptr()
}
pub fn java_main_looper(&self) -> ndk::looper::ForeignLooper {
self.main_looper.clone()
}
pub fn native_window(&self) -> Option<NativeWindow> {
@@ -173,7 +182,7 @@ impl AndroidAppInner {
trace!("Calling ALooper_pollOnce, timeout = {timeout_milliseconds}");
assert_eq!(
ndk_sys::ALooper_forThread(),
self.looper.ptr,
self.looper_as_ptr(),
"Application tried to poll events from non-main thread"
);
let id = ndk_sys::ALooper_pollOnce(
@@ -245,7 +254,7 @@ impl AndroidAppInner {
trace!("Calling pre_exec_cmd({ipc_cmd:#?})");
self.native_activity.pre_exec_cmd(
ipc_cmd,
self.looper(),
self.looper_as_ptr(),
LOOPER_ID_INPUT,
);
@@ -282,7 +291,7 @@ impl AndroidAppInner {
pub fn create_waker(&self) -> AndroidAppWaker {
// Safety: we know that the looper is a valid, non-null pointer
unsafe { AndroidAppWaker::new(self.looper.ptr) }
unsafe { AndroidAppWaker::new(self.looper_as_ptr()) }
}
pub fn config(&self) -> ConfigurationRef {
@@ -405,7 +414,7 @@ impl AndroidAppInner {
// trigger a wake up)
let queue = self
.native_activity
.looper_attached_input_queue(self.looper(), LOOPER_ID_INPUT);
.looper_attached_input_queue(self.looper_as_ptr(), LOOPER_ID_INPUT);
// Note: we don't treat it as an error if there is no queue, so if applications
// iterate input before a queue has been created (e.g. before onStart) then