nandi/jolt-nativepublic Fork 0
789bb134c91bfdee346df2dc9656bf2e7f9bbd1a
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.

android_jni.rs · 129 lines · 5.3 KBRust Blame HistoryRaw
Let the media plane cross to the phone, camera and all fd0e21a nandi 18d ago1//! The JavaVM and the Activity, handed across from `libvidya.so`.
2//!
3//! On a desktop this library needs nothing from the host: it opens V4L2 nodes
4//! and PipeWire streams by itself. On the phone the camera is Java, and every
5//! call into it needs the process's `JavaVM` and the running `Activity`.
6//!
7//! Neither is reachable from here. `android-activity`'s glue receives them, and
8//! the glue runs in `libvidya.so` — a different shared object, because whoever
9//! holds the glue owns the event loop and that is the UI's job, not this one's.
10//! The usual way across, `ndk_context`, does not answer: its handles live in a
11//! `static` inside the `ndk-context` crate, and a `static` is per-object. Two
12//! cdylibs that both link it get two of them, and the one this library would
13//! read is the one nobody ever set.
14//!
15//! So the handles are pushed rather than pulled. `libjoltapp.so` links both
16//! objects, reads them out of libvidya's C ABI, and passes them to
17//! [`crate::joltmoq_android_init`] before any call starts. Everything here is
18//! `None` until that has happened, and a camera call fails with a reason rather
19//! than dereferencing a null.
20
21use std::ptr;
22use std::sync::atomic::{AtomicPtr, Ordering};
23
24use jni::objects::{JClass, JObject, JValue};
25use jni::{jni_sig, jni_str, JavaVM};
26
27/// Set once, before the application starts. Read from every thread that talks
28/// to Camera2 — the tokio workers among them, which is why this is not a
29/// `Mutex<Option<_>>` anyone could hold across a JNI call.
30static VM: AtomicPtr<jni::sys::JavaVM> = AtomicPtr::new(ptr::null_mut());
31
32/// The Activity, as the glue's own global reference. Not a local ref that would
33/// die with the frame that made it: libvidya holds this one for the lifetime of
34/// the process, so it is as good here as it is there.
35static ACTIVITY: AtomicPtr<jni::sys::_jobject> = AtomicPtr::new(ptr::null_mut());
36
37/// Store the handles. Called once, from the glue, before any session opens.
38///
39/// # Safety
40///
41/// `vm` must be the process's `JavaVM` and `activity` a global reference to the
42/// running Activity, both still live — which is what libvidya's accessors
43/// return for as long as the process runs.
44pub unsafe fn set(vm: *mut jni::sys::JavaVM, activity: jni::sys::jobject) {
45 VM.store(vm, Ordering::Release);
46 ACTIVITY.store(activity, Ordering::Release);
47
48 // And the same pair again, into this object's `ndk_context`.
49 //
50 // Not for anything here: cpal's AAudio host asks `ndk_context` for the VM
51 // when it opens a stream, and the copy it asks is the one linked into
52 // *this* library, which nothing has ever filled in. libvidya's glue
53 // initialised libvidya's copy, and that is a different static — the same
54 // reason this module exists at all, arriving a second time from underneath.
55 //
56 // Unset, it is not a fallback but a panic: `android context was not
57 // initialized`, on the audio thread, the moment a call starts.
58 //
59 // SAFETY: the caller's contract, and the same values stored above.
60 unsafe { ndk_context::initialize_android_context(vm.cast(), activity.cast()) };
61}
62
63/// The `JavaVM`, or `None` before the glue has handed it over.
64pub fn vm() -> Option<JavaVM> {
65 let ptr = VM.load(Ordering::Acquire);
66 if ptr.is_null() {
67 return None;
68 }
69 // SAFETY: non-null only after `set`, whose contract is that this is the
70 // process's own VM. A JavaVM outlives every thread that could read it.
71 Some(unsafe { JavaVM::from_raw(ptr) })
72}
73
74/// The Activity's `jobject`, or `None` before the glue has handed it over.
75pub fn activity() -> Option<jni::sys::jobject> {
76 let ptr = ACTIVITY.load(Ordering::Acquire);
77 if ptr.is_null() {
78 None
79 } else {
80 Some(ptr)
81 }
82}
83
84/// Load an **application** class (APK `classes.dex`) by binary name.
85///
86/// `Env::find_class` on a thread this library made — a tokio worker, the media
87/// pump — resolves against the *system* class loader, which knows nothing of
88/// the APK. `uk.nandi.frq.CameraCapture` is not there and never will be. The
89/// Activity's own loader is the one that can see it.
90///
91/// `binary_name` is the Java binary name: dots, not slashes.
92pub fn load_app_class<'a>(
93 env: &mut jni::Env<'a>,
94 activity: &JObject<'_>,
95 binary_name: &str,
96) -> Result<JClass<'a>, String> {
97 let loader = env
98 .call_method(
99 activity,
100 jni_str!("getClassLoader"),
101 jni_sig!(() -> java.lang.ClassLoader),
102 &[],
103 )
104 .map_err(|e| format!("getClassLoader: {e}"))?
105 .l()
106 .map_err(|e| format!("getClassLoader: {e}"))?;
107 if loader.is_null() {
108 return Err("Activity.getClassLoader returned null".into());
109 }
110
111 let class_name = env
112 .new_string(binary_name)
113 .map_err(|e| format!("new_string({binary_name}): {e}"))?;
114 let loaded = env
115 .call_method(
116 &loader,
117 jni_str!("loadClass"),
118 jni_sig!((java.lang.String) -> java.lang.Class),
119 &[JValue::Object(class_name.as_ref())],
120 )
121 .map_err(|e| format!("loadClass({binary_name}): {e}"))?
122 .l()
123 .map_err(|e| format!("loadClass({binary_name}): {e}"))?;
124 if loaded.is_null() {
125 return Err(format!("loadClass({binary_name}) returned null"));
126 }
127 env.cast_local::<JClass>(loaded)
128 .map_err(|e| format!("cast {binary_name}: {e}"))
129}