docs: add Phase 2C JNI VM & NativeActivity implementation plan
This commit is contained in:
parent
cf64600a34
commit
0e980a3dc0
1 changed files with 861 additions and 0 deletions
|
|
@ -0,0 +1,861 @@
|
|||
# Molotov Phase 2C: JNI VM Environment & Android NativeActivity Lifecycle Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement the synthetic Android JNI VM environment (`JavaVM`, `JNIEnv`) and `ANativeActivity` lifecycle in `molotov-core`, providing the Android Java framework environment expected by `libroblox.so` to execute `JNI_OnLoad` and register engine lifecycle and window callbacks.
|
||||
|
||||
**Architecture:** A native JNI implementation providing synthetic C function pointer dispatch tables for `JavaVM` and `JNIEnv`. Emulates required Android framework classes (`android/content/Context`, `android/os/Build`, `com/roblox/client/*`), constructs the `ANativeActivity` structure, and drives the native activity lifecycle.
|
||||
|
||||
**Tech Stack:** Rust 1.75+, `libc`, `tracing`, `thiserror`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-09-29-molotov-architecture-design.md` (Section 6.3)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Language & Edition**: Rust 1.75+ (2021 edition), strictly zero Wine or Proton.
|
||||
- **License**: GNU General Public License v3.0 or later (GPL-3.0-or-later) with headers in all source files.
|
||||
- **No Faking Success**: All unknown Java methods or unmapped classes return explicit JNI exceptions or `NULL` with diagnostic logging, never plausible fake handles.
|
||||
- **JNI Version**: Supports `JNI_VERSION_1_6` (0x00010006) matching Android NDK expectations.
|
||||
- **Device Profile**: Emulates a 64-bit Android 13/14 device (`ro.build.version.sdk = 33`, `ro.product.cpu.abi = x86_64`).
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **JNI Method and Field Signature Mismatch**: Incompatible argument counts or types passed to `Call*Method` must not corrupt registers or the stack.
|
||||
2. **String Memory Lifetime**: `GetStringUTFChars` must return valid null-terminated UTF-8 strings that remain valid until `ReleaseStringUTFChars`.
|
||||
3. **Double Activity Lifecycle Triggers**: Calling `onResume` when already resumed or `onDestroy` multiple times must be gracefully handled without panics.
|
||||
4. **Local vs Global Reference Leaks**: JNI object allocation must track references so `DeleteLocalRef` and `DeleteGlobalRef` clean up allocated wrapper memory.
|
||||
5. **Null Pointer Handling in JNI Calls**: Passing null `jobject`, `jclass`, or `jstring` must return `JNI_ERR` or null without dereference panics.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: JNI C-ABI Types & Synthetic Dispatch Tables
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/jni/types.rs`
|
||||
- Create: `crates/molotov-core/src/jni/vm.rs`
|
||||
- Create: `crates/molotov-core/src/jni/env.rs`
|
||||
- Create: `crates/molotov-core/src/jni/mod.rs`
|
||||
- Test: `crates/molotov-core/tests/jni_tables_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: None
|
||||
- Produces:
|
||||
- `molotov_core::jni::vm::JavaVMWrapper`
|
||||
- `molotov_core::jni::env::JNIEnvWrapper`
|
||||
- `molotov_core::jni::types::{JNI_OK, JNI_VERSION_1_6}`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for synthetic JavaVM and JNIEnv creation**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/jni_tables_test.rs
|
||||
use molotov_core::jni::env::JNIEnvWrapper;
|
||||
use molotov_core::jni::types::JNI_VERSION_1_6;
|
||||
use molotov_core::jni::vm::JavaVMWrapper;
|
||||
|
||||
#[test]
|
||||
fn test_synthetic_javavm_and_jnienv_version() {
|
||||
let vm = JavaVMWrapper::new();
|
||||
let env = JNIEnvWrapper::new();
|
||||
|
||||
unsafe {
|
||||
let env_ptr = env.as_raw_ptr();
|
||||
let get_version = (**env_ptr).GetVersion;
|
||||
assert!(get_version.is_some());
|
||||
let ver = get_version.unwrap()(env_ptr);
|
||||
assert_eq!(ver, JNI_VERSION_1_6);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_tables_test`
|
||||
Expected: FAIL with unexported module `jni`.
|
||||
|
||||
- [ ] **Step 3: Implement JNI types, JavaVM, and JNIEnv dispatch tables**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/types.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::ffi::c_void;
|
||||
|
||||
pub const JNI_OK: i32 = 0;
|
||||
pub const JNI_ERR: i32 = -1;
|
||||
pub const JNI_VERSION_1_6: i32 = 0x00010006;
|
||||
|
||||
pub type JNIEnvPtr = *mut *const JNINativeInterface;
|
||||
pub type JavaVMPtr = *mut *const JNIInvokeInterface;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct JNIInvokeInterface {
|
||||
pub reserved0: *mut c_void,
|
||||
pub reserved1: *mut c_void,
|
||||
pub reserved2: *mut c_void,
|
||||
pub DestroyJavaVM: Option<unsafe extern "C" fn(vm: JavaVMPtr) -> i32>,
|
||||
pub AttachCurrentThread: Option<unsafe extern "C" fn(vm: JavaVMPtr, penv: *mut *mut c_void, args: *mut c_void) -> i32>,
|
||||
pub DetachCurrentThread: Option<unsafe extern "C" fn(vm: JavaVMPtr) -> i32>,
|
||||
pub GetEnv: Option<unsafe extern "C" fn(vm: JavaVMPtr, penv: *mut *mut c_void, version: i32) -> i32>,
|
||||
pub AttachCurrentThreadAsDaemon: Option<unsafe extern "C" fn(vm: JavaVMPtr, penv: *mut *mut c_void, args: *mut c_void) -> i32>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct JNINativeInterface {
|
||||
pub reserved0: *mut c_void,
|
||||
pub reserved1: *mut c_void,
|
||||
pub reserved2: *mut c_void,
|
||||
pub reserved3: *mut c_void,
|
||||
pub GetVersion: Option<unsafe extern "C" fn(env: JNIEnvPtr) -> i32>,
|
||||
pub DefineClass: *mut c_void,
|
||||
pub FindClass: Option<unsafe extern "C" fn(env: JNIEnvPtr, name: *const libc::c_char) -> *mut c_void>,
|
||||
pub FromReflectedMethod: *mut c_void,
|
||||
pub FromReflectedField: *mut c_void,
|
||||
pub ToReflectedMethod: *mut c_void,
|
||||
pub GetSuperclass: *mut c_void,
|
||||
pub IsAssignableFrom: *mut c_void,
|
||||
pub ToReflectedField: *mut c_void,
|
||||
pub Throw: *mut c_void,
|
||||
pub ThrowNew: *mut c_void,
|
||||
pub ExceptionOccurred: *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,
|
||||
pub PushLocalFrame: *mut c_void,
|
||||
pub PopLocalFrame: *mut c_void,
|
||||
pub NewGlobalRef: Option<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void) -> *mut c_void>,
|
||||
pub DeleteGlobalRef: Option<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void)>,
|
||||
pub DeleteLocalRef: Option<unsafe extern "C" fn(env: JNIEnvPtr, obj: *mut c_void)>,
|
||||
pub IsSameObject: *mut c_void,
|
||||
pub NewLocalRef: *mut c_void,
|
||||
pub EnsureLocalCapacity: *mut c_void,
|
||||
pub AllocObject: *mut c_void,
|
||||
pub NewObject: *mut c_void,
|
||||
pub NewObjectA: *mut c_void,
|
||||
pub GetObjectClass: *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 CallByteMethod: *mut c_void,
|
||||
pub CallCharMethod: *mut c_void,
|
||||
pub CallShortMethod: *mut c_void,
|
||||
pub CallIntMethod: *mut c_void,
|
||||
pub CallLongMethod: *mut c_void,
|
||||
pub CallFloatMethod: *mut c_void,
|
||||
pub CallDoubleMethod: *mut c_void,
|
||||
pub CallVoidMethod: *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 CallStaticByteMethod: *mut c_void,
|
||||
pub CallStaticCharMethod: *mut c_void,
|
||||
pub CallStaticShortMethod: *mut c_void,
|
||||
pub CallStaticIntMethod: *mut c_void,
|
||||
pub CallStaticLongMethod: *mut c_void,
|
||||
pub CallStaticFloatMethod: *mut c_void,
|
||||
pub CallStaticDoubleMethod: *mut c_void,
|
||||
pub CallStaticVoidMethod: *mut c_void,
|
||||
pub GetFieldID: *mut c_void,
|
||||
pub GetStaticFieldID: *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)>,
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/env.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use super::types::{JNIEnvPtr, JNINativeInterface, JNI_VERSION_1_6};
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
|
||||
unsafe extern "C" fn jni_get_version(_env: JNIEnvPtr) -> i32 {
|
||||
JNI_VERSION_1_6
|
||||
}
|
||||
|
||||
unsafe extern "C" fn jni_exception_describe(_env: JNIEnvPtr) {
|
||||
tracing::debug!("JNI: ExceptionDescribe called");
|
||||
}
|
||||
|
||||
unsafe extern "C" fn jni_exception_clear(_env: JNIEnvPtr) {
|
||||
tracing::debug!("JNI: ExceptionClear called");
|
||||
}
|
||||
|
||||
pub struct JNIEnvWrapper {
|
||||
table: Box<JNINativeInterface>,
|
||||
table_ptr: *const JNINativeInterface,
|
||||
}
|
||||
|
||||
impl Default for JNIEnvWrapper {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
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 table_ptr = &*table as *const JNINativeInterface;
|
||||
|
||||
Self { table, table_ptr }
|
||||
}
|
||||
|
||||
pub fn as_raw_ptr(&self) -> JNIEnvPtr {
|
||||
&self.table_ptr as *const *const JNINativeInterface as JNIEnvPtr
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/vm.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use super::env::JNIEnvWrapper;
|
||||
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 {
|
||||
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);
|
||||
JNI_OK
|
||||
}
|
||||
|
||||
unsafe extern "C" fn jni_attach_current_thread(_vm: JavaVMPtr, penv: *mut *mut c_void, _args: *mut c_void) -> i32 {
|
||||
jni_get_env(_vm, penv, JNI_VERSION_1_6)
|
||||
}
|
||||
|
||||
unsafe extern "C" fn jni_detach_current_thread(_vm: JavaVMPtr) -> i32 {
|
||||
JNI_OK
|
||||
}
|
||||
|
||||
pub struct JavaVMWrapper {
|
||||
table: Box<JNIInvokeInterface>,
|
||||
table_ptr: *const JNIInvokeInterface,
|
||||
}
|
||||
|
||||
impl Default for JavaVMWrapper {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl JavaVMWrapper {
|
||||
pub fn new() -> Self {
|
||||
let table = Box::new(JNIInvokeInterface {
|
||||
reserved0: ptr::null_mut(),
|
||||
reserved1: ptr::null_mut(),
|
||||
reserved2: ptr::null_mut(),
|
||||
DestroyJavaVM: None,
|
||||
AttachCurrentThread: Some(jni_attach_current_thread),
|
||||
DetachCurrentThread: Some(jni_detach_current_thread),
|
||||
GetEnv: Some(jni_get_env),
|
||||
AttachCurrentThreadAsDaemon: Some(jni_attach_current_thread),
|
||||
});
|
||||
|
||||
let table_ptr = &*table as *const JNIInvokeInterface;
|
||||
|
||||
Self { table, table_ptr }
|
||||
}
|
||||
|
||||
pub fn as_raw_ptr(&self) -> JavaVMPtr {
|
||||
&self.table_ptr as *const *const JNIInvokeInterface as JavaVMPtr
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/mod.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pub mod env;
|
||||
pub mod types;
|
||||
pub mod vm;
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/lib.rs` to expose `pub mod jni;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_tables_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(jni): implement synthetic JavaVM and JNIEnv dispatch tables"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Emulated Android Java Object Registry & String Table
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/jni/objects.rs`
|
||||
- Modify: `crates/molotov-core/src/jni/mod.rs`
|
||||
- Test: `crates/molotov-core/tests/jni_objects_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::jni::types::*`
|
||||
- Produces:
|
||||
- `molotov_core::jni::objects::JniObjectRegistry`
|
||||
- `molotov_core::jni::objects::JniObject`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for JNI UTF string management and object handles**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/jni_objects_test.rs
|
||||
use molotov_core::jni::objects::JniObjectRegistry;
|
||||
|
||||
#[test]
|
||||
fn test_jni_string_allocation_and_read() {
|
||||
let mut registry = JniObjectRegistry::new();
|
||||
let str_handle = registry.alloc_string("hello_roblox");
|
||||
|
||||
assert_eq!(registry.get_string(str_handle), Some("hello_roblox"));
|
||||
registry.release_object(str_handle);
|
||||
assert_eq!(registry.get_string(str_handle), None);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_objects_test`
|
||||
Expected: FAIL with unexported module `objects`.
|
||||
|
||||
- [ ] **Step 3: Implement JniObjectRegistry**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/objects.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum JniObject {
|
||||
Class(String),
|
||||
String(String, CString),
|
||||
Instance { class_name: String },
|
||||
}
|
||||
|
||||
pub struct JniObjectRegistry {
|
||||
objects: HashMap<usize, JniObject>,
|
||||
next_handle: usize,
|
||||
}
|
||||
|
||||
impl Default for JniObjectRegistry {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl JniObjectRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
objects: HashMap::new(),
|
||||
next_handle: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn alloc_string(&mut self, text: &str) -> usize {
|
||||
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));
|
||||
handle
|
||||
}
|
||||
|
||||
pub fn get_string(&self, handle: usize) -> Option<&str> {
|
||||
match self.objects.get(&handle) {
|
||||
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()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn release_object(&mut self, handle: usize) -> bool {
|
||||
self.objects.remove(&handle).is_some()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/jni/mod.rs` to expose `pub mod objects;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_objects_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(jni): implement JNI object registry and string memory management"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Android Framework Emulation (Context, Build & Application)
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/jni/framework.rs`
|
||||
- Modify: `crates/molotov-core/src/jni/mod.rs`
|
||||
- Test: `crates/molotov-core/tests/jni_framework_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::jni::objects::JniObjectRegistry`
|
||||
- Produces:
|
||||
- `molotov_core::jni::framework::AndroidEnvironment`
|
||||
- `molotov_core::jni::framework::AndroidEnvironment::resolve_class()`
|
||||
- `molotov_core::jni::framework::AndroidEnvironment::call_method()`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for Android Context package name and Build properties**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/jni_framework_test.rs
|
||||
use molotov_core::jni::framework::AndroidEnvironment;
|
||||
|
||||
#[test]
|
||||
fn test_android_environment_package_name_and_build() {
|
||||
let env = AndroidEnvironment::new();
|
||||
assert_eq!(env.package_name(), "com.roblox.client");
|
||||
assert_eq!(env.build_model(), "Pixel Tablet (x86_64)");
|
||||
assert_eq!(env.build_sdk_version(), 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_known_android_classes() {
|
||||
let mut env = AndroidEnvironment::new();
|
||||
assert!(env.find_class("android/content/Context").is_some());
|
||||
assert!(env.find_class("android/os/Build").is_some());
|
||||
assert!(env.find_class("com/roblox/client/RobloxApplication").is_some());
|
||||
assert!(env.find_class("com/unknown/BogusClass").is_none());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_framework_test`
|
||||
Expected: FAIL with unexported module `framework`.
|
||||
|
||||
- [ ] **Step 3: Implement AndroidEnvironment**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/framework.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use super::objects::JniObjectRegistry;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct AndroidEnvironment {
|
||||
pub registry: JniObjectRegistry,
|
||||
known_classes: HashSet<String>,
|
||||
files_dir: PathBuf,
|
||||
cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for AndroidEnvironment {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AndroidEnvironment {
|
||||
pub fn new() -> Self {
|
||||
let mut known = HashSet::new();
|
||||
known.insert("android/content/Context".to_string());
|
||||
known.insert("android/app/Activity".to_string());
|
||||
known.insert("android/os/Build".to_string());
|
||||
known.insert("android/os/Build$VERSION".to_string());
|
||||
known.insert("android/view/SurfaceView".to_string());
|
||||
known.insert("com/roblox/client/RobloxApplication".to_string());
|
||||
known.insert("com/roblox/client/ActivityNativeMain".to_string());
|
||||
|
||||
Self {
|
||||
registry: JniObjectRegistry::new(),
|
||||
known_classes: known,
|
||||
files_dir: PathBuf::from("/tmp/molotov_files"),
|
||||
cache_dir: PathBuf::from("/tmp/molotov_cache"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn package_name(&self) -> &'static str {
|
||||
"com.roblox.client"
|
||||
}
|
||||
|
||||
pub fn build_model(&self) -> &'static str {
|
||||
"Pixel Tablet (x86_64)"
|
||||
}
|
||||
|
||||
pub fn build_sdk_version(&self) -> i32 {
|
||||
33 // Android 13
|
||||
}
|
||||
|
||||
pub fn find_class(&mut self, class_name: &str) -> Option<usize> {
|
||||
if self.known_classes.contains(class_name) {
|
||||
Some(self.registry.alloc_class(class_name))
|
||||
} else {
|
||||
tracing::warn!("JNI: FindClass requested unknown class '{}' -> returning NULL", class_name);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/jni/mod.rs` to expose `pub mod framework;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_framework_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(jni): implement Android framework class emulation and device properties"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Android NativeActivity C-ABI & Lifecycle Dispatcher
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/src/jni/activity.rs`
|
||||
- Modify: `crates/molotov-core/src/jni/mod.rs`
|
||||
- Test: `crates/molotov-core/tests/jni_activity_test.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `molotov_core::jni::vm::JavaVMWrapper`, `molotov_core::jni::env::JNIEnvWrapper`
|
||||
- Produces:
|
||||
- `molotov_core::jni::activity::ANativeActivity`
|
||||
- `molotov_core::jni::activity::ANativeActivityCallbacks`
|
||||
- `molotov_core::jni::activity::ActivityRunner`
|
||||
|
||||
- [ ] **Step 1: Write the failing test for NativeActivity structure and lifecycle registration**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/jni_activity_test.rs
|
||||
use molotov_core::jni::activity::ActivityRunner;
|
||||
|
||||
#[test]
|
||||
fn test_activity_runner_lifecycle_callbacks() {
|
||||
let mut runner = ActivityRunner::new();
|
||||
assert_eq!(runner.is_resumed(), false);
|
||||
|
||||
runner.trigger_resume();
|
||||
assert_eq!(runner.is_resumed(), true);
|
||||
|
||||
runner.trigger_pause();
|
||||
assert_eq!(runner.is_resumed(), false);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_activity_test`
|
||||
Expected: FAIL with unexported module `activity`.
|
||||
|
||||
- [ ] **Step 3: Implement ANativeActivity and ActivityRunner**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/src/jni/activity.rs
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use super::env::JNIEnvWrapper;
|
||||
use super::types::{JNIEnvPtr, JavaVMPtr};
|
||||
use super::vm::JavaVMWrapper;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ANativeActivityCallbacks {
|
||||
pub onStart: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onResume: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onSaveInstanceState: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, outSize: *mut usize) -> *mut c_void>,
|
||||
pub onPause: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onStop: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onDestroy: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onWindowFocusChanged: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, hasFocus: i32)>,
|
||||
pub onNativeWindowCreated: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, window: *mut c_void)>,
|
||||
pub onNativeWindowResized: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, window: *mut c_void)>,
|
||||
pub onNativeWindowRedrawNeeded: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, window: *mut c_void)>,
|
||||
pub onNativeWindowDestroyed: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, window: *mut c_void)>,
|
||||
pub onInputQueueCreated: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, queue: *mut c_void)>,
|
||||
pub onInputQueueDestroyed: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, queue: *mut c_void)>,
|
||||
pub onContentRectChanged: Option<unsafe extern "C" fn(activity: *mut ANativeActivity, rect: *const c_void)>,
|
||||
pub onConfigurationChanged: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
pub onLowMemory: Option<unsafe extern "C" fn(activity: *mut ANativeActivity)>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ANativeActivity {
|
||||
pub callbacks: *mut ANativeActivityCallbacks,
|
||||
pub vm: JavaVMPtr,
|
||||
pub env: JNIEnvPtr,
|
||||
pub clazz: *mut c_void,
|
||||
pub internalDataPath: *const libc::c_char,
|
||||
pub externalDataPath: *const libc::c_char,
|
||||
pub sdkVersion: i32,
|
||||
pub instance: *mut c_void,
|
||||
pub assetManager: *mut c_void,
|
||||
pub obbPath: *const libc::c_char,
|
||||
}
|
||||
|
||||
pub struct ActivityRunner {
|
||||
vm: JavaVMWrapper,
|
||||
env: JNIEnvWrapper,
|
||||
callbacks: Box<ANativeActivityCallbacks>,
|
||||
activity: Box<ANativeActivity>,
|
||||
resumed: bool,
|
||||
}
|
||||
|
||||
impl Default for ActivityRunner {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActivityRunner {
|
||||
pub fn new() -> Self {
|
||||
let vm = JavaVMWrapper::new();
|
||||
let env = JNIEnvWrapper::new();
|
||||
|
||||
let mut callbacks = Box::new(ANativeActivityCallbacks {
|
||||
onStart: None,
|
||||
onResume: None,
|
||||
onSaveInstanceState: None,
|
||||
onPause: None,
|
||||
onStop: None,
|
||||
onDestroy: None,
|
||||
onWindowFocusChanged: None,
|
||||
onNativeWindowCreated: None,
|
||||
onNativeWindowResized: None,
|
||||
onNativeWindowRedrawNeeded: None,
|
||||
onNativeWindowDestroyed: None,
|
||||
onInputQueueCreated: None,
|
||||
onInputQueueDestroyed: None,
|
||||
onContentRectChanged: None,
|
||||
onConfigurationChanged: None,
|
||||
onLowMemory: None,
|
||||
});
|
||||
|
||||
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(),
|
||||
sdkVersion: 33,
|
||||
instance: ptr::null_mut(),
|
||||
assetManager: ptr::null_mut(),
|
||||
obbPath: ptr::null(),
|
||||
});
|
||||
|
||||
Self {
|
||||
vm,
|
||||
env,
|
||||
callbacks,
|
||||
activity,
|
||||
resumed: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_resumed(&self) -> bool {
|
||||
self.resumed
|
||||
}
|
||||
|
||||
pub fn trigger_resume(&mut self) {
|
||||
self.resumed = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update `crates/molotov-core/src/jni/mod.rs` to expose `pub mod activity;`.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_activity_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/
|
||||
git commit -m "feat(jni): implement ANativeActivity struct and lifecycle callback runner"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: End-to-End JNI_OnLoad and ANativeActivity Integration Test
|
||||
|
||||
**Files:**
|
||||
- Create: `crates/molotov-core/tests/jni_lifecycle_test.rs`
|
||||
- Modify: `crates/molotov-core/src/jni/mod.rs`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: All `molotov-core::jni::*` modules
|
||||
- Produces: Verified JNI bootstrapping and activity lifecycle testing
|
||||
|
||||
- [ ] **Step 1: Write integration test simulating JNI_OnLoad and ANativeActivity_onCreate**
|
||||
|
||||
```rust
|
||||
// crates/molotov-core/tests/jni_lifecycle_test.rs
|
||||
use molotov_core::jni::activity::{ANativeActivity, ActivityRunner};
|
||||
use molotov_core::jni::types::{JavaVMPtr, JNI_VERSION_1_6};
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if let Some(cbs) = act.callbacks.as_mut() {
|
||||
cbs.onResume = Some(mock_on_resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static mut RESUME_CALLED: bool = false;
|
||||
unsafe extern "C" fn mock_on_resume(_activity: *mut ANativeActivity) {
|
||||
RESUME_CALLED = 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());
|
||||
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
|
||||
runner.trigger_resume();
|
||||
assert!(RESUME_CALLED, "Registered onResume callback must be invoked");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it passes**
|
||||
|
||||
Run: `cargo test -p molotov-core --test jni_lifecycle_test`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/molotov-core/tests/jni_lifecycle_test.rs
|
||||
git commit -m "test(jni): implement JNI_OnLoad and ANativeActivity end-to-end integration test"
|
||||
```
|
||||
|
||||
---
|
||||
Reference in a new issue