| Bring vidya in cfd3e36 nandi 19d ago | 1 | //! The Android entry point, and the `AndroidApp` the frame loop needs. |
| 2 | //! |
| 3 | //! On a desktop the caller owns `main` and this library is just a dependency. |
| 4 | //! Android inverts that: the activity is created by the platform, and whoever |
| 5 | //! owns the activity owns the event loop. winit cannot build one on Android |
| 6 | //! without the `AndroidApp` handle that `android-activity`'s glue receives, and |
| 7 | //! that glue only runs if *this* library is the NativeActivity's library. |
| 8 | //! |
| 9 | //! So on Android the roles swap. `libvidya.so` is the entry point named by the |
| 10 | //! manifest, [`android_main`] stashes the handle [`build_event_loop`] later |
| 11 | //! needs, and then hands control to the application — an embedded Jolt boot |
| 12 | //! image in `libjoltapp.so`, which calls straight back into the C ABI below. |
| 13 | //! |
| 14 | //! The application is reached by `dlopen` rather than by linking. Linking would |
| 15 | //! be a cycle — `libjoltapp.so` already needs this library for every `vidya_*` |
| 16 | //! symbol it registers with Chez — and Android's loader resolves a dlopened |
| 17 | //! library's dependencies without one. |
| 18 | |
| 19 | use std::ffi::{c_char, c_int, c_void, CString}; |
| 20 | use std::sync::Mutex; |
| 21 | |
| 22 | use winit::platform::android::activity::AndroidApp; |
| 23 | |
| 24 | /// The library holding the application's `vidya_jolt_main`. |
| 25 | const APP_LIBRARY: &str = "libjoltapp.so"; |
| 26 | |
| 27 | /// Set once, before any application code runs, and read on the loop thread. |
| 28 | static ANDROID_APP: Mutex<Option<AndroidApp>> = Mutex::new(None); |
| 29 | |
| Catch up with vidya c90f8af nandi 19d ago | 30 | /// Point `HOME` (and the XDG directories under it) at the app's own storage. |
| 31 | /// |
| 32 | /// An Android process starts with none of them set, and an application written |
| 33 | /// against a Unix — which is every application this library loads — reads a |
| 34 | /// missing `HOME` as the empty string and writes to `/.cache`, `/.config`, or |
| 35 | /// worse. Nothing there is writable, so caches, saved sessions and settings all |
| 36 | /// fail quietly on the phone and nowhere else. |
| 37 | /// |
| 38 | /// The app's internal data directory is what a desktop's home directory is |
| 39 | /// here: private to the app, writable without a permission, and removed with |
| 40 | /// it. Set only when unset, so an application that has its own idea keeps it. |
| 41 | fn set_home_from(app: &AndroidApp) { |
| 42 | let Some(home) = app.internal_data_path() else { |
| 43 | log(ANDROID_LOG_ERROR, "vidya: no internal data path for HOME"); |
| 44 | return; |
| 45 | }; |
| 46 | let mut set = |key: &str, value: &std::path::Path| { |
| 47 | if std::env::var_os(key).is_none() { |
| 48 | // SAFETY: this runs before the application starts, on the only |
| 49 | // thread there is; nothing else can be reading the environment. |
| 50 | unsafe { std::env::set_var(key, value) }; |
| 51 | } |
| 52 | }; |
| 53 | set("HOME", &home); |
| 54 | set("XDG_CACHE_HOME", &home.join(".cache")); |
| 55 | set("XDG_CONFIG_HOME", &home.join(".config")); |
| 56 | set("XDG_DATA_HOME", &home.join(".local/share")); |
| 57 | } |
| 58 | |
| Bring vidya in cfd3e36 nandi 19d ago | 59 | /// The handle winit needs to build an event loop. `None` off Android's own |
| 60 | /// thread, or before the glue has started. |
| 61 | pub fn android_app() -> Option<AndroidApp> { |
| 62 | ANDROID_APP.lock().ok()?.clone() |
| 63 | } |
| 64 | |
| Let the media plane cross to the phone, camera and all fd0e21a nandi 18d ago | 65 | /// The process's `JavaVM`, for a library that is not this one. |
| 66 | /// |
| 67 | /// `libjoltmoq.so` reaches Camera2 through JNI and cannot get here by itself: |
| 68 | /// the handle arrives in [`android_main`], which only runs in the object |
| 69 | /// holding android-activity's glue, and the usual way across — `ndk_context` — |
| 70 | /// keeps its handles in a `static`, which is per shared object. So the glue in |
| 71 | /// `android/jolt_main.c` reads them out through here and hands them on. |
| 72 | /// |
| 73 | /// Null before the glue has started. |
| 74 | #[no_mangle] |
| 75 | pub extern "C" fn vidya_android_vm() -> *mut c_void { |
| 76 | match android_app() { |
| 77 | Some(app) => app.vm_as_ptr(), |
| 78 | None => std::ptr::null_mut(), |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /// The running Activity, as the glue's own global reference — see |
| 83 | /// [`vidya_android_vm`]. It is good for the lifetime of the process, which is |
| 84 | /// what makes it safe to hand to another object; a local reference would die |
| 85 | /// with the frame that made it. |
| 86 | /// |
| 87 | /// Null before the glue has started. |
| 88 | #[no_mangle] |
| 89 | pub extern "C" fn vidya_android_activity() -> *mut c_void { |
| 90 | match android_app() { |
| 91 | Some(app) => app.activity_as_ptr(), |
| 92 | None => std::ptr::null_mut(), |
| 93 | } |
| 94 | } |
| 95 | |
| Bring vidya in cfd3e36 nandi 19d ago | 96 | extern "C" { |
| 97 | fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void; |
| 98 | fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; |
| 99 | fn dlerror() -> *const c_char; |
| 100 | fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; |
| 101 | } |
| 102 | |
| 103 | const ANDROID_LOG_INFO: c_int = 4; |
| 104 | const ANDROID_LOG_ERROR: c_int = 6; |
| 105 | const RTLD_NOW: c_int = 2; |
| 106 | |
| 107 | /// What went wrong on the way to a platform call, for `adb logcat -s Vidya`. |
| 108 | pub(crate) fn warn(message: &str) { |
| 109 | log(ANDROID_LOG_ERROR, message); |
| 110 | } |
| 111 | |
| 112 | fn log(priority: c_int, message: &str) { |
| 113 | let (Ok(tag), Ok(text)) = (CString::new("Vidya"), CString::new(message)) else { |
| 114 | return; |
| 115 | }; |
| 116 | // SAFETY: both pointers are NUL-terminated and live across the call. |
| 117 | unsafe { __android_log_write(priority, tag.as_ptr(), text.as_ptr()) }; |
| 118 | } |
| 119 | |
| 120 | fn last_dl_error() -> String { |
| 121 | // SAFETY: dlerror returns a borrowed C string, or null when nothing failed. |
| 122 | let err = unsafe { dlerror() }; |
| 123 | if err.is_null() { |
| 124 | return "unknown error".to_owned(); |
| 125 | } |
| 126 | unsafe { std::ffi::CStr::from_ptr(err) } |
| 127 | .to_string_lossy() |
| 128 | .into_owned() |
| 129 | } |
| 130 | |
| 131 | /// Entry point for `android-activity`'s NativeActivity glue. |
| 132 | /// |
| 133 | /// Returning from here finishes the activity, which looks like a one-frame |
| 134 | /// flash, so a failure to reach the application is logged rather than silent. |
| 135 | #[no_mangle] |
| 136 | pub extern "C" fn android_main(app: AndroidApp) { |
| Catch up with vidya c90f8af nandi 19d ago | 137 | set_home_from(&app); |
| 138 | |
| Bring vidya in cfd3e36 nandi 19d ago | 139 | if let Ok(mut slot) = ANDROID_APP.lock() { |
| 140 | *slot = Some(app); |
| 141 | } |
| 142 | |
| 143 | log(ANDROID_LOG_INFO, "vidya: loading the Jolt application"); |
| 144 | |
| 145 | let Ok(name) = CString::new(APP_LIBRARY) else { |
| 146 | return; |
| 147 | }; |
| 148 | // SAFETY: `name` is a NUL-terminated library name; the handle is only used |
| 149 | // to look up one symbol and is deliberately never closed — the application |
| 150 | // runs for the lifetime of the process. |
| 151 | let handle = unsafe { dlopen(name.as_ptr(), RTLD_NOW) }; |
| 152 | if handle.is_null() { |
| 153 | log( |
| 154 | ANDROID_LOG_ERROR, |
| 155 | &format!("vidya: cannot load {APP_LIBRARY}: {}", last_dl_error()), |
| 156 | ); |
| 157 | return; |
| 158 | } |
| 159 | |
| 160 | let Ok(symbol) = CString::new("vidya_jolt_main") else { |
| 161 | return; |
| 162 | }; |
| 163 | // SAFETY: as above; the result is checked before it is called. |
| 164 | let entry = unsafe { dlsym(handle, symbol.as_ptr()) }; |
| 165 | if entry.is_null() { |
| 166 | log( |
| 167 | ANDROID_LOG_ERROR, |
| 168 | &format!( |
| 169 | "vidya: {APP_LIBRARY} has no vidya_jolt_main: {}", |
| 170 | last_dl_error() |
| 171 | ), |
| 172 | ); |
| 173 | return; |
| 174 | } |
| 175 | |
| 176 | // SAFETY: `vidya_jolt_main` is declared by the glue in android/jolt_main.c |
| 177 | // as `int vidya_jolt_main(void)`; it boots Chez and does not return until |
| 178 | // the application exits. |
| 179 | let entry: extern "C" fn() -> c_int = unsafe { std::mem::transmute(entry) }; |
| 180 | let status = entry(); |
| 181 | log( |
| 182 | ANDROID_LOG_INFO, |
| 183 | &format!("vidya: Jolt application exited with status {status}"), |
| 184 | ); |
| 185 | } |