refactor(android): forbid unsafe workspace-wide, quarantine JNI in app (#91)
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>
This commit is contained in:
@@ -1,10 +1,5 @@
|
||||
//! Safe-area insets.
|
||||
//!
|
||||
// JNI FFI (Android only) requires `unsafe` to reconstruct `JavaVM` /
|
||||
// `JObject` handles from raw pointers handed over by the runtime. Scoped to
|
||||
// this module so the rest of the workspace stays `deny(unsafe_code)`.
|
||||
#![allow(unsafe_code)]
|
||||
//!
|
||||
//! Reports the OS-reserved regions around the playable surface (status
|
||||
//! bar at the top, gesture / navigation bar at the bottom on Android,
|
||||
//! display cutouts, etc.) so UI anchored to a screen edge can avoid
|
||||
@@ -296,30 +291,12 @@ mod android {
|
||||
}
|
||||
|
||||
fn query_insets() -> Result<SafeAreaInsets, String> {
|
||||
use bevy::android::ANDROID_APP;
|
||||
use jni::{JavaVM, objects::JObject};
|
||||
use solitaire_data::android_jni;
|
||||
|
||||
let app = ANDROID_APP
|
||||
.get()
|
||||
.ok_or_else(|| "ANDROID_APP not initialized".to_string())?;
|
||||
|
||||
// SAFETY: `vm_as_ptr()` returns the JavaVM* set up by the Android
|
||||
// runtime; valid for the lifetime of the process.
|
||||
let vm = unsafe { JavaVM::from_raw(app.vm_as_ptr().cast()) }
|
||||
.map_err(|e| format!("JavaVM::from_raw: {e}"))?;
|
||||
|
||||
let mut env = vm
|
||||
.attach_current_thread_permanently()
|
||||
.map_err(|e| format!("attach_current_thread: {e}"))?;
|
||||
|
||||
// SAFETY: `activity_as_ptr()` returns the NativeActivity jobject
|
||||
// pointer — valid for the lifetime of the process.
|
||||
let activity = unsafe { JObject::from_raw(app.activity_as_ptr() as _) };
|
||||
|
||||
(|| -> jni::errors::Result<SafeAreaInsets> {
|
||||
android_jni::with_activity_env(|env, activity| {
|
||||
// Window window = activity.getWindow();
|
||||
let window = env
|
||||
.call_method(&activity, "getWindow", "()Landroid/view/Window;", &[])?
|
||||
.call_method(activity, "getWindow", "()Landroid/view/Window;", &[])?
|
||||
.l()?;
|
||||
|
||||
// View decor = window.getDecorView();
|
||||
@@ -371,8 +348,7 @@ mod android {
|
||||
left,
|
||||
right,
|
||||
})
|
||||
})()
|
||||
.map_err(|e| format!("safe-area JNI: {e}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user