fix(jni): implement full 233-pointer JNINativeInterface table, pinned JNI pointers, and working method dispatchers

This commit is contained in:
katR 2026-09-29 22:19:59 +05:30
parent 1c4e21f625
commit b901f42592
8 changed files with 626 additions and 133 deletions

View file

@ -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<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
@ -41,11 +52,11 @@ pub struct ANativeActivity {
}
pub struct ActivityRunner {
pub vm: JavaVMWrapper,
pub env: JNIEnvWrapper,
pub callbacks: Box<ANativeActivityCallbacks>,
callbacks: Box<ANativeActivityCallbacks>,
pub activity: Box<ANativeActivity>,
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); }
}
}
}

View file

@ -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<JNINativeInterface>,
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::<JNINativeInterface>() });
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
}
}

View file

@ -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<AndroidEnvironment> = RwLock::new(AndroidEnvironment::new());
}
impl Default for AndroidEnvironment {
fn default() -> Self {
Self::new()

View file

@ -11,7 +11,7 @@ pub enum JniObject {
}
pub struct JniObjectRegistry {
objects: HashMap<usize, JniObject>,
objects: HashMap<usize, (JniObject, u32)>, // (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()
}
}

View file

@ -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<unsafe extern "C" fn(env: JNIEnvPtr) -> *mut c_void>,
pub ExceptionDescribe: Option<unsafe extern "C" fn(env: JNIEnvPtr)>,
pub ExceptionClear: Option<unsafe extern "C" fn(env: JNIEnvPtr)>,
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<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void) -> *mut c_void>,
pub IsInstanceOf: *mut c_void,
pub GetMethodID: Option<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, name: *const libc::c_char, sig: *const libc::c_char) -> *mut c_void>,
pub CallObjectMethod: *mut c_void,
pub CallBooleanMethod: *mut c_void,
pub CallObjectMethod: Option<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void, methodID: *mut c_void) -> *mut c_void>,
pub CallObjectMethodV: *mut c_void,
pub CallObjectMethodA: *mut c_void,
pub CallBooleanMethod: Option<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void, methodID: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void, methodID: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void, methodID: *mut c_void)>,
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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, name: *const libc::c_char, sig: *const libc::c_char) -> *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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, name: *const libc::c_char, sig: *const libc::c_char) -> *mut c_void>,
pub CallStaticObjectMethod: *mut c_void,
pub CallStaticBooleanMethod: *mut c_void,
pub CallStaticObjectMethod: Option<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, methodID: *mut c_void) -> *mut c_void>,
pub CallStaticObjectMethodV: *mut c_void,
pub CallStaticObjectMethodA: *mut c_void,
pub CallStaticBooleanMethod: Option<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, methodID: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, methodID: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, methodID: *mut c_void)>,
pub CallStaticVoidMethodV: *mut c_void,
pub CallStaticVoidMethodA: *mut c_void,
pub GetStaticFieldID: Option<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, name: *const libc::c_char, sig: *const libc::c_char) -> *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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, fieldID: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, bytes: *const libc::c_char) -> *mut c_void>,
pub GetStringUTFLength: Option<unsafe extern "C" fn(env: JNIEnvPtr, string: *mut c_void) -> i32>,
pub GetStringUTFChars: Option<unsafe extern "C" fn(env: JNIEnvPtr, string: *mut c_void, isCopy: *mut u8) -> *const libc::c_char>,
pub ReleaseStringUTFChars: Option<unsafe extern "C" fn(env: JNIEnvPtr, string: *mut c_void, utf: *const libc::c_char)>,
pub GetArrayLength: Option<unsafe extern "C" fn(env: JNIEnvPtr, array: *mut c_void) -> 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<unsafe extern "C" fn(env: JNIEnvPtr, clazz: *mut c_void, methods: *const c_void, nMethods: i32) -> i32>,
pub UnregisterNatives: *mut c_void,
pub MonitorEnter: *mut c_void,
pub MonitorExit: *mut c_void,
pub GetJavaVM: Option<unsafe extern "C" fn(env: JNIEnvPtr, vm: *mut JavaVMPtr) -> 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<unsafe extern "C" fn(env: JNIEnvPtr) -> u8>,
pub NewDirectByteBuffer: *mut c_void,
pub GetDirectBufferAddress: *mut c_void,
pub GetDirectBufferCapacity: *mut c_void,
pub GetObjectRefType: *mut c_void,
}

View file

@ -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<JNIInvokeInterface>,
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
}
}

View file

@ -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::<JNINativeInterface>(),
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");
}
}

View file

@ -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();
}
}