This repository has been archived on 2026-10-10. You can view files and clone it, but cannot push or open issues or pull requests.
molotov/docs/superpowers/plans/2026-09-29-molotov-phase2c-jni-activity.md

29 KiB

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

// 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
// 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)>,
}
// 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
    }
}
// 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
    }
}
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
// 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
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

// 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
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"