docs: add Phase 2C JNI VM & NativeActivity implementation plan

This commit is contained in:
katR 2026-09-29 22:06:50 +05:30
parent cf64600a34
commit 0e980a3dc0

View file

@ -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"
```
---