diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index b1c1713..47b05ad 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - input: TextInputAction enum representing action button types on soft keyboards. - input: InputEvent::TextAction event for handling action button presses from soft keyboards. - The `ndk` and `ndk-sys` crates are now re-exported under `android_activity::ndk` and `android_activity::ndk_sys` ([#194](https://github.com/rust-mobile/android-activity/pull/194)) +- `AndroidApp::java_main_looper()` gives access to the `ALooper` for the Java main / UI thread ([#198](https://github.com/rust-mobile/android-activity/pull/198)) +- `AndroidApp::run_on_java_main_thread()` can be used to run boxed closures on the Java main / UI thread ([#232](https://github.com/rust-mobile/android-activity/pull/232)) ### Changed - rust-version bumped to 1.85.0 ([#193](https://github.com/rust-mobile/android-activity/pull/193), [#219](https://github.com/rust-mobile/android-activity/pull/219)) diff --git a/android-activity/Cargo.toml b/android-activity/Cargo.toml index c13408b..44f474d 100644 --- a/android-activity/Cargo.toml +++ b/android-activity/Cargo.toml @@ -28,7 +28,7 @@ api-level-30 = ["ndk/api-level-30"] [dependencies] log = "0.4" simd_cesu8 = { version = "1.0.1", optional = true } -jni = "0.22.2" +jni = "0.22.4" ndk-sys = "0.6.0" ndk = { version = "0.9.0", default-features = false } ndk-context = "0.1.1" @@ -41,6 +41,9 @@ thiserror = "1" [build-dependencies] cc = { version = "1.0.42", features = ["parallel"] } +[dev-dependencies] +jni = "0.22.4" + [package.metadata.docs.rs] targets = [ "aarch64-linux-android", diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index e7c40b0..67d6525 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -22,6 +22,7 @@ use ndk::configuration::Configuration; use ndk::native_window::NativeWindow; use crate::error::InternalResult; +use crate::main_callbacks::MainCallbacks; use crate::util::{ abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic, try_get_path_from_ptr, @@ -111,6 +112,7 @@ impl AndroidApp { ptr: NonNull, jvm: jni::JavaVM, main_looper: ndk::looper::ForeignLooper, + main_callbacks: MainCallbacks, ) -> Self { // We attach to the thread before creating the AndroidApp jvm.with_local_frame(10, |env| -> jni::errors::Result<_> { @@ -132,6 +134,7 @@ impl AndroidApp { config: ConfigurationRef::new(config), native_window: Default::default(), main_looper, + main_callbacks, key_maps: Mutex::new(HashMap::new()), input_receiver: Mutex::new(None), })), @@ -285,6 +288,8 @@ pub struct AndroidAppInner { config: ConfigurationRef, native_window: RwLock>, + pub(crate) main_callbacks: MainCallbacks, + /// Looper associated with the activity's Java main thread, sometimes called /// the UI thread. main_looper: ndk::looper::ForeignLooper, @@ -623,6 +628,13 @@ impl AndroidAppInner { } } + pub fn run_on_java_main_thread(&self, f: Box) + where + F: FnOnce() + Send + 'static, + { + self.main_callbacks.run_on_java_main_thread(f); + } + pub fn config(&self) -> ConfigurationRef { self.config.clone() } @@ -1011,13 +1023,23 @@ pub unsafe extern "C" fn _rust_glue_entry(native_app: *mut ffi::android_app) { // SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance let jni_activity = unsafe { env.as_cast_raw::>(&jni_activity)? }; - if let Err(err) = init_android_main_thread(&jvm, &jni_activity) { - eprintln!("Failed to name Java thread and set thread context class loader: {err}"); - } + let main_callbacks = match init_android_main_thread(&jvm, &jni_activity, &main_looper) { + Ok(main_callbacks) => main_callbacks, + Err(err) => { + eprintln!( + "Failed to name Java thread and set thread context class loader: {err}" + ); + return Err(err); + } + }; unsafe { - let app = - AndroidApp::new(NonNull::new(native_app).unwrap(), jvm.clone(), main_looper); + let app = AndroidApp::new( + NonNull::new(native_app).unwrap(), + jvm.clone(), + main_looper, + main_callbacks, + ); // 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(|| { diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index fb81dfb..e6f7a56 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -190,6 +190,8 @@ mod jni_utils; mod waker; pub use waker::AndroidAppWaker; +mod main_callbacks; + /// A rectangle with integer edge coordinates. Used to represent window insets, for example. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Rect { @@ -678,6 +680,91 @@ impl AndroidApp { self.inner.read().unwrap().create_waker() } + /// Runs the given closure on the Java main / UI thread. + /// + /// This is useful for performing operations that must be executed on the + /// main thread, such as interacting with Android SDK APIs that require + /// execution on the main thread. + /// + /// Any panic within the closure will be caught and logged as an error, + /// (assuming your application is built to allow unwinding). + /// + /// The thread will be attached to the JVM (for using JNI) and any + /// un-cleared Java exceptions left over by the callback will be caught, + /// cleared and logged as an error. + /// + /// There is no built-in mechanism to propagate results back to the caller + /// but you can use channels or other synchronization primitives that you + /// capture. + /// + /// It's important to avoid blocking the `android_main` thread while waiting + /// for any results because this could lead to deadlocks for `Activity` + /// callbacks that require a synchronous response for the `android_activity` + /// thread. + /// + /// # Example + /// + /// This example demonstrates using the `jni` 0.22 API to show a toast + /// message from the Java main thread. + /// + /// ```no_run + /// use android_activity::AndroidApp; + /// use jni::{objects::JString, refs::Global}; + /// + /// jni::bind_java_type! { Context => "android.content.Context" } + /// jni::bind_java_type! { + /// Activity => "android.app.Activity", + /// type_map { + /// Context => "android.content.Context", + /// }, + /// is_instance_of { + /// context: Context + /// }, + /// } + /// + /// jni::bind_java_type! { + /// Toast => "android.widget.Toast", + /// type_map { + /// Context => "android.content.Context", + /// }, + /// methods { + /// static fn make_text(context: Context, text: JCharSequence, duration: i32) -> Toast, + /// fn show(), + /// } + /// } + /// + /// enum ToastDuration { + /// Short = 0, + /// Long = 1, + /// } + /// + /// fn send_toast(outer_app: &AndroidApp, msg: impl AsRef, duration: ToastDuration) { + /// let app = outer_app.clone(); + /// let msg = msg.as_ref().to_string(); + /// outer_app.run_on_java_main_thread(Box::new(move || { + /// let jvm = unsafe { jni::JavaVM::from_raw(app.vm_as_ptr() as _) }; + /// // As an micro optimization you could use jvm.with_top_local_frame, since we know + /// // we're already attached + /// if let Err(err) = jvm.attach_current_thread(|env| -> jni::errors::Result<()> { + /// let activity: jni::sys::jobject = app.activity_as_ptr() as _; + /// let activity = unsafe { env.as_cast_raw::>(&activity)? }; + /// let message = JString::new(env, &msg)?; + /// let toast = Toast::make_text(env, activity.as_ref(), &message, duration as i32)?; + /// toast.show(env)?; + /// Ok(()) + /// }) { + /// log::error!("Failed to show toast on main thread: {err:?}"); + /// } + /// })); + /// } + /// ``` + pub fn run_on_java_main_thread(&self, f: Box) + where + F: FnOnce() + Send + 'static, + { + self.inner.read().unwrap().run_on_java_main_thread(f); + } + /// Returns a **reference** to this application's [`ndk::configuration::Configuration`]. /// /// # Warning diff --git a/android-activity/src/main_callbacks.rs b/android-activity/src/main_callbacks.rs new file mode 100644 index 0000000..a731cf2 --- /dev/null +++ b/android-activity/src/main_callbacks.rs @@ -0,0 +1,226 @@ +use jni::vm::JavaVM; +use std::{ + ffi::c_void, + panic::{catch_unwind, AssertUnwindSafe}, + sync::{atomic::AtomicBool, Arc, Mutex, Weak}, +}; + +use crate::util::abort_on_panic; + +struct CallbackBuffers { + pub front: Vec>, + pub back: Vec>, +} + +impl std::fmt::Debug for CallbackBuffers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CallbackBuffers") + .field("front", &self.front.len()) + .field("back", &self.back.len()) + .finish() + } +} + +impl CallbackBuffers { + pub fn take_front(&mut self) -> Vec> { + std::mem::swap(&mut self.front, &mut self.back); + std::mem::take(&mut self.back) + } + + // After calling `take_front` and draining callbacks then the empty + // vec should be put back so the capacity can be reused + // + // The given `back` vector must be empty + pub fn replace_back(&mut self, back: Vec>) { + assert!(back.is_empty()); + self.back = back; + } +} + +#[derive(Debug)] +pub(crate) struct MainCallbacksState { + _pending_detach: AtomicBool, + event_fd: libc::c_int, + callbacks: Mutex, +} + +impl Drop for MainCallbacksState { + fn drop(&mut self) { + eprintln!("Dropping MainCallbacksState"); + log::warn!("Dropping MainCallbacksState"); + } +} + +#[derive(Debug, Clone)] +pub(crate) struct MainCallbacks { + inner: Arc, +} + +impl std::ops::Deref for MainCallbacks { + type Target = MainCallbacksState; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl MainCallbacks { + pub fn new(java_main_looper: &ndk::looper::ForeignLooper) -> Self { + let java_main_callbacks_event_fd = + unsafe { libc::eventfd(0, libc::EFD_NONBLOCK | libc::EFD_CLOEXEC) }; + assert_ne!( + java_main_callbacks_event_fd, -1, + "Failed to create Java main looper event fd" + ); + + let inner = Arc::new(MainCallbacksState { + _pending_detach: AtomicBool::new(false), + event_fd: java_main_callbacks_event_fd, + callbacks: Mutex::new(CallbackBuffers { + front: Vec::new(), + back: Vec::new(), + }), + }); + + let weak = Arc::downgrade(&inner); + let weak = weak.into_raw(); + unsafe { + ndk_sys::ALooper_addFd( + java_main_looper.ptr().as_ptr(), + java_main_callbacks_event_fd, + ndk_sys::ALOOPER_POLL_CALLBACK, + ndk_sys::ALOOPER_EVENT_INPUT as libc::c_int, + Some(run_java_main_callbacks), + weak as _, + ); + } + + Self { inner } + } + + pub fn wake_java_main_for_callbacks(&self) { + let count: u64 = 1; + + loop { + match unsafe { + libc::write(self.event_fd, &count as *const _ as *const libc::c_void, 8) + } { + 8 => break, + -1 => { + let err = std::io::Error::last_os_error(); + if err.kind() != std::io::ErrorKind::Interrupted { + log::error!("Failure waking up java main loop: {}", err); + return; + } + } + count => { + log::error!("Spurious write of {count} bytes while waking up java main loop"); + return; + } + } + } + } + + pub fn run_on_java_main_thread(&self, f: Box) + where + F: FnOnce() + Send + 'static, + { + { + let mut guard = self.callbacks.lock().unwrap(); + guard.front.push(f); + } + + self.wake_java_main_for_callbacks(); + } + + // Asynchronously detach the callbacks event fd from the Java main looper + // + // Note: we can't do this synchronously because ALooper_removeFd can't + // guarantee that there isn't already a callback pending (which will still + // require a valid data pointer) + // + // Since the java main Looper runs for the lifetime of the application + // process we never actually expect to detach the callbacks event fd, and in + // the unlikely case where there is no future callback after calling + // `wake_java_main_for_callbacks` then the event fd and `MainCallbacks` will + // be leaked - but the implication is that the process is about to terminate + // (otherwise the Looper would still be running) + pub fn _detach_callbacks_event_fd_from_java_main_looper(&mut self) { + self._pending_detach + .store(true, std::sync::atomic::Ordering::SeqCst); + self.wake_java_main_for_callbacks(); + } +} + +unsafe extern "C" fn run_java_main_callbacks(fd: i32, events: i32, data: *mut c_void) -> i32 { + abort_on_panic(|| { + // Reset the eventfd counter + if events & ndk_sys::ALOOPER_EVENT_INPUT as i32 != 0 { + let counter: u64 = 0; + loop { + match unsafe { libc::read(fd, &counter as *const _ as *mut libc::c_void, 8) } { + 8 => break, + -1 => { + let error = std::io::Error::last_os_error(); + if error.kind() != std::io::ErrorKind::Interrupted { + log::error!("Error reading from fd: {:?}", error); + break; + } + } + count => { + log::error!("Unexpected read count from event fd: {}", count); + } + } + } + } + + let weak_ptr: *const MainCallbacksState = data.cast(); + let weak_ref = Weak::from_raw(weak_ptr); + let maybe_upgraded = weak_ref.upgrade(); + + // Make sure we don't Drop the Weak reference (so the data pointer + // remains valid for future callbacks) + let _ = weak_ref.into_raw(); + + if let Some(main_callbacks) = maybe_upgraded { + if main_callbacks + ._pending_detach + .load(std::sync::atomic::Ordering::SeqCst) + { + let _ = unsafe { libc::close(main_callbacks.event_fd) }; + let _drop_weak = Weak::from_raw(weak_ptr); + // Returning zero indicates that the fd / callback should be + // removed from the Looper + return 0; + } + + let mut callbacks = main_callbacks.callbacks.lock().unwrap().take_front(); + + let jvm = JavaVM::singleton().unwrap(); + + for callback in callbacks.drain(0..) { + let res = jvm.attach_current_thread(|_env| -> jni::errors::Result<()> { + let res = catch_unwind(AssertUnwindSafe(|| { + callback(); + })); + if let Err(err) = res { + log::error!("Panic in Java main/UI thread callback: {:?}", err); + } + Ok(()) + }); + if let Err(err) = res { + log::error!( + "JNI Error while running Java main/UI thread callback: {:?}", + err + ); + } + } + + // put callbacks vec back so we can keep reusing its capacity + let mut guard = main_callbacks.callbacks.lock().unwrap(); + guard.replace_back(callbacks); + } + + 1 + }) +} diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs index 2323a05..517928f 100644 --- a/android-activity/src/native_activity/glue.rs +++ b/android-activity/src/native_activity/glue.rs @@ -898,13 +898,19 @@ fn rust_glue_entry( // SAFETY: We know jni_activity is a valid JNI global ref to an Activity instance let jni_activity = unsafe { env.as_cast_raw::>(&jni_activity)? }; - if let Err(err) = init_android_main_thread(&jvm, &jni_activity) { - eprintln!( - "Failed to name Java thread and set thread context class loader: {err}" - ); - } + let main_callbacks = + match init_android_main_thread(&jvm, &jni_activity, &main_looper) { + Ok(callbacks) => callbacks, + Err(err) => { + eprintln!( + "Failed to name Java thread and set thread context class loader: {err}" + ); + return Err(err); + } + }; - let app = AndroidApp::new(rust_glue.clone(), jvm.clone(), main_looper); + let app = + AndroidApp::new(rust_glue.clone(), jvm.clone(), main_looper, main_callbacks); rust_glue.notify_main_thread_running(); diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs index becef88..1f29836 100644 --- a/android-activity/src/native_activity/mod.rs +++ b/android-activity/src/native_activity/mod.rs @@ -13,6 +13,7 @@ use ndk::input_queue::InputQueue; use ndk::{asset::AssetManager, native_window::NativeWindow}; use crate::error::InternalResult; +use crate::main_callbacks::MainCallbacks; use crate::{ util, AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags, @@ -66,6 +67,7 @@ impl AndroidApp { native_activity: NativeActivityGlue, jvm: JavaVM, main_looper: ndk::looper::ForeignLooper, + main_callbacks: MainCallbacks, ) -> Self { jvm.with_local_frame(10, |env| -> jni::errors::Result<_> { if let Err(err) = crate::input::jni_init(env) { @@ -84,6 +86,7 @@ impl AndroidApp { native_activity, looper, main_looper, + main_callbacks, key_maps: Mutex::new(HashMap::new()), input_receiver: Mutex::new(None), })), @@ -118,6 +121,8 @@ pub(crate) struct AndroidAppInner { pub(crate) native_activity: NativeActivityGlue, + main_callbacks: MainCallbacks, + /// Looper associated with the Rust `android_main` thread looper: ndk::looper::ForeignLooper, @@ -294,6 +299,13 @@ impl AndroidAppInner { unsafe { AndroidAppWaker::new(self.looper_as_ptr()) } } + pub fn run_on_java_main_thread(&self, f: Box) + where + F: FnOnce() + Send + 'static, + { + self.main_callbacks.run_on_java_main_thread(f); + } + pub fn config(&self) -> ConfigurationRef { self.native_activity.config() } diff --git a/android-activity/src/util.rs b/android-activity/src/util.rs index 22ed657..25d1529 100644 --- a/android-activity/src/util.rs +++ b/android-activity/src/util.rs @@ -15,6 +15,8 @@ use std::{ sync::OnceLock, }; +use crate::main_callbacks::MainCallbacks; + pub fn try_get_path_from_ptr(path: *const c_char) -> Option { if path.is_null() { return None; @@ -118,10 +120,14 @@ pub(crate) fn abort_on_panic(f: impl FnOnce() -> R) -> R { }) } -static NDK_CONTEXT_ONCE: OnceLock<()> = OnceLock::new(); +struct AppState { + main_callbacks: MainCallbacks, +} + +static APP_ONCE: OnceLock = OnceLock::new(); // Get the Application instance from the Activity -fn get_application<'local, 'any>( +pub(crate) fn get_application<'local, 'any>( env: &mut jni::Env<'local>, activity: &JObject<'any>, ) -> jni::errors::Result> { @@ -136,40 +142,53 @@ fn get_application<'local, 'any>( Ok(app) } +fn try_init_current_thread(env: &mut jni::Env, activity: &JObject) -> jni::errors::Result<()> { + let activity_class = env.get_object_class(activity)?; + let class_loader = activity_class.get_class_loader(env)?; + + let thread = JThread::current_thread(env)?; + thread.set_context_class_loader(env, &class_loader)?; + let thread_name = JString::from_jni_str(env, jni_str!("android_main"))?; + thread.set_name(env, &thread_name)?; + + // Also name native thread - this needs to happen here after attaching to a JVM thread, + // since that changes the thread name to something like "Thread-2". + unsafe { + let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap(); + let _ = libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr()); + } + Ok(()) +} + /// Name the Java Thread + native thread "android_main" and set the Java Thread context class loader /// so that jni code can more-easily find non-system Java classes. pub(crate) fn init_android_main_thread( vm: &JavaVM, jni_activity: &JObject, -) -> jni::errors::Result<()> { - vm.with_local_frame(10, |env| -> jni::errors::Result<()> { - let activity_class = env.get_object_class(jni_activity)?; + java_main_looper: &ndk::looper::ForeignLooper, +) -> jni::errors::Result { + vm.with_local_frame(10, |env| -> jni::errors::Result<_> { + let app_state = APP_ONCE.get_or_init(|| unsafe { + let application = + get_application(env, jni_activity).expect("Failed to get Application instance"); + let app_global = env + .new_global_ref(application) + .expect("Failed to create global ref for Application"); + // Make sure we don't delete the global reference via Drop + let app_global = app_global.into_raw(); + ndk_context::initialize_android_context(vm.get_raw().cast(), app_global.cast()); - if let Ok(application) = get_application(env, jni_activity) { - NDK_CONTEXT_ONCE.get_or_init(|| unsafe { - let app_global = env - .new_global_ref(application) - .expect("Failed to create global ref for Application"); - // Make sure we don't delete the global reference via Drop - let app_global = app_global.into_raw(); - ndk_context::initialize_android_context(vm.get_raw().cast(), app_global.cast()); - }); + let main_callbacks = MainCallbacks::new(java_main_looper); + + AppState { main_callbacks } + }); + + if let Err(err) = try_init_current_thread(env, jni_activity) { + eprintln!("Failed to initialize Java thread state: {:?}", err); } - let class_loader = activity_class.get_class_loader(env)?; + let main_callbacks = app_state.main_callbacks.clone(); - let thread = JThread::current_thread(env)?; - thread.set_context_class_loader(env, &class_loader)?; - let thread_name = JString::from_jni_str(env, jni_str!("android_main"))?; - thread.set_name(env, &thread_name)?; - - // Also name native thread - this needs to happen here after attaching to a JVM thread, - // since that changes the thread name to something like "Thread-2". - unsafe { - let thread_name = std::ffi::CStr::from_bytes_with_nul(b"android_main\0").unwrap(); - let _ = libc::pthread_setname_np(libc::pthread_self(), thread_name.as_ptr()); - } - - Ok(()) + Ok(main_callbacks) }) }