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