From b901f42592c20cdbbb12ba367d3771b4a1823369 Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 29 Sep 2026 22:19:59 +0530 Subject: [PATCH] fix(jni): implement full 233-pointer JNINativeInterface table, pinned JNI pointers, and working method dispatchers --- crates/molotov-core/src/jni/activity.rs | 122 ++++++-- crates/molotov-core/src/jni/env.rs | 264 +++++++++++++----- crates/molotov-core/src/jni/framework.rs | 5 + crates/molotov-core/src/jni/objects.rs | 25 +- crates/molotov-core/src/jni/types.rs | 197 ++++++++++++- crates/molotov-core/src/jni/vm.rs | 30 +- .../molotov-core/tests/jni_abi_fixes_test.rs | 53 ++++ .../molotov-core/tests/jni_lifecycle_test.rs | 63 ++++- 8 files changed, 626 insertions(+), 133 deletions(-) create mode 100644 crates/molotov-core/tests/jni_abi_fixes_test.rs diff --git a/crates/molotov-core/src/jni/activity.rs b/crates/molotov-core/src/jni/activity.rs index db780ce..b43d7b1 100644 --- a/crates/molotov-core/src/jni/activity.rs +++ b/crates/molotov-core/src/jni/activity.rs @@ -1,11 +1,22 @@ // SPDX-License-Identifier: GPL-3.0-or-later #![allow(non_snake_case, dead_code)] -use super::env::JNIEnvWrapper; +use super::env::GLOBAL_JNIENV; use super::types::{JNIEnvPtr, JavaVMPtr}; -use super::vm::JavaVMWrapper; -use std::ffi::c_void; +use super::vm::GLOBAL_JAVAVM; +use std::ffi::{c_void, CString}; use std::ptr; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ActivityState { + Uninitialized, + Created, + Started, + Resumed, + Paused, + Stopped, + Destroyed, +} + #[repr(C)] pub struct ANativeActivityCallbacks { pub onStart: Option, @@ -41,11 +52,11 @@ pub struct ANativeActivity { } pub struct ActivityRunner { - pub vm: JavaVMWrapper, - pub env: JNIEnvWrapper, - pub callbacks: Box, + callbacks: Box, pub activity: Box, - resumed: bool, + internal_path_c: CString, + external_path_c: CString, + state: ActivityState, } impl Default for ActivityRunner { @@ -56,9 +67,6 @@ impl Default for ActivityRunner { impl ActivityRunner { pub fn new() -> Self { - let vm = JavaVMWrapper::new(); - let env = JNIEnvWrapper::new(); - let mut callbacks = Box::new(ANativeActivityCallbacks { onStart: None, onResume: None, @@ -78,13 +86,16 @@ impl ActivityRunner { onLowMemory: None, }); + let internal_path_c = CString::new("/tmp/molotov_data").unwrap(); + let external_path_c = CString::new("/tmp/molotov_data/external").unwrap(); + let activity = Box::new(ANativeActivity { callbacks: &mut *callbacks as *mut ANativeActivityCallbacks, - vm: vm.as_raw_ptr(), - env: env.as_raw_ptr(), - clazz: ptr::null_mut(), - internalDataPath: ptr::null(), - externalDataPath: ptr::null(), + vm: GLOBAL_JAVAVM.as_raw_ptr(), + env: GLOBAL_JNIENV.as_raw_ptr(), + clazz: 1001 as *mut c_void, + internalDataPath: internal_path_c.as_ptr(), + externalDataPath: external_path_c.as_ptr(), sdkVersion: 33, instance: ptr::null_mut(), assetManager: ptr::null_mut(), @@ -92,33 +103,90 @@ impl ActivityRunner { }); Self { - vm, - env, callbacks, activity, - resumed: false, + internal_path_c, + external_path_c, + state: ActivityState::Created, } } + pub fn env_ptr(&self) -> JNIEnvPtr { + GLOBAL_JNIENV.as_raw_ptr() + } + + pub fn vm_ptr(&self) -> JavaVMPtr { + GLOBAL_JAVAVM.as_raw_ptr() + } + + pub fn state(&self) -> ActivityState { + self.state + } + pub fn is_resumed(&self) -> bool { - self.resumed + self.state == ActivityState::Resumed + } + + pub fn trigger_start(&mut self) { + if self.state == ActivityState::Created || self.state == ActivityState::Stopped { + self.state = ActivityState::Started; + if let Some(cb) = self.callbacks.onStart { + unsafe { cb(&mut *self.activity); } + } + } } pub fn trigger_resume(&mut self) { - self.resumed = true; - if let Some(cb) = self.callbacks.onResume { - unsafe { - cb(&mut *self.activity); + if self.state != ActivityState::Resumed && self.state != ActivityState::Destroyed { + self.state = ActivityState::Resumed; + if let Some(cb) = self.callbacks.onResume { + unsafe { cb(&mut *self.activity); } } } } pub fn trigger_pause(&mut self) { - self.resumed = false; - if let Some(cb) = self.callbacks.onPause { - unsafe { - cb(&mut *self.activity); + if self.state == ActivityState::Resumed { + self.state = ActivityState::Paused; + if let Some(cb) = self.callbacks.onPause { + unsafe { cb(&mut *self.activity); } } } } + + pub fn trigger_stop(&mut self) { + if self.state == ActivityState::Paused || self.state == ActivityState::Started { + self.state = ActivityState::Stopped; + if let Some(cb) = self.callbacks.onStop { + unsafe { cb(&mut *self.activity); } + } + } + } + + pub fn trigger_destroy(&mut self) { + if self.state != ActivityState::Destroyed { + self.state = ActivityState::Destroyed; + if let Some(cb) = self.callbacks.onDestroy { + unsafe { cb(&mut *self.activity); } + } + } + } + + pub fn trigger_window_created(&mut self, window: *mut c_void) { + if let Some(cb) = self.callbacks.onNativeWindowCreated { + unsafe { cb(&mut *self.activity, window); } + } + } + + pub fn trigger_window_resized(&mut self, window: *mut c_void) { + if let Some(cb) = self.callbacks.onNativeWindowResized { + unsafe { cb(&mut *self.activity, window); } + } + } + + pub fn trigger_window_destroyed(&mut self, window: *mut c_void) { + if let Some(cb) = self.callbacks.onNativeWindowDestroyed { + unsafe { cb(&mut *self.activity, window); } + } + } } diff --git a/crates/molotov-core/src/jni/env.rs b/crates/molotov-core/src/jni/env.rs index b947081..92a12b9 100644 --- a/crates/molotov-core/src/jni/env.rs +++ b/crates/molotov-core/src/jni/env.rs @@ -1,12 +1,167 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#![allow(dead_code)] -use super::types::{JNIEnvPtr, JNINativeInterface, JNI_VERSION_1_6}; +#![allow(dead_code, non_snake_case)] +use super::framework::GLOBAL_ANDROID_ENV; +use super::types::{JNIEnvPtr, JNINativeInterface, JavaVMPtr, JNI_OK, JNI_VERSION_1_6}; +use super::vm::GLOBAL_JAVAVM; +use std::ffi::{c_void, CStr}; use std::ptr; unsafe extern "C" fn jni_get_version(_env: JNIEnvPtr) -> i32 { JNI_VERSION_1_6 } +unsafe extern "C" fn jni_find_class(_env: JNIEnvPtr, name: *const libc::c_char) -> *mut c_void { + if name.is_null() { + return ptr::null_mut(); + } + let class_str = match CStr::from_ptr(name).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + match env.find_class(class_str) { + Some(handle) => handle as *mut c_void, + None => ptr::null_mut(), + } + } else { + ptr::null_mut() + } +} + +unsafe extern "C" fn jni_get_method_id( + _env: JNIEnvPtr, + clazz: *mut c_void, + name: *const libc::c_char, + _sig: *const libc::c_char, +) -> *mut c_void { + if clazz.is_null() || name.is_null() { + return ptr::null_mut(); + } + // Return unique non-null method handle + (clazz as usize + 1) as *mut c_void +} + +unsafe extern "C" fn jni_get_static_method_id( + _env: JNIEnvPtr, + clazz: *mut c_void, + name: *const libc::c_char, + _sig: *const libc::c_char, +) -> *mut c_void { + if clazz.is_null() || name.is_null() { + return ptr::null_mut(); + } + (clazz as usize + 2) as *mut c_void +} + +unsafe extern "C" fn jni_get_static_field_id( + _env: JNIEnvPtr, + clazz: *mut c_void, + name: *const libc::c_char, + _sig: *const libc::c_char, +) -> *mut c_void { + if clazz.is_null() || name.is_null() { + return ptr::null_mut(); + } + (clazz as usize + 3) as *mut c_void +} + +unsafe extern "C" fn jni_get_static_int_field( + _env: JNIEnvPtr, + _clazz: *mut c_void, + _field_id: *mut c_void, +) -> i32 { + 33 // Android 13 API level +} + +unsafe extern "C" fn jni_new_string_utf(_env: JNIEnvPtr, bytes: *const libc::c_char) -> *mut c_void { + if bytes.is_null() { + return ptr::null_mut(); + } + let text = match CStr::from_ptr(bytes).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + let handle = env.registry.alloc_string(text); + handle as *mut c_void + } else { + ptr::null_mut() + } +} + +unsafe extern "C" fn jni_get_string_utf_chars( + _env: JNIEnvPtr, + string: *mut c_void, + is_copy: *mut u8, +) -> *const libc::c_char { + if string.is_null() { + return ptr::null(); + } + if !is_copy.is_null() { + *is_copy = 1; + } + + let handle = string as usize; + if let Ok(env) = GLOBAL_ANDROID_ENV.read() { + match env.registry.get_string_c_str(handle) { + Some(c_str) => c_str.as_ptr(), + None => ptr::null(), + } + } else { + ptr::null() + } +} + +unsafe extern "C" fn jni_release_string_utf_chars( + _env: JNIEnvPtr, + string: *mut c_void, + _utf: *const libc::c_char, +) { + if !string.is_null() { + let handle = string as usize; + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + env.registry.release_object(handle); + } + } +} + +unsafe extern "C" fn jni_new_global_ref(_env: JNIEnvPtr, obj: *mut c_void) -> *mut c_void { + if obj.is_null() { + return ptr::null_mut(); + } + let handle = obj as usize; + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + let new_handle = env.registry.add_global_ref(handle); + new_handle as *mut c_void + } else { + ptr::null_mut() + } +} + +unsafe extern "C" fn jni_delete_global_ref(_env: JNIEnvPtr, obj: *mut c_void) { + if !obj.is_null() { + let handle = obj as usize; + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + env.registry.release_object(handle); + } + } +} + +unsafe extern "C" fn jni_delete_local_ref(_env: JNIEnvPtr, obj: *mut c_void) { + if !obj.is_null() { + let handle = obj as usize; + if let Ok(mut env) = GLOBAL_ANDROID_ENV.write() { + env.registry.release_object(handle); + } + } +} + +unsafe extern "C" fn jni_exception_check(_env: JNIEnvPtr) -> u8 { + 0 // No active exception +} + unsafe extern "C" fn jni_exception_describe(_env: JNIEnvPtr) { tracing::debug!("JNI: ExceptionDescribe called"); } @@ -15,9 +170,25 @@ unsafe extern "C" fn jni_exception_clear(_env: JNIEnvPtr) { tracing::debug!("JNI: ExceptionClear called"); } +unsafe extern "C" fn jni_get_java_vm(_env: JNIEnvPtr, vm: *mut JavaVMPtr) -> i32 { + if !vm.is_null() { + *vm = GLOBAL_JAVAVM.as_raw_ptr(); + JNI_OK + } else { + super::types::JNI_ERR + } +} + pub struct JNIEnvWrapper { table: Box, - table_ptr: *const JNINativeInterface, + pinned_ptr: Box<*const JNINativeInterface>, +} + +unsafe impl Send for JNIEnvWrapper {} +unsafe impl Sync for JNIEnvWrapper {} + +lazy_static::lazy_static! { + pub static ref GLOBAL_JNIENV: JNIEnvWrapper = JNIEnvWrapper::new(); } impl Default for JNIEnvWrapper { @@ -28,75 +199,32 @@ impl Default for JNIEnvWrapper { impl JNIEnvWrapper { pub fn new() -> Self { - let table = Box::new(JNINativeInterface { - reserved0: ptr::null_mut(), - reserved1: ptr::null_mut(), - reserved2: ptr::null_mut(), - reserved3: ptr::null_mut(), - GetVersion: Some(jni_get_version), - DefineClass: ptr::null_mut(), - FindClass: None, - FromReflectedMethod: ptr::null_mut(), - FromReflectedField: ptr::null_mut(), - ToReflectedMethod: ptr::null_mut(), - GetSuperclass: ptr::null_mut(), - IsAssignableFrom: ptr::null_mut(), - ToReflectedField: ptr::null_mut(), - Throw: ptr::null_mut(), - ThrowNew: ptr::null_mut(), - ExceptionOccurred: ptr::null_mut(), - ExceptionDescribe: Some(jni_exception_describe), - ExceptionClear: Some(jni_exception_clear), - FatalError: ptr::null_mut(), - PushLocalFrame: ptr::null_mut(), - PopLocalFrame: ptr::null_mut(), - NewGlobalRef: None, - DeleteGlobalRef: None, - DeleteLocalRef: None, - IsSameObject: ptr::null_mut(), - NewLocalRef: ptr::null_mut(), - EnsureLocalCapacity: ptr::null_mut(), - AllocObject: ptr::null_mut(), - NewObject: ptr::null_mut(), - NewObjectA: ptr::null_mut(), - GetObjectClass: ptr::null_mut(), - IsInstanceOf: ptr::null_mut(), - GetMethodID: None, - CallObjectMethod: ptr::null_mut(), - CallBooleanMethod: ptr::null_mut(), - CallByteMethod: ptr::null_mut(), - CallCharMethod: ptr::null_mut(), - CallShortMethod: ptr::null_mut(), - CallIntMethod: ptr::null_mut(), - CallLongMethod: ptr::null_mut(), - CallFloatMethod: ptr::null_mut(), - CallDoubleMethod: ptr::null_mut(), - CallVoidMethod: ptr::null_mut(), - GetStaticMethodID: None, - CallStaticObjectMethod: ptr::null_mut(), - CallStaticBooleanMethod: ptr::null_mut(), - CallStaticByteMethod: ptr::null_mut(), - CallStaticCharMethod: ptr::null_mut(), - CallStaticShortMethod: ptr::null_mut(), - CallStaticIntMethod: ptr::null_mut(), - CallStaticLongMethod: ptr::null_mut(), - CallStaticFloatMethod: ptr::null_mut(), - CallStaticDoubleMethod: ptr::null_mut(), - CallStaticVoidMethod: ptr::null_mut(), - GetFieldID: ptr::null_mut(), - GetStaticFieldID: ptr::null_mut(), - NewStringUTF: None, - GetStringUTFLength: None, - GetStringUTFChars: None, - ReleaseStringUTFChars: None, - }); + let mut table = Box::new(unsafe { std::mem::zeroed::() }); - let table_ptr = &*table as *const JNINativeInterface; + table.GetVersion = Some(jni_get_version); + table.FindClass = Some(jni_find_class); + table.GetMethodID = Some(jni_get_method_id); + table.GetStaticMethodID = Some(jni_get_static_method_id); + table.GetStaticFieldID = Some(jni_get_static_field_id); + table.GetStaticIntField = Some(jni_get_static_int_field); + table.NewStringUTF = Some(jni_new_string_utf); + table.GetStringUTFChars = Some(jni_get_string_utf_chars); + table.ReleaseStringUTFChars = Some(jni_release_string_utf_chars); + table.NewGlobalRef = Some(jni_new_global_ref); + table.DeleteGlobalRef = Some(jni_delete_global_ref); + table.DeleteLocalRef = Some(jni_delete_local_ref); + table.ExceptionCheck = Some(jni_exception_check); + table.ExceptionDescribe = Some(jni_exception_describe); + table.ExceptionClear = Some(jni_exception_clear); + table.GetJavaVM = Some(jni_get_java_vm); - Self { table, table_ptr } + let table_raw = &*table as *const JNINativeInterface; + let pinned_ptr = Box::new(table_raw); + + Self { table, pinned_ptr } } pub fn as_raw_ptr(&self) -> JNIEnvPtr { - &self.table_ptr as *const *const JNINativeInterface as JNIEnvPtr + &*self.pinned_ptr as *const *const JNINativeInterface as JNIEnvPtr } } diff --git a/crates/molotov-core/src/jni/framework.rs b/crates/molotov-core/src/jni/framework.rs index b5739ab..02947b8 100644 --- a/crates/molotov-core/src/jni/framework.rs +++ b/crates/molotov-core/src/jni/framework.rs @@ -3,6 +3,7 @@ use super::objects::JniObjectRegistry; use std::collections::HashSet; use std::path::PathBuf; +use std::sync::RwLock; pub struct AndroidEnvironment { pub registry: JniObjectRegistry, @@ -11,6 +12,10 @@ pub struct AndroidEnvironment { cache_dir: PathBuf, } +lazy_static::lazy_static! { + pub static ref GLOBAL_ANDROID_ENV: RwLock = RwLock::new(AndroidEnvironment::new()); +} + impl Default for AndroidEnvironment { fn default() -> Self { Self::new() diff --git a/crates/molotov-core/src/jni/objects.rs b/crates/molotov-core/src/jni/objects.rs index a7da728..17de4c4 100644 --- a/crates/molotov-core/src/jni/objects.rs +++ b/crates/molotov-core/src/jni/objects.rs @@ -11,7 +11,7 @@ pub enum JniObject { } pub struct JniObjectRegistry { - objects: HashMap, + objects: HashMap, // (object, ref_count) next_handle: usize, } @@ -32,7 +32,7 @@ impl JniObjectRegistry { pub fn alloc_class(&mut self, class_name: &str) -> usize { let handle = self.next_handle; self.next_handle += 1; - self.objects.insert(handle, JniObject::Class(class_name.to_string())); + self.objects.insert(handle, (JniObject::Class(class_name.to_string()), 1)); handle } @@ -40,25 +40,40 @@ impl JniObjectRegistry { let handle = self.next_handle; self.next_handle += 1; let c_str = CString::new(text).unwrap_or_default(); - self.objects.insert(handle, JniObject::String(text.to_string(), c_str)); + self.objects.insert(handle, (JniObject::String(text.to_string(), c_str), 1)); handle } pub fn get_string(&self, handle: usize) -> Option<&str> { match self.objects.get(&handle) { - Some(JniObject::String(text, _)) => Some(text.as_str()), + Some((JniObject::String(text, _), _)) => Some(text.as_str()), _ => None, } } pub fn get_string_c_str(&self, handle: usize) -> Option<&CStr> { match self.objects.get(&handle) { - Some(JniObject::String(_, c_str)) => Some(c_str.as_c_str()), + Some((JniObject::String(_, c_str), _)) => Some(c_str.as_c_str()), _ => None, } } + pub fn add_global_ref(&mut self, handle: usize) -> usize { + if let Some((_, refs)) = self.objects.get_mut(&handle) { + *refs += 1; + handle + } else { + 0 + } + } + pub fn release_object(&mut self, handle: usize) -> bool { + if let Some((_, refs)) = self.objects.get_mut(&handle) { + if *refs > 1 { + *refs -= 1; + return true; + } + } self.objects.remove(&handle).is_some() } } diff --git a/crates/molotov-core/src/jni/types.rs b/crates/molotov-core/src/jni/types.rs index 3dab55f..d34d3a9 100644 --- a/crates/molotov-core/src/jni/types.rs +++ b/crates/molotov-core/src/jni/types.rs @@ -38,7 +38,7 @@ pub struct JNINativeInterface { pub ToReflectedField: *mut c_void, pub Throw: *mut c_void, pub ThrowNew: *mut c_void, - pub ExceptionOccurred: *mut c_void, + pub ExceptionOccurred: Option *mut c_void>, pub ExceptionDescribe: Option, pub ExceptionClear: Option, pub FatalError: *mut c_void, @@ -52,35 +52,208 @@ pub struct JNINativeInterface { pub EnsureLocalCapacity: *mut c_void, pub AllocObject: *mut c_void, pub NewObject: *mut c_void, + pub NewObjectV: *mut c_void, pub NewObjectA: *mut c_void, - pub GetObjectClass: *mut c_void, + pub GetObjectClass: Option *mut c_void>, pub IsInstanceOf: *mut c_void, pub GetMethodID: Option *mut c_void>, - pub CallObjectMethod: *mut c_void, - pub CallBooleanMethod: *mut c_void, + pub CallObjectMethod: Option *mut c_void>, + pub CallObjectMethodV: *mut c_void, + pub CallObjectMethodA: *mut c_void, + pub CallBooleanMethod: Option u8>, + pub CallBooleanMethodV: *mut c_void, + pub CallBooleanMethodA: *mut c_void, pub CallByteMethod: *mut c_void, + pub CallByteMethodV: *mut c_void, + pub CallByteMethodA: *mut c_void, pub CallCharMethod: *mut c_void, + pub CallCharMethodV: *mut c_void, + pub CallCharMethodA: *mut c_void, pub CallShortMethod: *mut c_void, - pub CallIntMethod: *mut c_void, + pub CallShortMethodV: *mut c_void, + pub CallShortMethodA: *mut c_void, + pub CallIntMethod: Option i32>, + pub CallIntMethodV: *mut c_void, + pub CallIntMethodA: *mut c_void, pub CallLongMethod: *mut c_void, + pub CallLongMethodV: *mut c_void, + pub CallLongMethodA: *mut c_void, pub CallFloatMethod: *mut c_void, + pub CallFloatMethodV: *mut c_void, + pub CallFloatMethodA: *mut c_void, pub CallDoubleMethod: *mut c_void, - pub CallVoidMethod: *mut c_void, + pub CallDoubleMethodV: *mut c_void, + pub CallDoubleMethodA: *mut c_void, + pub CallVoidMethod: Option, + pub CallVoidMethodV: *mut c_void, + pub CallVoidMethodA: *mut c_void, + pub CallNonvirtualObjectMethod: *mut c_void, + pub CallNonvirtualObjectMethodV: *mut c_void, + pub CallNonvirtualObjectMethodA: *mut c_void, + pub CallNonvirtualBooleanMethod: *mut c_void, + pub CallNonvirtualBooleanMethodV: *mut c_void, + pub CallNonvirtualBooleanMethodA: *mut c_void, + pub CallNonvirtualByteMethod: *mut c_void, + pub CallNonvirtualByteMethodV: *mut c_void, + pub CallNonvirtualByteMethodA: *mut c_void, + pub CallNonvirtualCharMethod: *mut c_void, + pub CallNonvirtualCharMethodV: *mut c_void, + pub CallNonvirtualCharMethodA: *mut c_void, + pub CallNonvirtualShortMethod: *mut c_void, + pub CallNonvirtualShortMethodV: *mut c_void, + pub CallNonvirtualShortMethodA: *mut c_void, + pub CallNonvirtualIntMethod: *mut c_void, + pub CallNonvirtualIntMethodV: *mut c_void, + pub CallNonvirtualIntMethodA: *mut c_void, + pub CallNonvirtualLongMethod: *mut c_void, + pub CallNonvirtualLongMethodV: *mut c_void, + pub CallNonvirtualLongMethodA: *mut c_void, + pub CallNonvirtualFloatMethod: *mut c_void, + pub CallNonvirtualFloatMethodV: *mut c_void, + pub CallNonvirtualFloatMethodA: *mut c_void, + pub CallNonvirtualDoubleMethod: *mut c_void, + pub CallNonvirtualDoubleMethodV: *mut c_void, + pub CallNonvirtualDoubleMethodA: *mut c_void, + pub CallNonvirtualVoidMethod: *mut c_void, + pub CallNonvirtualVoidMethodV: *mut c_void, + pub CallNonvirtualVoidMethodA: *mut c_void, + pub GetFieldID: Option *mut c_void>, + pub GetObjectField: *mut c_void, + pub GetBooleanField: *mut c_void, + pub GetByteField: *mut c_void, + pub GetCharField: *mut c_void, + pub GetShortField: *mut c_void, + pub GetIntField: *mut c_void, + pub GetLongField: *mut c_void, + pub GetFloatField: *mut c_void, + pub GetDoubleField: *mut c_void, + pub SetObjectField: *mut c_void, + pub SetBooleanField: *mut c_void, + pub SetByteField: *mut c_void, + pub SetCharField: *mut c_void, + pub SetShortField: *mut c_void, + pub SetIntField: *mut c_void, + pub SetLongField: *mut c_void, + pub SetFloatField: *mut c_void, + pub SetDoubleField: *mut c_void, pub GetStaticMethodID: Option *mut c_void>, - pub CallStaticObjectMethod: *mut c_void, - pub CallStaticBooleanMethod: *mut c_void, + pub CallStaticObjectMethod: Option *mut c_void>, + pub CallStaticObjectMethodV: *mut c_void, + pub CallStaticObjectMethodA: *mut c_void, + pub CallStaticBooleanMethod: Option u8>, + pub CallStaticBooleanMethodV: *mut c_void, + pub CallStaticBooleanMethodA: *mut c_void, pub CallStaticByteMethod: *mut c_void, + pub CallStaticByteMethodV: *mut c_void, + pub CallStaticByteMethodA: *mut c_void, pub CallStaticCharMethod: *mut c_void, + pub CallStaticCharMethodV: *mut c_void, + pub CallStaticCharMethodA: *mut c_void, pub CallStaticShortMethod: *mut c_void, - pub CallStaticIntMethod: *mut c_void, + pub CallStaticShortMethodV: *mut c_void, + pub CallStaticShortMethodA: *mut c_void, + pub CallStaticIntMethod: Option i32>, + pub CallStaticIntMethodV: *mut c_void, + pub CallStaticIntMethodA: *mut c_void, pub CallStaticLongMethod: *mut c_void, + pub CallStaticLongMethodV: *mut c_void, + pub CallStaticLongMethodA: *mut c_void, pub CallStaticFloatMethod: *mut c_void, + pub CallStaticFloatMethodV: *mut c_void, + pub CallStaticFloatMethodA: *mut c_void, pub CallStaticDoubleMethod: *mut c_void, - pub CallStaticVoidMethod: *mut c_void, - pub GetFieldID: *mut c_void, - pub GetStaticFieldID: *mut c_void, + pub CallStaticDoubleMethodV: *mut c_void, + pub CallStaticDoubleMethodA: *mut c_void, + pub CallStaticVoidMethod: Option, + pub CallStaticVoidMethodV: *mut c_void, + pub CallStaticVoidMethodA: *mut c_void, + pub GetStaticFieldID: Option *mut c_void>, + pub GetStaticObjectField: *mut c_void, + pub GetStaticBooleanField: *mut c_void, + pub GetStaticByteField: *mut c_void, + pub GetStaticCharField: *mut c_void, + pub GetStaticShortField: *mut c_void, + pub GetStaticIntField: Option i32>, + pub GetStaticLongField: *mut c_void, + pub GetStaticFloatField: *mut c_void, + pub GetStaticDoubleField: *mut c_void, + pub SetStaticObjectField: *mut c_void, + pub SetStaticBooleanField: *mut c_void, + pub SetStaticByteField: *mut c_void, + pub SetStaticCharField: *mut c_void, + pub SetStaticShortField: *mut c_void, + pub SetStaticIntField: *mut c_void, + pub SetStaticLongField: *mut c_void, + pub SetStaticFloatField: *mut c_void, + pub SetStaticDoubleField: *mut c_void, + pub NewString: *mut c_void, + pub GetStringLength: *mut c_void, + pub GetStringChars: *mut c_void, + pub ReleaseStringChars: *mut c_void, pub NewStringUTF: Option *mut c_void>, pub GetStringUTFLength: Option i32>, pub GetStringUTFChars: Option *const libc::c_char>, pub ReleaseStringUTFChars: Option, + pub GetArrayLength: Option i32>, + pub NewObjectArray: *mut c_void, + pub GetObjectArrayElement: *mut c_void, + pub SetObjectArrayElement: *mut c_void, + pub NewBooleanArray: *mut c_void, + pub NewByteArray: *mut c_void, + pub NewCharArray: *mut c_void, + pub NewShortArray: *mut c_void, + pub NewIntArray: *mut c_void, + pub NewLongArray: *mut c_void, + pub NewFloatArray: *mut c_void, + pub NewDoubleArray: *mut c_void, + pub GetBooleanArrayElements: *mut c_void, + pub GetByteArrayElements: *mut c_void, + pub GetCharArrayElements: *mut c_void, + pub GetShortArrayElements: *mut c_void, + pub GetIntArrayElements: *mut c_void, + pub GetLongArrayElements: *mut c_void, + pub GetFloatArrayElements: *mut c_void, + pub GetDoubleArrayElements: *mut c_void, + pub ReleaseBooleanArrayElements: *mut c_void, + pub ReleaseByteArrayElements: *mut c_void, + pub ReleaseCharArrayElements: *mut c_void, + pub ReleaseShortArrayElements: *mut c_void, + pub ReleaseIntArrayElements: *mut c_void, + pub ReleaseLongArrayElements: *mut c_void, + pub ReleaseFloatArrayElements: *mut c_void, + pub ReleaseDoubleArrayElements: *mut c_void, + pub GetBooleanArrayRegion: *mut c_void, + pub GetByteArrayRegion: *mut c_void, + pub GetCharArrayRegion: *mut c_void, + pub GetShortArrayRegion: *mut c_void, + pub GetIntArrayRegion: *mut c_void, + pub GetLongArrayRegion: *mut c_void, + pub GetFloatArrayRegion: *mut c_void, + pub GetDoubleArrayRegion: *mut c_void, + pub SetBooleanArrayRegion: *mut c_void, + pub SetByteArrayRegion: *mut c_void, + pub SetCharArrayRegion: *mut c_void, + pub SetShortArrayRegion: *mut c_void, + pub SetIntArrayRegion: *mut c_void, + pub SetLongArrayRegion: *mut c_void, + pub SetFloatArrayRegion: *mut c_void, + pub SetDoubleArrayRegion: *mut c_void, + pub RegisterNatives: Option i32>, + pub UnregisterNatives: *mut c_void, + pub MonitorEnter: *mut c_void, + pub MonitorExit: *mut c_void, + pub GetJavaVM: Option i32>, + pub GetStringRegion: *mut c_void, + pub GetStringUTFRegion: *mut c_void, + pub GetPrimitiveArrayCritical: *mut c_void, + pub ReleasePrimitiveArrayCritical: *mut c_void, + pub GetStringCritical: *mut c_void, + pub ReleaseStringCritical: *mut c_void, + pub NewWeakGlobalRef: *mut c_void, + pub DeleteWeakGlobalRef: *mut c_void, + pub ExceptionCheck: Option u8>, + pub NewDirectByteBuffer: *mut c_void, + pub GetDirectBufferAddress: *mut c_void, + pub GetDirectBufferCapacity: *mut c_void, + pub GetObjectRefType: *mut c_void, } diff --git a/crates/molotov-core/src/jni/vm.rs b/crates/molotov-core/src/jni/vm.rs index 778ec5a..53d8111 100644 --- a/crates/molotov-core/src/jni/vm.rs +++ b/crates/molotov-core/src/jni/vm.rs @@ -1,18 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#![allow(dead_code)] -use super::env::JNIEnvWrapper; +#![allow(dead_code, non_snake_case)] +use super::env::GLOBAL_JNIENV; use super::types::{JNIInvokeInterface, JavaVMPtr, JNI_OK, JNI_VERSION_1_6}; use std::ffi::c_void; use std::ptr; -unsafe extern "C" fn jni_get_env(_vm: JavaVMPtr, penv: *mut *mut c_void, _version: i32) -> i32 { +unsafe extern "C" fn jni_get_env(_vm: JavaVMPtr, penv: *mut *mut c_void, version: i32) -> i32 { if penv.is_null() { return super::types::JNI_ERR; } - // Return standard JNIEnv - let env = Box::new(JNIEnvWrapper::new()); - *penv = env.as_raw_ptr() as *mut c_void; - std::mem::forget(env); + if version != JNI_VERSION_1_6 { + tracing::warn!("JNI: requested unsupported version 0x{:x}", version); + } + *penv = GLOBAL_JNIENV.as_raw_ptr() as *mut c_void; JNI_OK } @@ -26,7 +26,14 @@ unsafe extern "C" fn jni_detach_current_thread(_vm: JavaVMPtr) -> i32 { pub struct JavaVMWrapper { table: Box, - table_ptr: *const JNIInvokeInterface, + pinned_ptr: Box<*const JNIInvokeInterface>, +} + +unsafe impl Send for JavaVMWrapper {} +unsafe impl Sync for JavaVMWrapper {} + +lazy_static::lazy_static! { + pub static ref GLOBAL_JAVAVM: JavaVMWrapper = JavaVMWrapper::new(); } impl Default for JavaVMWrapper { @@ -48,12 +55,13 @@ impl JavaVMWrapper { AttachCurrentThreadAsDaemon: Some(jni_attach_current_thread), }); - let table_ptr = &*table as *const JNIInvokeInterface; + let table_raw = &*table as *const JNIInvokeInterface; + let pinned_ptr = Box::new(table_raw); - Self { table, table_ptr } + Self { table, pinned_ptr } } pub fn as_raw_ptr(&self) -> JavaVMPtr { - &self.table_ptr as *const *const JNIInvokeInterface as JavaVMPtr + &*self.pinned_ptr as *const *const JNIInvokeInterface as JavaVMPtr } } diff --git a/crates/molotov-core/tests/jni_abi_fixes_test.rs b/crates/molotov-core/tests/jni_abi_fixes_test.rs new file mode 100644 index 0000000..79e08f8 --- /dev/null +++ b/crates/molotov-core/tests/jni_abi_fixes_test.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +use molotov_core::jni::activity::ActivityRunner; +use molotov_core::jni::types::JNINativeInterface; +use std::ffi::{CStr, CString}; + +#[test] +fn test_jni_native_interface_size_matches_standard_spec() { + // 233 function pointers * 8 bytes = 1864 bytes on 64-bit systems + assert_eq!( + std::mem::size_of::(), + 1864, + "JNINativeInterface must strictly match standard JNI 233-pointer ABI specification" + ); +} + +#[test] +fn test_new_string_utf_and_get_string_utf_chars_via_jni_env() { + let runner = ActivityRunner::new(); + let env = runner.env_ptr(); + + unsafe { + let new_string_utf = (**env).NewStringUTF.expect("NewStringUTF must be implemented"); + let test_c_str = CString::new("roblox_test_string").unwrap(); + let jstr = new_string_utf(env, test_c_str.as_ptr()); + assert!(!jstr.is_null(), "NewStringUTF must return non-null jobject"); + + let get_string_utf = (**env).GetStringUTFChars.expect("GetStringUTFChars must be implemented"); + let mut is_copy = 0u8; + let utf_chars = get_string_utf(env, jstr, &mut is_copy); + assert!(!utf_chars.is_null()); + assert_eq!(CStr::from_ptr(utf_chars).to_str().unwrap(), "roblox_test_string"); + + let release_string_utf = (**env).ReleaseStringUTFChars.expect("ReleaseStringUTFChars must be implemented"); + release_string_utf(env, jstr, utf_chars); + } +} + +#[test] +fn test_find_class_and_device_profile_via_jni_env() { + let runner = ActivityRunner::new(); + let env = runner.env_ptr(); + + unsafe { + let find_class = (**env).FindClass.expect("FindClass must be implemented"); + let class_name = CString::new("android/os/Build").unwrap(); + let clazz = find_class(env, class_name.as_ptr()); + assert!(!clazz.is_null(), "android/os/Build must be recognized by AndroidEnvironment"); + + let bogus_name = CString::new("non/existent/Class").unwrap(); + let bogus = find_class(env, bogus_name.as_ptr()); + assert!(bogus.is_null(), "Unrecognized classes must return NULL"); + } +} diff --git a/crates/molotov-core/tests/jni_lifecycle_test.rs b/crates/molotov-core/tests/jni_lifecycle_test.rs index 3c8140f..0d70321 100644 --- a/crates/molotov-core/tests/jni_lifecycle_test.rs +++ b/crates/molotov-core/tests/jni_lifecycle_test.rs @@ -1,50 +1,93 @@ // SPDX-License-Identifier: GPL-3.0-or-later use molotov_core::jni::activity::{ANativeActivity, ActivityRunner}; use molotov_core::jni::types::{JavaVMPtr, JNI_VERSION_1_6}; +use std::ffi::{c_void, CStr, CString}; + +static mut RESUME_CALLED: bool = false; +static mut WINDOW_CREATED: bool = false; -// Simulated JNI_OnLoad exported by libroblox.so unsafe extern "C" fn mock_jni_onload(vm: JavaVMPtr, _reserved: *mut ()) -> i32 { let mut env = std::ptr::null_mut(); let res = (**vm).GetEnv.unwrap()(vm, &mut env, JNI_VERSION_1_6); - if res == 0 && !env.is_null() { - JNI_VERSION_1_6 - } else { - -1 + if res != 0 || env.is_null() { + return -1; } + let jni_env = env as molotov_core::jni::types::JNIEnvPtr; + + // Verify JNIEnv can find classes + let class_name = CString::new("android/os/Build").unwrap(); + let build_class = (**jni_env).FindClass.unwrap()(jni_env, class_name.as_ptr()); + if build_class.is_null() { + return -2; + } + + // Verify String creation + let test_str = CString::new("RobloxEngine_Init").unwrap(); + let jstr = (**jni_env).NewStringUTF.unwrap()(jni_env, test_str.as_ptr()); + if jstr.is_null() { + return -3; + } + + let utf = (**jni_env).GetStringUTFChars.unwrap()(jni_env, jstr, std::ptr::null_mut()); + if CStr::from_ptr(utf).to_str().unwrap() != "RobloxEngine_Init" { + return -4; + } + (**jni_env).ReleaseStringUTFChars.unwrap()(jni_env, jstr, utf); + + JNI_VERSION_1_6 } -// Simulated ANativeActivity_onCreate exported by libroblox.so unsafe extern "C" fn mock_native_activity_on_create( activity: *mut ANativeActivity, _saved_state: *mut (), _saved_state_size: usize, ) { if let Some(act) = activity.as_mut() { + // Assert internalDataPath is non-null and valid + assert!(!act.internalDataPath.is_null()); + let path = CStr::from_ptr(act.internalDataPath).to_str().unwrap(); + assert!(path.contains("molotov")); + if let Some(cbs) = act.callbacks.as_mut() { cbs.onResume = Some(mock_on_resume); + cbs.onNativeWindowCreated = Some(mock_on_window_created); } } } -static mut RESUME_CALLED: bool = false; unsafe extern "C" fn mock_on_resume(_activity: *mut ANativeActivity) { RESUME_CALLED = true; } +unsafe extern "C" fn mock_on_window_created(_activity: *mut ANativeActivity, window: *mut c_void) { + if !window.is_null() { + WINDOW_CREATED = true; + } +} + #[test] fn test_jni_onload_and_native_activity_lifecycle() { let mut runner = ActivityRunner::new(); - // 1. Simulate JNI_OnLoad execution unsafe { - let ver = mock_jni_onload(runner.activity.vm, std::ptr::null_mut()); + // 1. Simulate JNI_OnLoad execution + let ver = mock_jni_onload(runner.vm_ptr(), std::ptr::null_mut()); assert_eq!(ver, JNI_VERSION_1_6, "JNI_OnLoad must return JNI_VERSION_1_6"); // 2. Simulate ANativeActivity_onCreate mock_native_activity_on_create(&mut *runner.activity, std::ptr::null_mut(), 0); - // 3. Trigger Resume callback registered by the engine + // 3. Trigger Lifecycle + runner.trigger_start(); runner.trigger_resume(); assert!(RESUME_CALLED, "Registered onResume callback must be invoked"); + + let dummy_window = 0x1234_5678 as *mut c_void; + runner.trigger_window_created(dummy_window); + assert!(WINDOW_CREATED, "onNativeWindowCreated callback must be invoked"); + + runner.trigger_pause(); + runner.trigger_stop(); + runner.trigger_destroy(); } }