nandi/jolt-nativepublic Fork 0
8d68cc1481d3dc46b723b97313bbb7cd714d3325
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.

Let the media plane cross to the phone, camera and all fd0e21a · on 8d68cc1481d3dc46b723b97313bbb7cd714d3325 · nandi · 18d ago
android_jni.rs · 129 lines · 5.3 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
//! The JavaVM and the Activity, handed across from `libvidya.so`.
//!
//! On a desktop this library needs nothing from the host: it opens V4L2 nodes
//! and PipeWire streams by itself. On the phone the camera is Java, and every
//! call into it needs the process's `JavaVM` and the running `Activity`.
//!
//! Neither is reachable from here. `android-activity`'s glue receives them, and
//! the glue runs in `libvidya.so` — a different shared object, because whoever
//! holds the glue owns the event loop and that is the UI's job, not this one's.
//! The usual way across, `ndk_context`, does not answer: its handles live in a
//! `static` inside the `ndk-context` crate, and a `static` is per-object. Two
//! cdylibs that both link it get two of them, and the one this library would
//! read is the one nobody ever set.
//!
//! So the handles are pushed rather than pulled. `libjoltapp.so` links both
//! objects, reads them out of libvidya's C ABI, and passes them to
//! [`crate::joltmoq_android_init`] before any call starts. Everything here is
//! `None` until that has happened, and a camera call fails with a reason rather
//! than dereferencing a null.

use std::ptr;
use std::sync::atomic::{AtomicPtr, Ordering};

use jni::objects::{JClass, JObject, JValue};
use jni::{jni_sig, jni_str, JavaVM};

/// Set once, before the application starts. Read from every thread that talks
/// to Camera2 — the tokio workers among them, which is why this is not a
/// `Mutex<Option<_>>` anyone could hold across a JNI call.
static VM: AtomicPtr<jni::sys::JavaVM> = AtomicPtr::new(ptr::null_mut());

/// The Activity, as the glue's own global reference. Not a local ref that would
/// die with the frame that made it: libvidya holds this one for the lifetime of
/// the process, so it is as good here as it is there.
static ACTIVITY: AtomicPtr<jni::sys::_jobject> = AtomicPtr::new(ptr::null_mut());

/// Store the handles. Called once, from the glue, before any session opens.
///
/// # Safety
///
/// `vm` must be the process's `JavaVM` and `activity` a global reference to the
/// running Activity, both still live — which is what libvidya's accessors
/// return for as long as the process runs.
pub unsafe fn set(vm: *mut jni::sys::JavaVM, activity: jni::sys::jobject) {
    VM.store(vm, Ordering::Release);
    ACTIVITY.store(activity, Ordering::Release);

    // And the same pair again, into this object's `ndk_context`.
    //
    // Not for anything here: cpal's AAudio host asks `ndk_context` for the VM
    // when it opens a stream, and the copy it asks is the one linked into
    // *this* library, which nothing has ever filled in. libvidya's glue
    // initialised libvidya's copy, and that is a different static — the same
    // reason this module exists at all, arriving a second time from underneath.
    //
    // Unset, it is not a fallback but a panic: `android context was not
    // initialized`, on the audio thread, the moment a call starts.
    //
    // SAFETY: the caller's contract, and the same values stored above.
    unsafe { ndk_context::initialize_android_context(vm.cast(), activity.cast()) };
}

/// The `JavaVM`, or `None` before the glue has handed it over.
pub fn vm() -> Option<JavaVM> {
    let ptr = VM.load(Ordering::Acquire);
    if ptr.is_null() {
        return None;
    }
    // SAFETY: non-null only after `set`, whose contract is that this is the
    // process's own VM. A JavaVM outlives every thread that could read it.
    Some(unsafe { JavaVM::from_raw(ptr) })
}

/// The Activity's `jobject`, or `None` before the glue has handed it over.
pub fn activity() -> Option<jni::sys::jobject> {
    let ptr = ACTIVITY.load(Ordering::Acquire);
    if ptr.is_null() {
        None
    } else {
        Some(ptr)
    }
}

/// Load an **application** class (APK `classes.dex`) by binary name.
///
/// `Env::find_class` on a thread this library made — a tokio worker, the media
/// pump — resolves against the *system* class loader, which knows nothing of
/// the APK. `uk.nandi.frq.CameraCapture` is not there and never will be. The
/// Activity's own loader is the one that can see it.
///
/// `binary_name` is the Java binary name: dots, not slashes.
pub fn load_app_class<'a>(
    env: &mut jni::Env<'a>,
    activity: &JObject<'_>,
    binary_name: &str,
) -> Result<JClass<'a>, String> {
    let loader = env
        .call_method(
            activity,
            jni_str!("getClassLoader"),
            jni_sig!(() -> java.lang.ClassLoader),
            &[],
        )
        .map_err(|e| format!("getClassLoader: {e}"))?
        .l()
        .map_err(|e| format!("getClassLoader: {e}"))?;
    if loader.is_null() {
        return Err("Activity.getClassLoader returned null".into());
    }

    let class_name = env
        .new_string(binary_name)
        .map_err(|e| format!("new_string({binary_name}): {e}"))?;
    let loaded = env
        .call_method(
            &loader,
            jni_str!("loadClass"),
            jni_sig!((java.lang.String) -> java.lang.Class),
            &[JValue::Object(class_name.as_ref())],
        )
        .map_err(|e| format!("loadClass({binary_name}): {e}"))?
        .l()
        .map_err(|e| format!("loadClass({binary_name}): {e}"))?;
    if loaded.is_null() {
        return Err(format!("loadClass({binary_name}) returned null"));
    }
    env.cast_local::<JClass>(loaded)
        .map_err(|e| format!("cast {binary_name}: {e}"))
}