nandi/jolt-nativepublic Fork 0
fa4ecdfed6a830e6099d5fab9be24ef72ea1923b
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Catch up with vidya c90f8af · on fa4ecdfed6a830e6099d5fab9be24ef72ea1923b · nandi · 19d ago
android.rs · 154 lines · 5.9 KBRust Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! 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<Option<AndroidApp>> = 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<AndroidApp> {
    ANDROID_APP.lock().ok()?.clone()
}

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}"),
    );
}