From c1d00b919159347afb6bbd8aa94f05e87d33fd28 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Wed, 18 Mar 2026 01:39:40 +0000 Subject: [PATCH] Support an optional 'android_on_create' entrypoint This adds support for an optional `android_on_crate` entrypoint which is called from within the Activity.onCreate native method callback from the Java main / UI thread. This gives applications an opportunity initialize state while the `Activity`'s class loader is on the stack, so `FindClass` will be able to find application classes. This can be a more-convenient place to initialize JNI bindings, without needing to explicitly get the class loader from the Activity to be able to look up application classes from the android_main thread. This may also be convenient for initially using JNI to interact with your new Activity in case you need to use SDK APIs that are only safe to use from the Java main / UI thread. The moves the thread initialization functions out of util.rs into a new init.rs While adding documentation for this feature, this also does a more-general pass over the top-level crate documentation to try and ensure it's up-to-date. Fixes: #169 Addresses: #82 --- README.md | 60 ++- android-activity/CHANGELOG.md | 36 ++ android-activity/src/game_activity/mod.rs | 19 +- android-activity/src/init.rs | 323 ++++++++++++++++ android-activity/src/lib.rs | 367 +++++++++++++++++-- android-activity/src/native_activity/glue.rs | 24 +- android-activity/src/util.rs | 168 +-------- 7 files changed, 770 insertions(+), 227 deletions(-) create mode 100644 android-activity/src/init.rs diff --git a/README.md b/README.md index b8bd557..ec1eb6e 100644 --- a/README.md +++ b/README.md @@ -37,22 +37,33 @@ Cargo.toml [dependencies] log = "0.4" android_logger = "0.13" -android-activity = { version = "0.5", features = [ "native-activity" ] } +android-activity = { version = "0.6", features = [ "native-activity" ] } [lib] -crate_type = ["cdylib"] +crate-type = ["cdylib"] ``` -_Note: that you will need to either specify the **"native-activity"** feature or **"game-activity"** feature to identify which `Activity` base class your application is based on_ +_Note: that you will need to either specify the **"native-activity"** feature or +**"game-activity"** feature to identify which `Activity` base class your +application is based on_ lib.rs ```rust use android_activity::{AndroidApp, InputStatus, MainEvent, PollEvent}; -#[no_mangle] +#[unsafe(no_mangle)] fn android_main(app: AndroidApp) { - android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info)); + + // `android_main` is tied to your `Activity` lifecycle, not your application lifecycle + // and so it may be called multiple times if your activity is destroyed and recreated. + // + // Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state + // multiple times. + static APP_ONCE: OnceLock<()> = OnceLock::new(); + APP_ONCE.get_or_init(|| { + android_logger::init_once(android_logger::Config::default().with_min_level(log::Level::Info)); + }); loop { app.poll_events(Some(std::time::Duration::from_millis(500)) /* timeout */, |event| { @@ -62,6 +73,11 @@ fn android_main(app: AndroidApp) { PollEvent::Main(main_event) => { log::info!("Main event: {:?}", main_event); match main_event { + // Once you receive a `Destroy` event, your `AndroidApp` will no longer + // be associated with any `Activity` and it's methods will effectively be no-ops. + // + // You should return from `android_main` and if your `Activity` gets recreated then + // a new `AndroidApp` will be passsed to a new invocation of `android_main`. MainEvent::Destroy => { return; } _ => {} } @@ -85,6 +101,36 @@ cargo apk run adb logcat example:V *:S ``` +## Optional `android_on_create` entry point + +`android-activity` also supports an optional `android_on_create` entry point that gets called from the +`Activity.onCreate()` callback before `android_main()` is called, allowing for doing some setup work on the Java main +thread before the main Rust code starts running. + +For example: + +```rust +use std::sync::OnceLock; +use jni::{JavaVM, objects::JObject}; + +#[unsafe(no_mangle)] +fn android_on_create(state: &android_activity::OnCreateState) { + + // `android_on_create` is tied to your `Activity` lifecycle, not your application lifecycle + // and so it may be called multiple times if your activity is destroyed and recreated. + // + // Use a `OnceLock` or similar to ensure that you don't attempt to initialize global state + // multiple times. + static APP_ONCE: OnceLock<()> = OnceLock::new(); + APP_ONCE.get_or_init(|| { + // Initialize logging... + }); + let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) }; + let activity = state.activity_as_ptr() as jni::sys::jobject; + // Do some other setup work on the Java main thread before `android_main` starts running +} +``` + ## Full Examples See [this collection of examples](https://github.com/rust-mobile/rust-android-examples) (based on both `GameActivity` and `NativeActivity`). @@ -111,7 +157,7 @@ Even if you start out using `NativeActivity` for the convenience, it's likely th Firstly; if you have a [Winit](https://crates.io/crates/winit) based application and also have an explicit dependency on `ndk-glue` your application will need to remove its dependency on `ndk-glue` for the 0.28 release of Winit which will be based on android-activity (Since glue crates, due to their nature, can't be compatible with alternative glue crates). -Winit-based applications can follow the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[no_mangle]fn android_main(app: AndroidApp)` entry point. +Winit-based applications can follow the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) guidance for advice on how to switch over. Most Winit-based applications should aim to remove any explicit dependency on a specific glue crate (so not depend directly on `ndk-glue` or `android-activity` and instead rely on Winit to pull in the right glue crate). The main practical change will then be to add a `#[unsafe(no_mangle)]fn android_main(app: AndroidApp)` entry point. See the [Android documentation](https://docs.rs/winit/latest/winit/platform/android/index.html) for more details and also see the [Winit-based examples here](https://github.com/rust-mobile/rust-android-examples). @@ -126,7 +172,7 @@ Middleware libraries can instead look at using the [ndk-context](https://crates. The steps to switch a simple standalone application over from `ndk-glue` to `android-activity` (still based on `NativeActivity`) should be: 1. Remove `ndk-glue` from your Cargo.toml -2. Add a dependency on `android-activity`, like `android-activity = { version="0.5", features = [ "native-activity" ] }` +2. Add a dependency on `android-activity`, like `android-activity = { version="0.6", features = [ "native-activity" ] }` 3. Optionally add a dependency on `android_logger = "0.13.0"` 4. Update the `main` entry point to look like this: diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index 837efd8..ce29451 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -13,6 +13,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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)) +- Support for an optional `android_on_create` entry point that gets called from the `Activity.onCreate()` callback before `android_main()` is called, allowing for doing some setup work on the Java main / UI thread before the `android_main` Rust code starts running. + +For example: + +```rust +use std::sync::OnceLock; +use android_activity::OnCreateState; +use jni::{JavaVM, refs::Global, objects::JObject}; + +#[unsafe(no_mangle)] +fn android_on_create(state: &OnCreateState) { + static APP_ONCE: OnceLock<()> = OnceLock::new(); + APP_ONCE.get_or_init(|| { + // Initialize logging... + // + // Remember, `android_on_create` may be called multiple times but some + // logger crates will panic if initialized multiple times. + }); + let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) }; + let activity = state.activity_as_ptr() as jni::sys::jobject; + // Although the thread is implicitly already attached (we are inside an onCreate native method) + // using `vm.attach_current_thread` here will use the existing attachment, give us an `&Env` + // reference and also catch Java exceptions. + if let Err(err) = vm.attach_current_thread(|env| -> jni::errors::Result<()> { + // SAFETY: + // - The `Activity` reference / pointer is at least valid until we return + // - By creating a `Cast` we ensure we can't accidentally delete the reference + let activity = unsafe { env.as_cast_raw::(&activity)? }; + + // Do something with the activity on the Java main thread... + Ok(()) + }) { + eprintln!("Failed to interact with Android SDK on Java main thread: {err:?}"); + } +} +``` ### Changed diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index d0325d4..f9b8a08 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -21,11 +21,9 @@ use ndk::configuration::Configuration; use ndk::native_window::NativeWindow; use crate::error::InternalResult; +use crate::init::{init_android_main_thread, init_java_main_thread_on_create}; 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, -}; +use crate::util::{abort_on_panic, log_panic, try_get_path_from_ptr}; use crate::{ AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, PollEvent, Rect, WindowManagerFlags, @@ -1149,6 +1147,17 @@ pub unsafe extern "C" fn GameActivity_onCreate( saved_state: *mut ::std::os::raw::c_void, saved_state_size: libc::size_t, ) { + abort_on_panic(|| unsafe { + let vm = jni::JavaVM::from_raw((*activity).vm as *mut _); + let java_activity = (*activity).javaGameActivity; + let saved_state = if !saved_state.is_null() && saved_state_size > 0 { + std::slice::from_raw_parts(saved_state.cast(), saved_state_size) + } else { + &[] + }; + init_java_main_thread_on_create(vm, java_activity as *mut c_void, saved_state); + }); + GameActivity_onCreate_C(activity, saved_state, saved_state_size); } @@ -1162,8 +1171,6 @@ extern "Rust" { #[no_mangle] pub unsafe extern "C" fn _rust_glue_entry(game_activity_glue: *mut ffi::android_app) { abort_on_panic(|| { - let _join_log_forwarder = forward_stdio_to_logcat(); - let (jvm, jni_activity) = unsafe { let jvm = (*(*game_activity_glue).activity).vm; let activity: jobject = (*(*game_activity_glue).activity).javaGameActivity; diff --git a/android-activity/src/init.rs b/android-activity/src/init.rs new file mode 100644 index 0000000..3995ee0 --- /dev/null +++ b/android-activity/src/init.rs @@ -0,0 +1,323 @@ +use jni::{ + jni_sig, jni_str, + objects::{JObject, JString, JThread}, + vm::JavaVM, +}; +use log::Level; +use ndk::asset::AssetManager; +use std::{ + ffi::{c_void, CStr, CString}, + fs::File, + io::{BufRead as _, BufReader}, + os::fd::{FromRawFd as _, RawFd}, + sync::OnceLock, +}; + +use crate::{ + main_callbacks::MainCallbacks, util::android_log, OnCreateState, ANDROID_ACTIVITY_TAG, +}; + +fn forward_stdio_to_logcat() -> std::thread::JoinHandle> { + // XXX: make this stdout/stderr redirection an optional / opt-in feature?... + + let file = unsafe { + let mut logpipe: [RawFd; 2] = Default::default(); + libc::pipe2(logpipe.as_mut_ptr(), libc::O_CLOEXEC); + libc::dup2(logpipe[1], libc::STDOUT_FILENO); + libc::dup2(logpipe[1], libc::STDERR_FILENO); + libc::close(logpipe[1]); + + File::from_raw_fd(logpipe[0]) + }; + + std::thread::Builder::new() + .name("stdio-to-logcat".to_string()) + .spawn(move || -> std::io::Result<()> { + let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap(); + let mut reader = BufReader::new(file); + let mut buffer = String::new(); + loop { + buffer.clear(); + let len = match reader.read_line(&mut buffer) { + Ok(len) => len, + Err(e) => { + android_log( + Level::Error, + ANDROID_ACTIVITY_TAG, + &CString::new(format!( + "Logcat forwarder failed to read stdin/stderr: {e:?}" + )) + .unwrap(), + ); + break Err(e); + } + }; + if len == 0 { + break Ok(()); + } else if let Ok(msg) = CString::new(buffer.clone()) { + android_log(Level::Info, tag, &msg); + } + } + }) + .expect("Failed to start stdout/stderr to logcat forwarder thread") +} + +unsafe extern "C" fn _android_activity_anchor() {} + +/// Get a handle to the shared library that we are linked into, so that we can +/// look up symbols within it. +fn dlopen_self() -> Result<*mut c_void, String> { + unsafe { + let mut info: libc::Dl_info = std::mem::zeroed(); + + // NB: `dladdr` does not update the `dlerror` state + if libc::dladdr( + _android_activity_anchor as *const () as *const c_void, + &mut info, + ) == 0 + { + return Err("dladdr failed".into()); + } + if info.dli_fname.is_null() { + return Err("dladdr returned null dli_fname".into()); + } + + // Clear any existing error + libc::dlerror(); + + let handle = libc::dlopen(info.dli_fname, libc::RTLD_NOW | libc::RTLD_NOLOAD); + if handle.is_null() { + let err = CStr::from_ptr(libc::dlerror()) + .to_string_lossy() + .into_owned(); + let path = CStr::from_ptr(info.dli_fname) + .to_string_lossy() + .into_owned(); + return Err(format!("dlopen({path}) failed: {err}")); + } + + Ok(handle) + } +} + +/// Look up a symbol within our own shared library +/// +/// This can be used to look up optional application entry points, such as +/// `android_on_create` +/// +/// Returns `None` if the symbol is not found (which is not considered an error) +fn lookup_self_symbol(symbol: &CStr) -> Option<*mut c_void> { + unsafe { + let handle = match dlopen_self() { + Ok(h) => h, + Err(err) => { + let msg = format!( + "Warning: failed to dlopen self, looking for symbol {}: {err}", + symbol.to_string_lossy() + ); + android_log( + Level::Warn, + ANDROID_ACTIVITY_TAG, + &CString::new(msg).unwrap(), + ); + return None; + } + }; + + // Clear any existing error + libc::dlerror(); + + let sym = libc::dlsym(handle, symbol.as_ptr()); + + // Close the handle to avoid leaking a reference count + if libc::dlclose(handle) != 0 { + let err = CStr::from_ptr(libc::dlerror()) + .to_string_lossy() + .into_owned(); + let msg = format!("dlclose failed for self handle: {err}"); + android_log( + Level::Warn, + ANDROID_ACTIVITY_TAG, + &CString::new(msg).unwrap(), + ); + } + + if sym.is_null() { + None + } else { + Some(sym) + } + } +} + +/// Attempt to call an optional "android_on_create" entry point within the +/// application's shared library +/// +/// Note: this function does not propagate any errors, while it's assumed that +/// this is called within an `onCreate` native method. +/// +/// # Safety +/// +/// - This must be called from the Java main thread, while onCreate is running +/// - The `jni_activity` pointer must be a valid JNI reference to the Java +/// Activity instance being created +/// +/// The safety here also depends on the application declaring an +/// `android_on_create` function with the correct signature. (It's safe to not +/// declare an `android_on_create` function at all, and the code will simply +/// skip calling it) +pub(crate) unsafe fn init_java_main_thread_on_create( + jvm: JavaVM, + jni_activity: *mut c_void, + saved_state: &[u8], +) { + let _join_log_forwarder = forward_stdio_to_logcat(); + + let msg = CString::new(format!( + "Creating: Activity = {:p}, saved state size = {}", + jni_activity, + saved_state.len() + )) + .unwrap(); + android_log(Level::Info, ANDROID_ACTIVITY_TAG, &msg); + + // SAFETY: It's the application's responsibility to declare any `android_on_create` + // function with the correct signature and ABI. + let android_on_create: extern "Rust" fn(state: &OnCreateState) = unsafe { + let Some(symbol) = lookup_self_symbol(c"android_on_create") else { + // android_on_create is optional, so simply return if not found + return; + }; + std::mem::transmute(symbol) + }; + + let state = OnCreateState::new(jvm.clone(), jni_activity, saved_state); + // Catch any exceptions from the callback and log them instead of allowing any + // exception to propagate back to the Activity. + let res = jvm.attach_current_thread(|_env| -> jni::errors::Result<()> { + android_on_create(&state); + Ok(()) + }); + if let Err(err) = res { + let msg = CString::new(format!( + "JNI error while running android_on_create: {:?}", + err + )) + .unwrap(); + android_log(Level::Error, ANDROID_ACTIVITY_TAG, &msg); + } +} + +struct AppState { + main_callbacks: MainCallbacks, + app_asset_manager: AssetManager, +} + +static APP_ONCE: OnceLock = OnceLock::new(); + +// Get the Application instance from the Activity +fn get_application<'local, 'any>( + env: &mut jni::Env<'local>, + activity: &JObject<'any>, +) -> jni::errors::Result> { + let app = env + .call_method( + activity, + jni_str!("getApplication"), + jni_sig!(() -> android.app.Application), + &[], + )? + .l()?; + Ok(app) +} + +fn get_assets<'local, 'any>( + env: &mut jni::Env<'local>, + application: &JObject<'any>, +) -> jni::errors::Result> { + let assets_manager = env + .call_method( + application, + jni_str!("getAssets"), + jni_sig!(() -> android.content.res.AssetManager), + &[], + )? + .l()?; + Ok(assets_manager) +} + +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, + java_main_looper: &ndk::looper::ForeignLooper, +) -> jni::errors::Result<(AssetManager, MainCallbacks)> { + 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_asset_manager = + get_assets(env, &application).expect("Failed to get AssetManager"); + 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 asset_manager_global = env + .new_global_ref(app_asset_manager) + .expect("Failed to create global ref for AssetManager"); + // Make sure we don't delete the global reference via Drop because + // the AAssetManager pointer will only be valid while we can + // guarantee that the Java AssetManager is not garbage collected + let asset_manager_global = asset_manager_global.into_raw(); + let asset_manager_ptr = + ndk_sys::AAssetManager_fromJava(env.get_raw() as _, asset_manager_global as _); + assert_ne!( + asset_manager_ptr, + std::ptr::null_mut(), + "Failed to get Application AAssetManager" + ); + let app_asset_manager = + AssetManager::from_ptr(std::ptr::NonNull::new(asset_manager_ptr).unwrap()); + + let main_callbacks = MainCallbacks::new(java_main_looper); + + AppState { + main_callbacks, + app_asset_manager, + } + }); + + if let Err(err) = try_init_current_thread(env, jni_activity) { + let msg = + CString::new(format!("Failed to initialize Java thread state: {:?}", err)).unwrap(); + android_log(Level::Error, ANDROID_ACTIVITY_TAG, &msg); + } + + let asset_manager = unsafe { AssetManager::from_ptr(app_state.app_asset_manager.ptr()) }; + let main_callbacks = app_state.main_callbacks.clone(); + + Ok((asset_manager, main_callbacks)) + }) +} diff --git a/android-activity/src/lib.rs b/android-activity/src/lib.rs index 62af497..bd09b98 100644 --- a/android-activity/src/lib.rs +++ b/android-activity/src/lib.rs @@ -1,70 +1,270 @@ //! A glue layer for building standalone, Rust applications on Android //! -//! This crate provides a "glue" layer for building native Rust -//! applications on Android, supporting multiple [`Activity`] base classes. -//! It's comparable to [`android_native_app_glue.c`][ndk_concepts] -//! for C/C++ applications. +//! This crate provides a "glue" layer for building native Rust applications on +//! Android, supporting multiple [`Activity`] base classes. It's comparable to +//! [`android_native_app_glue.c`][ndk_concepts] for C/C++ applications. //! //! Currently the crate supports two `Activity` base classes: -//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling any Java or Kotlin code. +//! 1. [`NativeActivity`] - Built in to Android, this doesn't require compiling +//! any Java or Kotlin code. //! 2. [`GameActivity`] - From the Android Game Development Kit, it has more -//! sophisticated input handling support than `NativeActivity`. `GameActivity` -//! is also based on the `AndroidAppCompat` class which can help with supporting -//! a wider range of devices. +//! sophisticated input handling support than `NativeActivity`. +//! `GameActivity` is also based on the `AndroidAppCompat` class which can +//! help with supporting a wider range of devices. //! -//! Standalone applications based on this crate need to be built as `cdylib` libraries, like: +//! Standalone applications based on this crate need to be built as `cdylib` +//! libraries, like: //! ```toml //! [lib] -//! crate_type=["cdylib"] +//! crate-type=["cdylib"] //! ``` //! -//! and implement a `#[no_mangle]` `android_main` entry point like this: -//! ```no_run -//! #[no_mangle] -//! fn android_main(app: android_activity::AndroidApp) { +//! ## Lifecycle of an Activity //! +//! Keep in mind that Android's application programming model is based around +//! the +//! [lifecycle](https://developer.android.com/guide/components/activities/activity-lifecycle) +//! of [`Activity`] and [`Service`] components, and not the lifecycle of the +//! application process. +//! +//! An Android application may have multiple [`Activity`] and [`Service`] +//! instances created and destroyed over its lifetime, and each of these +//! [`Activity`] and [`Service`] instances will have their own lifecycles that +//! are independent from the lifecycle of the application process. +//! +//! See the Android SDK [activity lifecycle +//! documentation](https://developer.android.com/guide/components/activities/activity-lifecycle) +//! for more details on the [`Activity`] lifecycle. +//! +//! Although native applications will typically only have a single instance of +//! [`NativeActivity`] or [`GameActivity`], it's possible for these activities +//! to be created and destroyed multiple times within the lifetime of your +//! application process. +//! +//! Although [`NativeActivity`] and [`GameActivity`] were historically designed +//! for full-screen games and based on the assumption that there would only be a +//! single instance of these activities, it is good to keep in mind that Android +//! itself makes no such assumption. It's very common for non-native Android +//! applications to be tracking multiple `Activity` instances at the same time. +//! +//! The `android-activity` crate is designed to be robust to multiple `Activity` +//! instances being created and destroyed over the lifetime of the application +//! process. +//! +//! ## Entrypoints +//! +//! There are currently two supported entrypoints for an `android-activity` +//! application: +//! +//! 1. `android_on_create` **(optional)** - This runs early, on the Java main / +//! UI thread, during `Activity.onCreate()`. It can be a good place to +//! initialize logging and JNI bindings. +//! 2. `android_main` **(required)** - This run a dedicated main loop thread for +//! handling lifecycle and input events for your `Activity`. +//! +//! **Important**: Your `android-activity` entrypoints are tied to the lifecycle +//! of your native **`Activity`** (i.e. [`NativeActivity`] or [`GameActivity`]) +//! and not the lifecycle of your application process! This means that if your +//! `Activity` is destroyed and re-created (e.g. depending on how your +//! application handles configuration changes) then these entrypoints may be +//! called multiple times, for each `Activity` instance. +//! +//! #### Your AndroidManifest `configureChanges` state affects Activity re-creation +//! +//! Beware that, by default, certain configuration changes (e.g. device +//! rotation) will cause the Android system to destroy and re-create your +//! `Activity`, which will lead to a [`MainEvent::Destroy`] event being sent to +//! your `android_main()` thread and then `android_main()` will be called again +//! as a new native `Activity` instance is created. +//! +//! Since this can be awkward to handle, it is common practice to set the +//! `android:configChanges` property to indicate that your application can +//! handle these changes at runtime via events instead. +//! +//! **Example**: +//! +//! Here's how you can set `android:configChanges` for your `Activity` in your +//! AndroidManifest.xml: +//! +//! ```xml +//! +//! +//! +//! +//! ``` +//! +//! ### onCreate entrypoint: `android_on_create` (optional) +//! +//! The `android_on_create` entry point will be called from the Java main +//! thread, within the `Activity`'s `onCreate` method, before the `android_main` +//! entry point is called. +//! +//! This must be an exported, unmangled, `"Rust"` ABI function with the +//! signature `fn android_on_create(state: &OnCreateState)`. +//! +//! The easiest way to achieve this is with `#[unsafe(no_mangle)]` like this: +//! ```no_run +//! #[unsafe(no_mangle)] +//! fn android_on_create(state: &android_activity::OnCreateState) { +//! // Initialization code here +//! } +//! ``` +//! (Note `extern "Rust"` is the default ABI) +//! +//! **I/O redirection**: Before `android_on_create()` is called an I/O thread is +//! spawned that will handle redirecting standard input and output to the +//! Android log, visible via `logcat`. +//! +//! [`OnCreateState`] provides access to the Java VM and a JNI reference to the +//! `Activity` instance, as well as any saved state from a previous instance of +//! the Activity. +//! +//! Due to the way JNI class loading works, this can be a convenient place to +//! initialize JNI bindings because it's called while the `Activity`'s +//! `onCreate` callback is on the stack, so the default class loader will be +//! able to find the application's Java classes. See the Android +//! [JNI tips](https://developer.android.com/ndk/guides/jni-tips#faq:-why-didnt-findclass-find-my-class) +//! guide for more details on this. +//! +//! This can also be a good place to initialize logging, since it's called +//! first. +//! +//! **Important**: This entrypoint must not block for a long time or do heavy +//! work, since it's running on the Java main thread and will block the +//! `Activity` from being created until it returns. +//! +//! Blocking the Java main thread for too long may cause an "Application Not +//! Responding" (ANR) dialog to be shown to the user, and cause users to force +//! close your application. +//! +//! **Panic behavior**: If `android_on_create` panics, the application will +//! abort. This is because the callback runs within a native JNI callback where +//! unwinding is not permitted. Ensure your initialization code either cannot +//! panic or uses `catch_unwind` internally if you want to allow partial +//! initialization failures. +//! +//! #### Example: +//! +//! ```no_run +//! # use std::sync::OnceLock; +//! # use android_activity::OnCreateState; +//! # use jni::{JavaVM, objects::JObject}; +//! #[unsafe(no_mangle)] +//! fn android_on_create(state: &OnCreateState) { +//! static APP_ONCE: OnceLock<()> = OnceLock::new(); +//! APP_ONCE.get_or_init(|| { +//! // Initialize logging... +//! // +//! // Remember, `android_on_create` may be called multiple times but, depending on +//! // the crate, logger initialization may panic if attempted multiple times. +//! }); +//! let vm = unsafe { JavaVM::from_raw(state.vm_as_ptr().cast()) }; +//! let activity = state.activity_as_ptr() as jni::sys::jobject; +//! // Although the thread is implicitly already attached (we are inside an onCreate native method) +//! // using `vm.attach_current_thread` here will use the existing attachment, give us an `&Env` +//! // reference and also catch Java exceptions. +//! if let Err(err) = vm.attach_current_thread(|env| -> jni::errors::Result<()> { +//! // SAFETY: +//! // - The `Activity` reference / pointer is at least valid until we return +//! // - By creating a `Cast` we ensure we can't accidentally delete the reference +//! let activity = unsafe { env.as_cast_raw::(&activity)? }; +//! +//! // Do something with the activity on the Java main thread... +//! Ok(()) +//! }) { +//! eprintln!("Failed to interact with Android SDK on Java main thread: {err:?}"); +//! } //! } //! ``` //! -//! Once your application's `Activity` class has loaded and it calls `onCreate` then -//! `android-activity` will spawn a dedicated thread to run your `android_main` function, -//! separate from the Java thread that created the corresponding `Activity`. +//! ### Main loop thread entrypoint: `android_main` (required) +//! +//! Your application must always define an `android_main` function as an entry +//! point for running a main loop thread for your Activity. +//! +//! This must be an exported, unmangled, `"Rust"` ABI function with the +//! signature `fn android_main(app: AndroidApp)`. +//! +//! The easiest way to achieve this is with `#[unsafe(no_mangle)]` like this: +//! ```no_run +//! #[unsafe(no_mangle)] +//! fn android_main(app: android_activity::AndroidApp) { +//! // Main loop code here +//! } +//! ``` +//! (Note `extern "Rust"` is the default ABI) +//! +//! Once your application's `Activity` class has loaded and it calls `onCreate` +//! then `android-activity` will spawn a dedicated thread to run your +//! `android_main` function, separate from the Java thread that created the +//! corresponding `Activity`. +//! +//! Before `android_main()` is called: +//! - A `JavaVM` and +//! [`android.content.Context`](https://developer.android.com/reference/android/content/Context) +//! instance will be associated with the [`ndk_context`] crate so that other, +//! independent, Rust crates are able to find a JavaVM for making JNI calls. +//! - The `JavaVM` will be attached to the native thread (for JNI) +//! - A [Looper] is attached to the Rust native thread. +//! +//! **Important:** This thread *must* call [`AndroidApp::poll_events()`] +//! regularly in order to receive lifecycle and input events for the `Activity`. +//! Some `Activity` lifecycle callbacks on the Java main thread will block until +//! the next time `poll_events()` is called, so if you don't call +//! `poll_events()` regularly you may trigger an ANR dialog and cause users to +//! force close your application. +//! +//! **Important**: You should return from `android_main()` as soon as possible +//! if you receive a [`MainEvent::Destroy`] event from `poll_events()`. Most +//! [`AndroidApp`] methods will become a no-op after [`MainEvent::Destroy`] is +//! received, since it no longer has an associated `Activity`. +//! +//! **Important**: Do *not* call `std::process::exit()` from your +//! `android_main()` function since that will subvert the normal lifecycle of +//! the `Activity` and other components. Keep in mind that code running in +//! `android_main()` does not logically own the entire process since there may +//! be other Android components (e.g. Services) running within the process. +//! +//! ## AndroidApp: State and Event Loop //! //! [`AndroidApp`] provides an interface to query state for the application as -//! well as monitor events, such as lifecycle and input events, that are -//! marshalled between the Java thread that owns the `Activity` and the native -//! thread that runs the `android_main()` code. +//! well as monitor events, such as lifecycle and input events for the +//! associated native `Activity` instance. //! -//! # Cheaply Clonable [`AndroidApp`] +//! ### Cheaply Cloneable [`AndroidApp`] //! //! [`AndroidApp`] is intended to be something that can be cheaply passed around -//! by referenced within an application. It is reference counted and can be -//! cheaply cloned. +//! within an application. It is reference-counted and can be cheaply cloned. //! -//! # `Send` and `Sync` [`AndroidApp`] +//! ### `Send` and `Sync` [`AndroidApp`] (**but...**) //! //! Although an [`AndroidApp`] implements `Send` and `Sync` you do need to take //! into consideration that some APIs, such as [`AndroidApp::poll_events()`] are //! explicitly documented to only be usable from your `android_main()` thread. //! -//! # Main Thread Initialization +//! ### No associated Activity after [`MainEvent::Destroy`] //! -//! Before `android_main()` is called, the following application state -//! is also initialized: +//! After you receive a [`MainEvent::Destroy`] event from `poll_events()` then +//! the [`AndroidApp`] will no longer have an associated `Activity` and most of +//! its methods will become no-ops. You should return from `android_main()` as +//! soon as possible after receiving a `Destroy` event since your native +//! `Activity` no longer exists. //! -//! 1. An I/O thread is spawned that will handle redirecting standard input -//! and output to the Android log, visible via `logcat`. -//! 2. A `JavaVM` and `Activity` instance will be associated with the [`ndk_context`] crate -//! so that other, independent, Rust crates are able to find a JavaVM -//! for making JNI calls. -//! 3. The `JavaVM` will be attached to the native thread -//! 4. A [Looper] is attached to the Rust native thread. +//! If a new [`Activity`] instance is created after that then a new +//! [`AndroidApp`] will be created for that new [`Activity`] instance and sent +//! to a new call to `android_main()`. //! -//! -//! These are undone after `android_main()` returns +//! **Important**: It's not recommended to store an [`AndroidApp`] as global +//! static state and it should instead be passed around by reference within your +//! application so it can be reliably dropped when the `Activity` is destroyed +//! and you return from `android_main()`. //! //! # Android Extensible Enums -// TODO: Move this to the NDK crate, which now implements this for most of the code? //! //! There are numerous enums in the `android-activity` API which are effectively //! bindings to enums declared in the Android SDK which need to be considered @@ -75,7 +275,7 @@ //! build an application that might be installed on new versions of Android. //! //! This crate follows a convention of adding a hidden `__Unknown(u32)` variant -//! to these enum to ensure we can always do lossless conversions between the +//! to these enums to ensure we can always do lossless conversions between the //! integers from the SDK and our corresponding Rust enums. This can be //! important in case you need to pass certain variants back to the SDK //! regardless of whether you knew about that variants specific semantics at @@ -108,13 +308,18 @@ //! ``` //! //! [`Activity`]: https://developer.android.com/reference/android/app/Activity -//! [`NativeActivity`]: https://developer.android.com/reference/android/app/NativeActivity +//! [`NativeActivity`]: +//! https://developer.android.com/reference/android/app/NativeActivity //! [ndk_concepts]: https://developer.android.com/ndk/guides/concepts#naa -//! [`GameActivity`]: https://developer.android.com/games/agdk/integrate-game-activity +//! [`GameActivity`]: +//! https://developer.android.com/games/agdk/integrate-game-activity +//! [`Service`]: https://developer.android.com/reference/android/app/Service //! [Looper]: https://developer.android.com/reference/android/os/Looper +//! [`Context`]: https://developer.android.com/reference/android/content/Context #![deny(clippy::manual_let_else)] +use std::ffi::CStr; use std::hash::Hash; use std::sync::Arc; use std::sync::RwLock; @@ -178,6 +383,8 @@ pub(crate) mod activity_impl; pub mod error; use error::Result; +mod init; + pub mod input; use input::KeyCharacterMap; @@ -193,6 +400,8 @@ pub use waker::AndroidAppWaker; mod main_callbacks; +pub(crate) const ANDROID_ACTIVITY_TAG: &CStr = c"android-activity"; + /// A rectangle with integer edge coordinates. Used to represent window insets, for example. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Rect { @@ -1078,3 +1287,81 @@ fn test_app_is_send_sync() { fn needs_send_sync() {} needs_send_sync::(); } + +/// The state passed to the optional `android_on_create` entry point if +/// available. +/// +/// This gives access to the Java VM, the Java `Activity` and any saved state +/// from a previous instance of the `Activity` that was saved via the +/// `onSaveInstanceState` callback. +/// +/// Each time `android_on_create` is called it will receive a new `Activity` +/// reference. +/// +/// See the top-level [`android-activity`](crate) documentation for more details +/// on `android_on_create`. +pub struct OnCreateState<'a> { + jvm: JavaVM, + java_activity: *mut c_void, + saved_state: &'a [u8], +} + +impl<'a> OnCreateState<'a> { + pub(crate) fn new(jvm: JavaVM, java_activity: *mut c_void, saved_state: &'a [u8]) -> Self { + Self { + jvm, + java_activity, + saved_state, + } + } + + /// Returns a pointer to the Java Virtual Machine, for making JNI calls + /// + /// If you use the `jni` crate, you can wrap this pointer as a `JavaVM` via: + /// ```no_run + /// # use jni::JavaVM; + /// # let on_create_state: android_activity::OnCreateState = todo!(); + /// let vm = unsafe { JavaVM::from_raw(on_create_state.vm_as_ptr().cast()) }; + /// ``` + pub fn vm_as_ptr(&self) -> *mut c_void { + self.jvm.get_raw().cast() + } + + /// Returns an (*unowned*) JNI global object reference for this `Activity` + /// as a pointer + /// + /// If you use the `jni` crate, you can cast this as a `JObject` reference + /// via: + /// + /// ```no_run + /// # use jni::{JavaVM, objects::JObject}; + /// # let on_create_state: android_activity::OnCreateState = todo!(); + /// let vm = unsafe { JavaVM::from_raw(on_create_state.vm_as_ptr().cast()) }; + /// let _res = vm.attach_current_thread(|env| -> jni::errors::Result<()> { + /// let activity = on_create_state.activity_as_ptr() as jni::sys::jobject; + /// // SAFETY: The reference / pointer is valid at least until we return from `android_on_create` + /// let activity = unsafe { env.as_cast_raw::(&activity)? }; + /// // Do something with `activity` here + /// Ok(()) + /// }); + /// ``` + /// + /// # JNI Safety + /// + /// It is not specified whether this will be a global or local reference and + /// in any case you must treat is as a reference that you do not own and + /// must not attempt to delete it. + /// - Don't wrap the reference as a `Global` which would try to delete the + /// reference when dropped. + /// - Don't wrap the reference in an `Auto` which would treat the reference + /// like a local reference and try to delete it when dropped. + pub fn activity_as_ptr(&self) -> *mut c_void { + self.java_activity as *mut c_void + } + + /// Returns the saved state of the `Activity` as a byte slice, which may be + /// empty if there is no saved state. + pub fn saved_state(&self) -> &[u8] { + self.saved_state + } +} diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs index ae261e0..0c63e49 100644 --- a/android-activity/src/native_activity/glue.rs +++ b/android-activity/src/native_activity/glue.rs @@ -13,7 +13,8 @@ use jni::{objects::JObject, refs::Global, vm::AttachConfig}; use ndk::{configuration::Configuration, input_queue::InputQueue, native_window::NativeWindow}; use crate::{ - util::{abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic}, + init::{init_android_main_thread, init_java_main_thread_on_create}, + util::{abort_on_panic, log_panic}, ConfigurationRef, }; @@ -878,16 +879,23 @@ extern "C" fn ANativeActivity_onCreate( saved_state_size: libc::size_t, ) { abort_on_panic(|| { - let _join_log_forwarder = forward_stdio_to_logcat(); - - eprintln!( - "Creating: {:p}, saved_state = {:p}, save_state_size = {}", - activity, saved_state, saved_state_size - ); - let main_looper = ndk::looper::ForeignLooper::for_thread().expect("Failed to get Java main looper"); + let (jvm, jni_activity) = unsafe { + let jvm: *mut jni::sys::JavaVM = (*activity).vm.cast(); + let jni_activity: jni::sys::jobject = (*activity).clazz as _; // Completely bogus name; this is the _instance_ not class pointer + (jni::JavaVM::from_raw(jvm), jni_activity) + }; + unsafe { + let saved_state = if !saved_state.is_null() && saved_state_size > 0 { + std::slice::from_raw_parts(saved_state.cast(), saved_state_size) + } else { + &[] + }; + init_java_main_thread_on_create(jvm, jni_activity as _, saved_state); + }; + // 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); diff --git a/android-activity/src/util.rs b/android-activity/src/util.rs index ba52883..6e04758 100644 --- a/android-activity/src/util.rs +++ b/android-activity/src/util.rs @@ -1,23 +1,9 @@ -use jni::{ - jni_sig, jni_str, - objects::{JObject, JString, JThread}, - vm::JavaVM, -}; -use log::{error, Level}; -use ndk::asset::AssetManager; +use log::Level; use std::{ ffi::{CStr, CString}, - fs::File, - io::{BufRead as _, BufReader, Result}, - os::{ - fd::{FromRawFd as _, RawFd}, - raw::c_char, - }, - sync::OnceLock, + os::raw::c_char, }; -use crate::main_callbacks::MainCallbacks; - pub fn try_get_path_from_ptr(path: *const c_char) -> Option { if path.is_null() { return None; @@ -45,44 +31,6 @@ pub(crate) fn android_log(level: Level, tag: &CStr, msg: &CStr) { } } -pub(crate) fn forward_stdio_to_logcat() -> std::thread::JoinHandle> { - // XXX: make this stdout/stderr redirection an optional / opt-in feature?... - - let file = unsafe { - let mut logpipe: [RawFd; 2] = Default::default(); - libc::pipe2(logpipe.as_mut_ptr(), libc::O_CLOEXEC); - libc::dup2(logpipe[1], libc::STDOUT_FILENO); - libc::dup2(logpipe[1], libc::STDERR_FILENO); - libc::close(logpipe[1]); - - File::from_raw_fd(logpipe[0]) - }; - - std::thread::Builder::new() - .name("stdio-to-logcat".to_string()) - .spawn(move || -> Result<()> { - let tag = CStr::from_bytes_with_nul(b"RustStdoutStderr\0").unwrap(); - let mut reader = BufReader::new(file); - let mut buffer = String::new(); - loop { - buffer.clear(); - let len = match reader.read_line(&mut buffer) { - Ok(len) => len, - Err(e) => { - error!("Logcat forwarder failed to read stdin/stderr: {e:?}"); - break Err(e); - } - }; - if len == 0 { - break Ok(()); - } else if let Ok(msg) = CString::new(buffer.clone()) { - android_log(Level::Info, tag, &msg); - } - } - }) - .expect("Failed to start stdout/stderr to logcat forwarder thread") -} - pub(crate) fn log_panic(panic: Box) { let rust_panic = unsafe { CStr::from_bytes_with_nul_unchecked(b"RustPanic\0") }; @@ -120,115 +68,3 @@ pub(crate) fn abort_on_panic(f: impl FnOnce() -> R) -> R { std::process::abort(); }) } - -struct AppState { - main_callbacks: MainCallbacks, - app_asset_manager: AssetManager, -} - -static APP_ONCE: OnceLock = OnceLock::new(); - -// Get the Application instance from the Activity -pub(crate) fn get_application<'local, 'any>( - env: &mut jni::Env<'local>, - activity: &JObject<'any>, -) -> jni::errors::Result> { - let app = env - .call_method( - activity, - jni_str!("getApplication"), - jni_sig!(() -> android.app.Application), - &[], - )? - .l()?; - Ok(app) -} - -pub(crate) fn get_assets<'local, 'any>( - env: &mut jni::Env<'local>, - application: &JObject<'any>, -) -> jni::errors::Result> { - let assets_manager = env - .call_method( - application, - jni_str!("getAssets"), - jni_sig!(() -> android.content.res.AssetManager), - &[], - )? - .l()?; - Ok(assets_manager) -} - -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, - java_main_looper: &ndk::looper::ForeignLooper, -) -> jni::errors::Result<(AssetManager, MainCallbacks)> { - 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_asset_manager = - get_assets(env, &application).expect("Failed to get AssetManager"); - 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 asset_manager_global = env - .new_global_ref(app_asset_manager) - .expect("Failed to create global ref for AssetManager"); - // Make sure we don't delete the global reference via Drop because - // the AAssetManager pointer will only be valid while we can - // guarantee that the Java AssetManager is not garbage collected - let asset_manager_global = asset_manager_global.into_raw(); - let asset_manager_ptr = - ndk_sys::AAssetManager_fromJava(env.get_raw() as _, asset_manager_global as _); - assert_ne!( - asset_manager_ptr, - std::ptr::null_mut(), - "Failed to get Application AAssetManager" - ); - let app_asset_manager = - AssetManager::from_ptr(std::ptr::NonNull::new(asset_manager_ptr).unwrap()); - - let main_callbacks = MainCallbacks::new(java_main_looper); - - AppState { - main_callbacks, - app_asset_manager, - } - }); - - if let Err(err) = try_init_current_thread(env, jni_activity) { - eprintln!("Failed to initialize Java thread state: {:?}", err); - } - - let asset_manager = unsafe { AssetManager::from_ptr(app_state.app_asset_manager.ptr()) }; - let main_callbacks = app_state.main_callbacks.clone(); - - Ok((asset_manager, main_callbacks)) - }) -}