9299176b2d
Addresses #91: the #90 posture (`deny(unsafe_code)` + three scattered `#![allow(unsafe_code)]` across solitaire_data and solitaire_engine) punched unsafe holes into otherwise-pure logic crates. Replace it by *reducing* the unsafe rather than relocating it, then forbidding it everywhere it can be forbidden. Changes: - solitaire_data: new safe `android_jni` bridge owning the cached `JavaVM` and activity `GlobalRef`; exposes `with_env` / `with_activity_env` so keystore/clipboard/safe-area never touch a raw handle. - keystore: drop the `JavaVM::from_raw` init path (now in the app) and replace the three `unsafe { JByteArray::from_raw(x.into_raw()) }` casts with the safe `JByteArray::from(JObject)` conversion jni 0.21 provides. - engine clipboard + safe_area: route through the bridge; remove their `#![allow(unsafe_code)]` and all `from_raw` calls. - solitaire_app: becomes the single owner of FFI unsafe. `android_main` reconstructs the raw `JavaVM` / activity once (it must, as the cdylib that exports `#[unsafe(no_mangle)]`) and registers the safe wrappers. It opts to its own `deny`-level lints with two scoped `#[allow(unsafe_code)]`. - workspace: `unsafe_code` is now `forbid`. Every crate except the app entry point is fully unsafe-free. Net: 7 unsafe sites across three crates collapse to 3 at the OS boundary in one crate. Verified with host `clippy --workspace -- -D warnings` and an `aarch64-linux-android` clippy build of solitaire_app (transitively engine + data); also fixed two latent android-only `collapsible_if` warnings surfaced in the keystore by the cross-target check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
66 lines
2.6 KiB
Rust
66 lines
2.6 KiB
Rust
//! Safe JNI bridge for Android platform integration.
|
|
//!
|
|
//! Reconstructing the raw `JavaVM` / `NativeActivity` handles handed over by
|
|
//! the Android runtime requires `unsafe` FFI. That single reconstruction lives
|
|
//! in `solitaire_app`'s entry point ([`solitaire_app::android_main`]); this
|
|
//! module only ever stores the resulting *safe* [`JavaVM`] and activity
|
|
//! [`GlobalRef`] and hands callers an attached [`JNIEnv`].
|
|
//!
|
|
//! As a result every consumer of Android JNI — the keystore, clipboard, and
|
|
//! safe-area subsystems — goes through the safe functions here and stays
|
|
//! `forbid(unsafe_code)`. The only crate carrying `unsafe` is `solitaire_app`.
|
|
//!
|
|
//! Only compiled and linked on `target_os = "android"`.
|
|
|
|
use jni::objects::{GlobalRef, JObject};
|
|
use jni::{JNIEnv, JavaVM};
|
|
use std::sync::OnceLock;
|
|
|
|
static ANDROID_JVM: OnceLock<JavaVM> = OnceLock::new();
|
|
static ANDROID_ACTIVITY: OnceLock<GlobalRef> = OnceLock::new();
|
|
|
|
/// Store the process-wide [`JavaVM`]. Called once from Android startup
|
|
/// (`solitaire_app::android_main`); subsequent calls are ignored.
|
|
pub fn set_jvm(vm: JavaVM) {
|
|
let _ = ANDROID_JVM.set(vm);
|
|
}
|
|
|
|
/// Store a global reference to the `NativeActivity`. Called once from Android
|
|
/// startup; subsequent calls are ignored.
|
|
pub fn set_activity(activity: GlobalRef) {
|
|
let _ = ANDROID_ACTIVITY.set(activity);
|
|
}
|
|
|
|
/// Run `f` with a [`JNIEnv`] attached to the current thread.
|
|
///
|
|
/// Returns an error string if the bridge has not been initialised yet or the
|
|
/// thread cannot be attached. The closure's JNI errors are surfaced through the
|
|
/// same `String` channel so callers have a single error type to map.
|
|
pub fn with_env<F, R>(f: F) -> Result<R, String>
|
|
where
|
|
F: for<'local> FnOnce(&mut JNIEnv<'local>) -> jni::errors::Result<R>,
|
|
{
|
|
let vm = ANDROID_JVM
|
|
.get()
|
|
.ok_or_else(|| "Android JavaVM not initialised".to_string())?;
|
|
let mut env = vm
|
|
.attach_current_thread_permanently()
|
|
.map_err(|e| format!("attach_current_thread: {e}"))?;
|
|
f(&mut env).map_err(|e| format!("JNI: {e}"))
|
|
}
|
|
|
|
/// Run `f` with an attached [`JNIEnv`] and the `NativeActivity` object.
|
|
///
|
|
/// Like [`with_env`] but also resolves the cached activity global reference, so
|
|
/// callers that need to invoke instance methods on the activity (clipboard,
|
|
/// window insets) never touch a raw handle.
|
|
pub fn with_activity_env<F, R>(f: F) -> Result<R, String>
|
|
where
|
|
F: for<'local> FnOnce(&mut JNIEnv<'local>, &JObject<'local>) -> jni::errors::Result<R>,
|
|
{
|
|
let activity = ANDROID_ACTIVITY
|
|
.get()
|
|
.ok_or_else(|| "Android activity not initialised".to_string())?;
|
|
with_env(|env| f(env, activity.as_obj()))
|
|
}
|