fangorn/huorn-minecraft
public
ref:main
//! huorn-minecraft - JNI library embedding Alacritty terminal for Minecraft
//!
//! Uses JNI_OnLoad to register native methods explicitly, which is required
//! because Fabric's Knot classloader prevents standard JNI name-based lookup.
#![allow(dead_code)]
pub mod audit;
pub mod backend;
mod glyph_cache;
mod renderer;
pub mod security;
pub mod terminal;
use std::sync::OnceLock;
use jni::objects::{JByteBuffer, JClass, JString};
use jni::sys::{
jboolean, jfloat, jint, jintArray, jlong, JNI_FALSE, JNI_TRUE, JNI_VERSION_1_8,
};
use jni::{JNIEnv, JavaVM, NativeMethod};
use terminal::TerminalState;
static AUDIT_LOGGER: OnceLock<audit::AuditLogger> = OnceLock::new();
fn to_handle(state: Box<TerminalState>) -> jlong {
Box::into_raw(state) as jlong
}
unsafe fn from_handle(handle: jlong) -> &'static mut TerminalState {
&mut *(handle as *mut TerminalState)
}
// --- Native method implementations ---
extern "system" fn native_create(
mut env: JNIEnv,
_class: JClass,
cols: jint,
rows: jint,
font_size: jfloat,
shell: JString,
working_dir: JString,
backend: JString,
) -> jlong {
let shell_str: String = match env.get_string(&shell) {
Ok(s) => s.into(),
Err(_) => String::new(),
};
let working_dir_str: String = match env.get_string(&working_dir) {
Ok(s) => s.into(),
Err(_) => String::new(),
};
let backend_str: String = match env.get_string(&backend) {
Ok(s) => s.into(),
Err(_) => String::new(),
};
match TerminalState::new(cols, rows, font_size, &shell_str, &working_dir_str, &backend_str) {
Ok(state) => to_handle(Box::new(state)),
Err(e) => {
let _ = env.throw_new("java/lang/RuntimeException", e);
0
}
}
}
extern "system" fn native_destroy(_env: JNIEnv, _class: JClass, handle: jlong) {
if handle != 0 {
unsafe {
let _ = Box::from_raw(handle as *mut TerminalState);
}
}
}
extern "system" fn native_send_text(mut env: JNIEnv, _class: JClass, handle: jlong, text: JString) {
if handle == 0 { return; }
let text_str: String = match env.get_string(&text) {
Ok(s) => s.into(),
Err(_) => return,
};
let state = unsafe { from_handle(handle) };
state.send_text(&text_str);
}
extern "system" fn native_send_key(_env: JNIEnv, _class: JClass, handle: jlong, keycode: jint, modifiers: jint) {
if handle == 0 { return; }
let state = unsafe { from_handle(handle) };
state.send_key(keycode, modifiers);
}
extern "system" fn native_get_pixel_data(env: JNIEnv, _class: JClass, handle: jlong, buffer: JByteBuffer) -> jboolean {
if handle == 0 { return JNI_FALSE; }
let state = unsafe { from_handle(handle) };
let dirty = state.render();
if dirty {
let pixels = state.pixel_buffer();
let buf_ptr = match env.get_direct_buffer_address(&buffer) {
Ok(ptr) => ptr,
Err(_) => return JNI_FALSE,
};
let buf_capacity = match env.get_direct_buffer_capacity(&buffer) {
Ok(cap) => cap,
Err(_) => return JNI_FALSE,
};
let len = buf_capacity.min(pixels.len());
unsafe { std::ptr::copy_nonoverlapping(pixels.as_ptr(), buf_ptr, len); }
}
if dirty { JNI_TRUE } else { JNI_FALSE }
}
extern "system" fn native_get_dimensions(env: JNIEnv, _class: JClass, handle: jlong) -> jintArray {
let dims = if handle != 0 {
let state = unsafe { from_handle(handle) };
state.dimensions()
} else {
[0, 0, 0, 0]
};
match env.new_int_array(4) {
Ok(arr) => {
let _ = env.set_int_array_region(&arr, 0, &dims);
arr.into_raw()
}
Err(_) => std::ptr::null_mut(),
}
}
extern "system" fn native_resize(_env: JNIEnv, _class: JClass, handle: jlong, cols: jint, rows: jint) {
if handle == 0 { return; }
let state = unsafe { from_handle(handle) };
state.resize(cols, rows);
}
extern "system" fn native_poll_pty(_env: JNIEnv, _class: JClass, handle: jlong) -> jboolean {
if handle == 0 { return JNI_FALSE; }
let state = unsafe { from_handle(handle) };
if state.poll_pty() { JNI_TRUE } else { JNI_FALSE }
}
extern "system" fn native_is_alive(_env: JNIEnv, _class: JClass, handle: jlong) -> jboolean {
if handle == 0 { return JNI_FALSE; }
let state = unsafe { from_handle(handle) };
if state.is_alive() { JNI_TRUE } else { JNI_FALSE }
}
extern "system" fn native_scroll(_env: JNIEnv, _class: JClass, handle: jlong, delta: jint) {
if handle == 0 { return; }
let state = unsafe { from_handle(handle) };
state.scroll(delta);
}
// --- Audit JNI functions ---
extern "system" fn native_init_audit(mut env: JNIEnv, _class: JClass, log_path: JString) {
let path: String = match env.get_string(&log_path) {
Ok(s) => s.into(),
Err(_) => return,
};
match audit::AuditLogger::new(&path) {
Ok(logger) => {
let _ = AUDIT_LOGGER.set(logger);
log::info!("[Huorn] Audit logging initialized: {}", path);
}
Err(e) => {
log::error!("[Huorn] Failed to init audit log: {}", e);
}
}
}
extern "system" fn native_audit_event(
mut env: JNIEnv,
_class: JClass,
_handle: jlong,
event_type: JString,
payload: JString,
) {
let event: String = match env.get_string(&event_type) {
Ok(s) => s.into(),
Err(_) => return,
};
let json: String = match env.get_string(&payload) {
Ok(s) => s.into(),
Err(_) => return,
};
if let Some(logger) = AUDIT_LOGGER.get() {
logger.log_event(&event, &json);
}
}
extern "system" fn native_audit_event_global(
mut env: JNIEnv,
_class: JClass,
event_type: JString,
payload: JString,
) {
let event: String = match env.get_string(&event_type) {
Ok(s) => s.into(),
Err(_) => return,
};
let json: String = match env.get_string(&payload) {
Ok(s) => s.into(),
Err(_) => return,
};
if let Some(logger) = AUDIT_LOGGER.get() {
logger.log_event(&event, &json);
}
}
// --- JNI_OnLoad: register native methods explicitly ---
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: JavaVM, _reserved: *mut std::ffi::c_void) -> jint {
let mut env = vm.get_env().expect("Failed to get JNI env in JNI_OnLoad");
let class_name = "io/fangorn/huorn/nativelib/NativeTerminal";
let class = match env.find_class(class_name) {
Ok(c) => c,
Err(_) => {
eprintln!("[Huorn] Failed to find class {}", class_name);
return JNI_VERSION_1_8;
}
};
let methods: &[NativeMethod] = &[
NativeMethod {
name: "nativeCreate".into(),
sig: "(IIFLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)J".into(),
fn_ptr: native_create as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeDestroy".into(),
sig: "(J)V".into(),
fn_ptr: native_destroy as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeSendText".into(),
sig: "(JLjava/lang/String;)V".into(),
fn_ptr: native_send_text as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeSendKey".into(),
sig: "(JII)V".into(),
fn_ptr: native_send_key as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeGetPixelData".into(),
sig: "(JLjava/nio/ByteBuffer;)Z".into(),
fn_ptr: native_get_pixel_data as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeGetDimensions".into(),
sig: "(J)[I".into(),
fn_ptr: native_get_dimensions as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeResize".into(),
sig: "(JII)V".into(),
fn_ptr: native_resize as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativePollPty".into(),
sig: "(J)Z".into(),
fn_ptr: native_poll_pty as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeIsAlive".into(),
sig: "(J)Z".into(),
fn_ptr: native_is_alive as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeScroll".into(),
sig: "(JI)V".into(),
fn_ptr: native_scroll as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeInitAudit".into(),
sig: "(Ljava/lang/String;)V".into(),
fn_ptr: native_init_audit as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeAuditEvent".into(),
sig: "(JLjava/lang/String;Ljava/lang/String;)V".into(),
fn_ptr: native_audit_event as *mut std::ffi::c_void,
},
NativeMethod {
name: "nativeAuditEventGlobal".into(),
sig: "(Ljava/lang/String;Ljava/lang/String;)V".into(),
fn_ptr: native_audit_event_global as *mut std::ffi::c_void,
},
];
match env.register_native_methods(&class, methods) {
Ok(_) => {
eprintln!("[Huorn] Registered {} native methods for {}", methods.len(), class_name);
}
Err(e) => {
eprintln!("[Huorn] Failed to register native methods: {}", e);
}
}
JNI_VERSION_1_8
}