From 65495afa3723441e8ec2039c2a586350f304d0ab Mon Sep 17 00:00:00 2001 From: elelanv Date: Thu, 18 Jun 2026 18:04:04 +0530 Subject: [PATCH 1/3] fix(android): upgrade jni to 0.22.4 for veilid-core compatibility veilid-core v0.5.3 depends on jni 0.22.x and exposes veilid_core_setup_android(EnvUnowned, JObject), which is incompatible with the jni 0.21 API used by the Android bridge. Migrate android_bridge.rs and jni_globals.rs to jni 0.22's EnvUnowned/ Env pattern, and harden build-android.sh NDK auto-detection on macOS without changing the jniLibs output path. Verified: - cargo build --release (host) - cargo ndk build (aarch64-linux-android) - cargo test -- --test-threads=1 (13 passed, 2 ignored) --- Cargo.lock | 2 +- Cargo.toml | 2 +- build-android.sh | 25 +++-- src/android_bridge.rs | 224 +++++++++++++++++------------------------- src/jni_globals.rs | 41 ++++---- 5 files changed, 127 insertions(+), 167 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1896b83..a5c373c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5809,7 +5809,7 @@ dependencies = [ "futures", "hickory-resolver", "iroh-blobs", - "jni 0.21.1", + "jni 0.22.4", "lazy_static", "log", "num_cpus", diff --git a/Cargo.toml b/Cargo.toml index 53f3052..aac5719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ log = "0.4" env_logger = "0.10" [target.'cfg(target_os = "android")'.dependencies] -jni = "0.21.1" +jni = "0.22.4" tokio = { version = "^1.43", default-features = false, features = ["rt", "rt-multi-thread", "sync", "time", "macros"] } veilid-core = { git = "https://gitlab.com/veilid/veilid.git", tag = "v0.5.3" } blake3 = "1.8.2" diff --git a/build-android.sh b/build-android.sh index 42ae22d..58601c0 100755 --- a/build-android.sh +++ b/build-android.sh @@ -3,15 +3,26 @@ clear # Auto-detect Android SDK if not provided -if [ -z "$ANDROID_HOME" ] && [ -d "$HOME/Android/Sdk" ]; then - export ANDROID_HOME="$HOME/Android/Sdk" +if [ -z "$ANDROID_HOME" ]; then + if [ -d "$HOME/Android/Sdk" ]; then + export ANDROID_HOME="$HOME/Android/Sdk" + elif [ -d "$HOME/Library/Android/sdk" ]; then + export ANDROID_HOME="$HOME/Library/Android/sdk" + fi fi # Auto-detect Android NDK if not provided -if [ -z "$ANDROID_NDK_HOME" ] && [ -d "$HOME/Android/Sdk/ndk" ]; then - NDK_LATEST="$(ls -1 "$HOME/Android/Sdk/ndk" 2>/dev/null | sort -V | tail -n 1)" - if [ -n "$NDK_LATEST" ] && [ -d "$HOME/Android/Sdk/ndk/$NDK_LATEST" ]; then - export ANDROID_NDK_HOME="$HOME/Android/Sdk/ndk/$NDK_LATEST" +if [ -z "$ANDROID_NDK_HOME" ] && [ -n "$ANDROID_HOME" ] && [ -d "$ANDROID_HOME/ndk" ]; then + NDK_LATEST="" + for ndk_dir in "$ANDROID_HOME/ndk"/*/; do + [ -d "$ndk_dir" ] || continue + candidate="$(basename "$ndk_dir")" + if [ -z "$NDK_LATEST" ] || [ "$(printf '%s\n' "$NDK_LATEST" "$candidate" | sort -V | tail -n 1)" = "$candidate" ]; then + NDK_LATEST="$candidate" + fi + done + if [ -n "$NDK_LATEST" ] && [ -d "$ANDROID_HOME/ndk/$NDK_LATEST" ]; then + export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_LATEST" fi fi @@ -51,4 +62,4 @@ cargo ndk -o $JNI_DIR \ --manifest-path ../Cargo.toml \ -t arm64-v8a \ -t x86_64 \ - build --release + build --release \ No newline at end of file diff --git a/src/android_bridge.rs b/src/android_bridge.rs index ee76a6a..65bbcd5 100644 --- a/src/android_bridge.rs +++ b/src/android_bridge.rs @@ -6,81 +6,74 @@ use crate::server; use crate::server::start; use crate::{log_debug, log_error, log_info}; use jni::errors::Result as JniResult; +use jni::errors::ThrowRuntimeExAndDefault; +use jni::jni_sig; +use jni::jni_str; +use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::{jint, jstring}; -use jni::{ - objects::GlobalRef, objects::JClass, objects::JMethodID, objects::JObject, objects::JString, - objects::JValue, objects::JValueGen, JNIEnv, JavaVM, -}; -use lazy_static::lazy_static; -use std::error::Error; -use std::sync::{Arc, Mutex, Once}; -use std::thread; +use jni::{Env, EnvUnowned}; use std::time::Duration; use veilid_core::veilid_core_setup_android; -trait IntoJObject { - fn into_jobject(&self) -> JObject; -} - -impl IntoJObject for GlobalRef { - fn into_jobject(&self) -> JObject { - unsafe { JObject::from_raw(self.as_raw()) } - } -} - #[no_mangle] #[allow(non_snake_case)] pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_initializeRustService( - env: JNIEnv, - class: JClass, + mut env: EnvUnowned, + _class: JClass, ) { - // match jni_globals::setup_android(env, class) { - // Ok(_) => log_debug!(TAG, "Rust service initialized successfully"), - // Err(e) => log_error!(TAG, "Failed to initialize Rust service: {:?}", e), - // } + env.with_env(|_env| -> JniResult<()> { + // match jni_globals::setup_android(env, class) { + // Ok(_) => log_debug!(TAG, "Rust service initialized successfully"), + // Err(e) => log_error!(TAG, "Failed to initialize Rust service: {:?}", e), + // } - log_info!(TAG, "SnowbirdBridge initialized"); + log_info!(TAG, "SnowbirdBridge initialized"); + Ok(()) + }) + .resolve::(); } #[no_mangle] #[allow(non_snake_case)] pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_startServer( - mut env: JNIEnv, + mut env: EnvUnowned, clazz: JClass, context: JObject, backend_base_directory: JString, server_socket_path: JString, ) -> jstring { - let env_ptr = env.get_native_interface(); - log_debug!(TAG, "Bridge: starting"); - match setup_jni_environments(&mut env, context, clazz) { - Ok(_) => { - log_debug!(TAG, "JNI stuff successful"); - } - Err(e) => { - log_error!(TAG, "Error doing JNI stuff: {:?}", e); - } - } + // Initialize JNI globals, smoke-test the Java callback, and read Java args while + // EnvUnowned is still available. veilid_core_setup_android consumes env/context. + let (backend_base_directory, server_socket_path, output) = env + .with_env(|env| -> JniResult<(String, String, jstring)> { + jni_globals::init_jni(env, clazz).map_err(|e| { + jni::errors::Error::ParseFailed(format!("Failed to initialize JNI globals: {e}")) + })?; + jni_smoke_test(env)?; + + let backend_base_directory = backend_base_directory.try_to_string(env)?; + let server_socket_path = server_socket_path.try_to_string(env)?; + let output = JString::from_str( + env, + format!("Server started on Unix socket: {server_socket_path}"), + )? + .into_raw(); - let backend_base_directory: String = env - .get_string(&backend_base_directory) - .expect("Couldn't get socket path string") - .into(); + log_debug!(TAG, "JNI stuff successful"); - let server_socket_path: String = env - .get_string(&server_socket_path) - .expect("Couldn't get socket path string") - .into(); + Ok((backend_base_directory, server_socket_path, output)) + }) + .resolve::(); - let backend_base_directory_clone = backend_base_directory.clone(); - let server_socket_path_clone = server_socket_path.clone(); + // Use another new JNIEnv for veilid_core_setup_android + veilid_core_setup_android(env, context); std::thread::spawn(move || { let runtime = tokio::runtime::Runtime::new().unwrap(); runtime.block_on(async { - start(&backend_base_directory_clone, &server_socket_path_clone) + start(&backend_base_directory, &server_socket_path) .await .unwrap(); }); @@ -88,105 +81,67 @@ pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_Sn log_debug!(TAG, "Bridge startup complete."); - let output = env - .new_string(format!( - "Server started on Unix socket: {}", - server_socket_path - )) - .expect("Couldn't create java string!"); - - output.into_raw() + output } #[no_mangle] #[allow(non_snake_case)] pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_SnowbirdBridge_stopServer( - mut env: JNIEnv, + mut env: EnvUnowned, _clazz: JClass, - ctx: JObject, + _ctx: JObject, ) -> jstring { log_debug!(TAG, "Bridge: stopping server"); - // Create a runtime to handle async operations - let runtime = tokio::runtime::Runtime::new().unwrap(); - - // Stop the backend server and clean up Veilid API - let stop_result = runtime.block_on(async { - // First stop the backend - match server::stop().await { - Ok(_) => { - log_info!(TAG, "Backend stopped successfully"); - - // Get the backend to access Veilid API - if let Ok(backend) = server::get_backend().await { - // Shutdown Veilid API - if let Some(veilid_api) = backend.get_veilid_api().await { - veilid_api.shutdown().await; - log_info!(TAG, "Veilid API shut down successfully"); + let stop_ok = env + .with_env(|_env| -> JniResult { + // Create a runtime to handle async operations + let runtime = tokio::runtime::Runtime::new().unwrap(); + + // Stop the backend server and clean up Veilid API + runtime.block_on(async { + // First stop the backend + match server::stop().await { + Ok(_) => { + log_info!(TAG, "Backend stopped successfully"); + + // Get the backend to access Veilid API + if let Ok(backend) = server::get_backend().await { + // Shutdown Veilid API + if let Some(veilid_api) = backend.get_veilid_api().await { + veilid_api.shutdown().await; + log_info!(TAG, "Veilid API shut down successfully"); + } + } + + // Add a small delay to ensure tasks complete + tokio::time::sleep(Duration::from_millis(500)).await; + Ok(true) + } + Err(e) => { + log_error!(TAG, "Error stopping server: {:?}", e); + Ok(false) } } - - // Add a small delay to ensure tasks complete - tokio::time::sleep(Duration::from_millis(500)).await; - - Ok(()) - } - Err(e) => { - log_error!(TAG, "Error stopping server: {:?}", e); - Err(e) - } - } - }); + }) + }) + .resolve::(); // Create response string based on result - let response = match stop_result { - Ok(_) => "Server stopped successfully", - Err(_) => "Error stopping server", + let response = if stop_ok { + "Server stopped successfully" + } else { + "Error stopping server" }; - let output = env - .new_string(response) - .expect("Couldn't create java string!"); - - output.into_raw() + env.with_env(|env| -> JniResult { + let output = JString::from_str(env, response)?; + Ok(output.into_raw()) + }) + .resolve::() } -fn with_env(env: &mut JNIEnv, f: F) -> Result> -where - F: FnOnce(JNIEnv) -> Result>, -{ - let env_ptr = env.get_native_interface(); - let new_env = unsafe { JNIEnv::from_raw(env_ptr).unwrap() }; - f(new_env) -} - -fn setup_jni_environments( - env: &mut JNIEnv, - context: JObject, - clazz: JClass, -) -> Result<(), Box> { - with_env(env, |env| Ok(jni_globals::init_jni(&env, clazz))); - - let global_context = env.new_global_ref(context)?; - - // Use a new JNIEnv for jni_smoke_test - with_env(env, |env| { - jni_smoke_test(env, global_context.into_jobject()) - })?; - - // Use another new JNIEnv for veilid_core_setup_android - with_env(env, |env| { - veilid_core_setup_android(env, global_context.into_jobject()); - Ok(()) - })?; - - Ok(()) -} - -fn jni_smoke_test<'local>( - mut env: JNIEnv<'local>, - context: JObject<'local>, -) -> Result<(), Box> { +fn jni_smoke_test(env: &mut Env) -> JniResult<()> { let class_name = "net/opendasharchive/openarchive/services/snowbird/SnowbirdBridge"; let method_name = "updateStatusFromRust"; let method_signature = "(ILjava/lang/String;)V"; @@ -196,15 +151,14 @@ fn jni_smoke_test<'local>( // Create a JValue for the String parameter (can be null) let error_message = env.new_string("Test error message")?; - let error_message_jvalue = JValue::Object(&error_message); // Call the static method env.call_static_method( - class_name, - method_name, - method_signature, - &[JValue::Int(status_code), error_message_jvalue], + jni_str!("net/opendasharchive/openarchive/services/snowbird/SnowbirdBridge"), + jni_str!("updateStatusFromRust"), + jni_sig!("(ILjava/lang/String;)V"), + &[JValue::Int(status_code), JValue::Object(&error_message)], )?; Ok(()) -} +} \ No newline at end of file diff --git a/src/jni_globals.rs b/src/jni_globals.rs index a3ff275..7edf2bc 100644 --- a/src/jni_globals.rs +++ b/src/jni_globals.rs @@ -1,9 +1,8 @@ #![allow(clippy::result_large_err)] // Allows for larger error types in Result -use jni::objects::{GlobalRef, JClass}; -use jni::AttachGuard; -use jni::JNIEnv; -use jni::JavaVM; +use jni::objects::{Global, JClass}; +use jni::vm::JavaVM; +use jni::Env; use once_cell::sync::Lazy; use std::result::Result as StdResult; use std::sync::{Arc, Mutex, Once}; @@ -28,23 +27,22 @@ pub enum JniError { pub type JniResult = StdResult; static JAVA_VM: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); -static CLASS: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); +static CLASS: Lazy>>>>> = + Lazy::new(|| Arc::new(Mutex::new(None))); static INIT: Once = Once::new(); #[allow(dead_code)] pub fn get_java_vm() -> JniResult { - let jvm_locked = JAVA_VM.lock(); - let jvm = jvm_locked.as_ref().unwrap(); - let env = jvm + let jvm_locked = JAVA_VM + .lock() + .map_err(|e| JniError::InitializationError(format!("Failed to acquire JavaVM lock: {e}")))?; + jvm_locked .as_ref() - .unwrap() - .attach_current_thread_as_daemon() - .unwrap(); - let vm = env.get_java_vm(); - return Ok(vm?); + .cloned() + .ok_or_else(|| JniError::InitializationError("JavaVM not initialized".into())) } -pub fn init_jni(env: &JNIEnv, class: JClass) -> JniResult<()> { +pub fn init_jni(env: &mut Env, class: JClass) -> JniResult<()> { INIT.call_once(|| { if let Err(e) = init_jni_inner(env, class) { eprintln!("Failed to initialize JNI: {e}"); @@ -53,7 +51,7 @@ pub fn init_jni(env: &JNIEnv, class: JClass) -> JniResult<()> { Ok(()) } -fn init_jni_inner(env: &JNIEnv, class: JClass) -> JniResult<()> { +fn init_jni_inner(env: &mut Env, class: JClass) -> JniResult<()> { let java_vm = env.get_java_vm()?; let global_class = env.new_global_ref(class)?; @@ -79,7 +77,7 @@ where #[allow(dead_code)] pub fn with_class(f: F) -> JniResult where - F: FnOnce(&GlobalRef) -> JniResult, + F: FnOnce(&Global>) -> JniResult, { let class_guard = CLASS .lock() @@ -103,7 +101,7 @@ where pub fn with_env(f: F) -> JniResult where - F: FnOnce(AttachGuard) -> JniResult, + F: FnOnce(&mut Env) -> JniResult, { let vm_guard = JAVA_VM .lock() @@ -113,9 +111,6 @@ where .as_ref() .ok_or_else(|| JniError::InitializationError("JavaVM not initialized".into()))?; - let env = vm - .attach_current_thread() - .map_err(|e| JniError::ThreadAttachError(format!("Failed to attach thread: {e}")))?; - - f(env) -} + vm.attach_current_thread(f) + .map_err(|e| JniError::ThreadAttachError(format!("Failed to attach thread: {e}"))) +} \ No newline at end of file From 3e112b854462175a30f88b448933d0f691d716a4 Mon Sep 17 00:00:00 2001 From: elelanv Date: Thu, 18 Jun 2026 18:10:38 +0530 Subject: [PATCH 2/3] chore(android): build arm64-v8a, armeabi-v7a, and x86_64 ABIs Add armeabi-v7a for older 32-bit ARM devices to match the Android app's Rust tooling. Skip deprecated 32-bit x86 (i686) emulator ABI. --- build-android.sh | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/build-android.sh b/build-android.sh index 58601c0..fd1d519 100755 --- a/build-android.sh +++ b/build-android.sh @@ -46,20 +46,18 @@ mkdir -p $JNI_DIR # # cargo update save-dweb-backend -# Add this target if we need to support older devices. -# armv7-linux-androideabi -# +# arm64-v8a: modern 64-bit ARM devices (primary production ABI) +# armv7-linux-androideabi: older 32-bit ARM devices (armeabi-v7a) +# x86_64-linux-android: emulators and x86_64 devices (dev/CI) rustup target add \ aarch64-linux-android \ + armv7-linux-androideabi \ x86_64-linux-android # Build the android libraries in the jniLibs directory -# -# Add this target if we need to support older devices. -# armeabi-v7a -# cargo ndk -o $JNI_DIR \ --manifest-path ../Cargo.toml \ -t arm64-v8a \ + -t armeabi-v7a \ -t x86_64 \ build --release \ No newline at end of file From 977aab4828b3fb9a5bc3483cbbcc5555986ca807 Mon Sep 17 00:00:00 2001 From: elelanv Date: Fri, 19 Jun 2026 15:30:41 +0530 Subject: [PATCH 3/3] fix(android): return early from startServer on JNI init failure When with_env().resolve::() fails, jni 0.22 throws to Java and returns a null jstring. Bail out before calling veilid_core_setup_android or spawning the server with empty paths. --- src/android_bridge.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/android_bridge.rs b/src/android_bridge.rs index 65bbcd5..a981b98 100644 --- a/src/android_bridge.rs +++ b/src/android_bridge.rs @@ -67,7 +67,11 @@ pub extern "system" fn Java_net_opendasharchive_openarchive_services_snowbird_Sn }) .resolve::(); - // Use another new JNIEnv for veilid_core_setup_android + // resolve() throws to Java and returns null on failure; do not start Veilid or the server. + if output.is_null() { + return output; + } + veilid_core_setup_android(env, context); std::thread::spawn(move || {