From 0e980a3dc0929e8103864ff402d51ca35c0e033d Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Tue, 29 Sep 2026 22:06:50 +0530 Subject: [PATCH] docs: add Phase 2C JNI VM & NativeActivity implementation plan --- ...2026-09-29-molotov-phase2c-jni-activity.md | 861 ++++++++++++++++++ 1 file changed, 861 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-29-molotov-phase2c-jni-activity.md diff --git a/docs/superpowers/plans/2026-09-29-molotov-phase2c-jni-activity.md b/docs/superpowers/plans/2026-09-29-molotov-phase2c-jni-activity.md new file mode 100644 index 0000000..1151d39 --- /dev/null +++ b/docs/superpowers/plans/2026-09-29-molotov-phase2c-jni-activity.md @@ -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 i32>, + pub AttachCurrentThread: Option i32>, + pub DetachCurrentThread: Option i32>, + pub GetEnv: Option i32>, + pub AttachCurrentThreadAsDaemon: Option 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 i32>, + pub DefineClass: *mut c_void, + pub FindClass: Option *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, + pub ExceptionClear: Option, + pub FatalError: *mut c_void, + pub PushLocalFrame: *mut c_void, + pub PopLocalFrame: *mut c_void, + pub NewGlobalRef: Option *mut c_void>, + pub DeleteGlobalRef: Option, + pub DeleteLocalRef: Option, + 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 *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 *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 *mut c_void>, + pub GetStringUTFLength: Option i32>, + pub GetStringUTFChars: Option *const libc::c_char>, + pub ReleaseStringUTFChars: Option, +} +``` + +```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, + 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, + 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, + 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, + 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 { + 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, + pub onResume: Option, + pub onSaveInstanceState: Option *mut c_void>, + pub onPause: Option, + pub onStop: Option, + pub onDestroy: Option, + pub onWindowFocusChanged: Option, + pub onNativeWindowCreated: Option, + pub onNativeWindowResized: Option, + pub onNativeWindowRedrawNeeded: Option, + pub onNativeWindowDestroyed: Option, + pub onInputQueueCreated: Option, + pub onInputQueueDestroyed: Option, + pub onContentRectChanged: Option, + pub onConfigurationChanged: Option, + pub onLowMemory: Option, +} + +#[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, + activity: Box, + 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" +``` + +---