//! The Android entry point, and the `AndroidApp` the frame loop needs. //! //! On a desktop the caller owns `main` and this library is just a dependency. //! Android inverts that: the activity is created by the platform, and whoever //! owns the activity owns the event loop. winit cannot build one on Android //! without the `AndroidApp` handle that `android-activity`'s glue receives, and //! that glue only runs if *this* library is the NativeActivity's library. //! //! So on Android the roles swap. `libvidya.so` is the entry point named by the //! manifest, [`android_main`] stashes the handle [`build_event_loop`] later //! needs, and then hands control to the application — an embedded Jolt boot //! image in `libjoltapp.so`, which calls straight back into the C ABI below. //! //! The application is reached by `dlopen` rather than by linking. Linking would //! be a cycle — `libjoltapp.so` already needs this library for every `vidya_*` //! symbol it registers with Chez — and Android's loader resolves a dlopened //! library's dependencies without one. use std::ffi::{c_char, c_int, c_void, CString}; use std::sync::Mutex; use winit::platform::android::activity::AndroidApp; /// The library holding the application's `vidya_jolt_main`. const APP_LIBRARY: &str = "libjoltapp.so"; /// Set once, before any application code runs, and read on the loop thread. static ANDROID_APP: Mutex> = Mutex::new(None); /// Point `HOME` (and the XDG directories under it) at the app's own storage. /// /// An Android process starts with none of them set, and an application written /// against a Unix — which is every application this library loads — reads a /// missing `HOME` as the empty string and writes to `/.cache`, `/.config`, or /// worse. Nothing there is writable, so caches, saved sessions and settings all /// fail quietly on the phone and nowhere else. /// /// The app's internal data directory is what a desktop's home directory is /// here: private to the app, writable without a permission, and removed with /// it. Set only when unset, so an application that has its own idea keeps it. fn set_home_from(app: &AndroidApp) { let Some(home) = app.internal_data_path() else { log(ANDROID_LOG_ERROR, "vidya: no internal data path for HOME"); return; }; let mut set = |key: &str, value: &std::path::Path| { if std::env::var_os(key).is_none() { // SAFETY: this runs before the application starts, on the only // thread there is; nothing else can be reading the environment. unsafe { std::env::set_var(key, value) }; } }; set("HOME", &home); set("XDG_CACHE_HOME", &home.join(".cache")); set("XDG_CONFIG_HOME", &home.join(".config")); set("XDG_DATA_HOME", &home.join(".local/share")); } /// The handle winit needs to build an event loop. `None` off Android's own /// thread, or before the glue has started. pub fn android_app() -> Option { ANDROID_APP.lock().ok()?.clone() } /// The process's `JavaVM`, for a library that is not this one. /// /// `libjoltmoq.so` reaches Camera2 through JNI and cannot get here by itself: /// the handle arrives in [`android_main`], which only runs in the object /// holding android-activity's glue, and the usual way across — `ndk_context` — /// keeps its handles in a `static`, which is per shared object. So the glue in /// `android/jolt_main.c` reads them out through here and hands them on. /// /// Null before the glue has started. #[no_mangle] pub extern "C" fn vidya_android_vm() -> *mut c_void { match android_app() { Some(app) => app.vm_as_ptr(), None => std::ptr::null_mut(), } } /// The running Activity, as the glue's own global reference — see /// [`vidya_android_vm`]. It is good for the lifetime of the process, which is /// what makes it safe to hand to another object; a local reference would die /// with the frame that made it. /// /// Null before the glue has started. #[no_mangle] pub extern "C" fn vidya_android_activity() -> *mut c_void { match android_app() { Some(app) => app.activity_as_ptr(), None => std::ptr::null_mut(), } } extern "C" { fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; fn dlerror() -> *const c_char; fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; } const ANDROID_LOG_INFO: c_int = 4; const ANDROID_LOG_ERROR: c_int = 6; const RTLD_NOW: c_int = 2; /// What went wrong on the way to a platform call, for `adb logcat -s Vidya`. pub(crate) fn warn(message: &str) { log(ANDROID_LOG_ERROR, message); } fn log(priority: c_int, message: &str) { let (Ok(tag), Ok(text)) = (CString::new("Vidya"), CString::new(message)) else { return; }; // SAFETY: both pointers are NUL-terminated and live across the call. unsafe { __android_log_write(priority, tag.as_ptr(), text.as_ptr()) }; } fn last_dl_error() -> String { // SAFETY: dlerror returns a borrowed C string, or null when nothing failed. let err = unsafe { dlerror() }; if err.is_null() { return "unknown error".to_owned(); } unsafe { std::ffi::CStr::from_ptr(err) } .to_string_lossy() .into_owned() } /// Entry point for `android-activity`'s NativeActivity glue. /// /// Returning from here finishes the activity, which looks like a one-frame /// flash, so a failure to reach the application is logged rather than silent. #[no_mangle] pub extern "C" fn android_main(app: AndroidApp) { set_home_from(&app); if let Ok(mut slot) = ANDROID_APP.lock() { *slot = Some(app); } log(ANDROID_LOG_INFO, "vidya: loading the Jolt application"); let Ok(name) = CString::new(APP_LIBRARY) else { return; }; // SAFETY: `name` is a NUL-terminated library name; the handle is only used // to look up one symbol and is deliberately never closed — the application // runs for the lifetime of the process. let handle = unsafe { dlopen(name.as_ptr(), RTLD_NOW) }; if handle.is_null() { log( ANDROID_LOG_ERROR, &format!("vidya: cannot load {APP_LIBRARY}: {}", last_dl_error()), ); return; } let Ok(symbol) = CString::new("vidya_jolt_main") else { return; }; // SAFETY: as above; the result is checked before it is called. let entry = unsafe { dlsym(handle, symbol.as_ptr()) }; if entry.is_null() { log( ANDROID_LOG_ERROR, &format!( "vidya: {APP_LIBRARY} has no vidya_jolt_main: {}", last_dl_error() ), ); return; } // SAFETY: `vidya_jolt_main` is declared by the glue in android/jolt_main.c // as `int vidya_jolt_main(void)`; it boots Chez and does not return until // the application exits. let entry: extern "C" fn() -> c_int = unsafe { std::mem::transmute(entry) }; let status = entry(); log( ANDROID_LOG_INFO, &format!("vidya: Jolt application exited with status {status}"), ); }