Add Linux and macOS support, batch class transformations with a disk cache, and add cross-platform release packaging. Also improve runtime namespace handling, keybind editing, and startup behavior.
This commit is contained in:
bbldrizzy committed 2026-10-27 20:30:09 +00:00
1 parent 910a126f04
commit 38ff6d387c
29 files changed
+2234 -491

No files matched your search

+139 -30
View File
@@ -1,14 +1,16 @@
//! JVMTI state and the two natives of dev.zprestige.prestige.inject.NativeBridge.
use crate::jni::*;
use std::ffi::{c_char, c_void};
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::Mutex;
static JVMTI: AtomicPtr<*const *const c_void> = AtomicPtr::new(std::ptr::null_mut());
/// jclass whose bytes the ClassFileLoadHook should capture during the current RetransformClasses call (0 = none).
static CAPTURE_TARGET: AtomicUsize = AtomicUsize::new(0);
static CAPTURED: Mutex<Option<Vec<u8>>> = Mutex::new(None);
/// Serialises classBytes calls: CAPTURE_TARGET holds a thread-local jclass reference, so only one thread may capture at a time.
/// jclass references (as addresses) whose bytes the ClassFileLoadHook captures during the current RetransformClasses call.
/// Never held across the JVMTI call: the hook runs on the calling thread and locks it again.
static CAPTURE_TARGETS: Mutex<Vec<usize>> = Mutex::new(Vec::new());
/// Captured bytes, index-aligned with CAPTURE_TARGETS.
static CAPTURED: Mutex<Vec<Option<Vec<u8>>>> = Mutex::new(Vec::new());
/// Serialises classBytes calls: CAPTURE_TARGETS holds thread-local jclass references, so only one thread may capture at a time.
static CAPTURE_LOCK: Mutex<()> = Mutex::new(());
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
@@ -22,7 +24,7 @@ pub unsafe fn init_jvmti(vm: VmPtr) -> Result<(), String> {
if rc != 0 {
return Err(format!("AddCapabilities failed ({rc})"));
}
enable_class_file_load_hook(jvmti, class_file_load_hook)?;
install_class_file_load_hook(jvmti, class_file_load_hook)?;
JVMTI.store(jvmti, Ordering::SeqCst);
Ok(())
}
@@ -40,14 +42,22 @@ unsafe extern "system" fn class_file_load_hook(
_new_len: *mut JInt,
_new_data: *mut *mut u8,
) {
let target = CAPTURE_TARGET.load(Ordering::SeqCst);
if target == 0 || class_being_redefined.is_null() || data.is_null() {
if class_being_redefined.is_null() || data.is_null() {
return;
}
let targets: Vec<usize> = match CAPTURE_TARGETS.lock() {
Ok(t) if !t.is_empty() => t.clone(),
_ => return,
};
let jni = Jni::new(env);
if jni.is_same_object(class_being_redefined, target as JObject) {
if let Ok(mut slot) = CAPTURED.lock() {
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
for (i, &target) in targets.iter().enumerate() {
if jni.is_same_object(class_being_redefined, target as JObject) {
if let Ok(mut slots) = CAPTURED.lock() {
if let Some(slot) = slots.get_mut(i) {
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
}
}
return;
}
}
}
@@ -58,22 +68,39 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
if jvmti.is_null() {
return std::ptr::null_mut();
}
let _serial = CAPTURE_LOCK.lock();
if let Ok(mut slot) = CAPTURED.lock() {
*slot = None;
match capture(jvmti, &[target]).and_then(|mut v| v.pop().flatten()) {
Some(bytes) => to_byte_array(&Jni::new(env), &bytes),
None => std::ptr::null_mut(),
}
}
/// Retransforms `classes` in one JVMTI call and returns the captured bytes, index-aligned (None = not captured).
unsafe fn capture(jvmti: JvmtiPtr, classes: &[JClass]) -> Option<Vec<Option<Vec<u8>>>> {
let _serial = CAPTURE_LOCK.lock();
if let Ok(mut c) = CAPTURED.lock() {
*c = vec![None; classes.len()];
}
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
*t = classes.iter().map(|&c| c as usize).collect();
}
let enabled = set_class_file_load_hook_enabled(jvmti, true);
let rc = if enabled.is_ok() { retransform_classes(jvmti, classes) } else { -1 };
let _ = set_class_file_load_hook_enabled(jvmti, false);
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
t.clear();
}
if let Err(e) = enabled {
crate::log!("classBytes: {e}");
return None;
}
CAPTURE_TARGET.store(target as usize, Ordering::SeqCst);
let rc = retransform_class(jvmti, target);
CAPTURE_TARGET.store(0, Ordering::SeqCst);
if rc != 0 {
crate::log!("RetransformClasses failed ({rc})");
return std::ptr::null_mut();
return None;
}
let bytes = CAPTURED.lock().ok().and_then(|mut slot| slot.take());
let Some(bytes) = bytes else {
return std::ptr::null_mut();
};
let jni = Jni::new(env);
CAPTURED.lock().ok().map(|mut c| std::mem::take(&mut *c))
}
unsafe fn to_byte_array(jni: &Jni, bytes: &[u8]) -> JObject {
let array = jni.new_byte_array(bytes.len() as JInt);
if !array.is_null() {
jni.set_byte_array_region(array, 0, bytes.len() as JInt, bytes.as_ptr());
@@ -81,6 +108,80 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
array
}
/// byte[][] classBytesBatch(Class[]): classBytes for many classes with a single RetransformClasses call (one safepoint, one pass
/// over the other agents' transformers). Entries are null for classes that were not captured; the result is null on failure.
pub unsafe extern "system" fn native_class_bytes_batch(env: EnvPtr, _cls: JClass, targets: JObject) -> JObject {
let jvmti = JVMTI.load(Ordering::SeqCst);
if jvmti.is_null() || targets.is_null() {
return std::ptr::null_mut();
}
let jni = Jni::new(env);
let n = jni.get_array_length(targets);
if n <= 0 {
return std::ptr::null_mut();
}
jni.ensure_local_capacity(2 * n + 16);
let classes: Vec<JClass> = (0..n).map(|i| jni.get_object_array_element(targets, i)).collect();
let Some(captured) = capture(jvmti, &classes) else {
return std::ptr::null_mut();
};
let byte_array_class = jni.find_class("[B");
let result = jni.new_object_array(n, byte_array_class, std::ptr::null_mut());
if result.is_null() {
return std::ptr::null_mut();
}
for (i, bytes) in captured.iter().enumerate() {
if let Some(bytes) = bytes {
let array = to_byte_array(&jni, bytes);
if !array.is_null() {
jni.set_object_array_element(result, i as JInt, array);
}
}
}
result
}
/// int redefineBatch(Class[], byte[][]): JVMTI RedefineClasses of many classes in one call (one safepoint). It is all or nothing:
/// the JVMTI error code is returned (0 = success) and no class is changed on failure.
pub unsafe extern "system" fn native_redefine_batch(env: EnvPtr, _cls: JClass, targets: JObject, bytes: JObject) -> JInt {
let jvmti = JVMTI.load(Ordering::SeqCst);
if jvmti.is_null() || targets.is_null() || bytes.is_null() {
return -1;
}
let jni = Jni::new(env);
let n = jni.get_array_length(targets);
if n <= 0 || jni.get_array_length(bytes) != n {
return -1;
}
jni.ensure_local_capacity(2 * n + 16);
let mut buffers: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
let mut classes: Vec<JClass> = Vec::with_capacity(n as usize);
for i in 0..n {
let array = jni.get_object_array_element(bytes, i);
if array.is_null() {
return -1;
}
let len = jni.get_array_length(array);
if len < 0 {
return -1;
}
let mut buf = vec![0u8; len as usize];
jni.get_byte_array_region(array, 0, len, buf.as_mut_ptr());
buffers.push(buf);
classes.push(jni.get_object_array_element(targets, i));
}
let defs: Vec<ClassDefinition> = classes
.iter()
.zip(&buffers)
.map(|(&klass, buf)| ClassDefinition { klass, class_byte_count: buf.len() as JInt, class_bytes: buf.as_ptr() })
.collect();
let rc = redefine_classes(jvmti, &defs);
if rc != 0 {
crate::log!("RedefineClasses (batch of {n}) failed ({rc})");
}
rc
}
/// int redefine(Class, byte[]): JVMTI RedefineClasses of a single class; returns the JVMTI error code (0 = success).
pub unsafe extern "system" fn native_redefine(env: EnvPtr, _cls: JClass, target: JClass, bytes: JObject) -> JInt {
let jvmti = JVMTI.load(Ordering::SeqCst);
@@ -111,6 +212,7 @@ pub unsafe extern "system" fn native_log(env: EnvPtr, _cls: JClass, message: JOb
crate::log!("[java] {text}");
}
#[cfg(windows)]
extern "system" {
fn FreeLibraryAndExitThread(module: *mut c_void, exit_code: u32) -> !;
}
@@ -129,11 +231,18 @@ pub unsafe extern "system" fn native_release(env: EnvPtr, cls: JClass) {
if rc != JNI_OK {
crate::log!("release: UnregisterNatives failed ({rc})");
}
crate::log!("[java] released, unloading the loader");
let module = crate::MODULE as usize;
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(2000));
FreeLibraryAndExitThread(module as *mut c_void, 0);
});
#[cfg(windows)]
{
crate::log!("[java] released, unloading the loader");
let module = crate::MODULE as usize;
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(2000));
FreeLibraryAndExitThread(module as *mut c_void, 0);
});
}
// On unix the library is deliberately NOT dlclose'd: a thread of ours would return into unmapped code (and the
// JVM may still hold the agent handle). The hook and natives are gone; the code just stays resident.
#[cfg(unix)]
crate::log!("[java] released (library stays resident on this platform)");
}
+26 -13
View File
@@ -12,7 +12,6 @@ pub type JBool = u8;
pub type EnvPtr = *mut *const *const c_void;
pub const JNI_OK: JInt = 0;
pub const JNI_VERSION_1_8: JInt = 0x0001_0008;
pub const JVMTI_VERSION_1_2: JInt = 0x3001_0200;
#[repr(C)]
@@ -60,9 +59,6 @@ impl Jni {
pub unsafe fn new_global_ref(&self, o: JObject) -> JObject {
f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o)
}
pub unsafe fn delete_local_ref(&self, o: JObject) {
f!(self, 23, unsafe extern "system" fn(EnvPtr, JObject))(self.env, o)
}
pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool {
f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0
}
@@ -108,6 +104,15 @@ impl Jni {
pub unsafe fn get_array_length(&self, a: JObject) -> JInt {
f!(self, 171, unsafe extern "system" fn(EnvPtr, JObject) -> JInt)(self.env, a)
}
pub unsafe fn get_object_array_element(&self, a: JObject, i: JInt) -> JObject {
f!(self, 173, unsafe extern "system" fn(EnvPtr, JObject, JInt) -> JObject)(self.env, a, i)
}
pub unsafe fn set_object_array_element(&self, a: JObject, i: JInt, v: JObject) {
f!(self, 174, unsafe extern "system" fn(EnvPtr, JObject, JInt, JObject))(self.env, a, i, v)
}
pub unsafe fn ensure_local_capacity(&self, capacity: JInt) -> JInt {
f!(self, 26, unsafe extern "system" fn(EnvPtr, JInt) -> JInt)(self.env, capacity)
}
pub unsafe fn new_object_array(&self, len: JInt, c: JClass, init: JObject) -> JObject {
f!(self, 172, unsafe extern "system" fn(EnvPtr, JInt, JClass, JObject) -> JObject)(self.env, len, c, init)
}
@@ -126,9 +131,6 @@ impl Jni {
pub unsafe fn unregister_natives(&self, c: JClass) -> JInt {
f!(self, 216, unsafe extern "system" fn(EnvPtr, JClass) -> JInt)(self.env, c)
}
pub unsafe fn exception_check(&self) -> bool {
f!(self, 228, unsafe extern "system" fn(EnvPtr) -> JBool)(self.env) != 0
}
/// Returns "ExceptionClass: message" of the pending exception and clears it.
pub unsafe fn take_exception(&self) -> Option<String> {
@@ -236,8 +238,8 @@ pub unsafe fn add_capabilities(j: JvmtiPtr, caps: &Capabilities) -> JInt {
f(j, caps)
}
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks) and enables the event.
pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks); the event itself stays off until enabled.
pub unsafe fn install_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
let mut callbacks = [0usize; 40];
callbacks[4] = hook as usize;
let set_callbacks = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const usize, JInt) -> JInt>(*(*j).add(121));
@@ -245,17 +247,28 @@ pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook)
if rc != 0 {
return Err(format!("SetEventCallbacks failed ({rc})"));
}
Ok(())
}
/// Turns the ClassFileLoadHook event on or off. It is only on while class bytes are being captured: with the event enabled the
/// JVM hands the bytes of every class it loads to the agent, which slows class loading down for the whole game.
pub unsafe fn set_class_file_load_hook_enabled(j: JvmtiPtr, enabled: bool) -> Result<(), String> {
let set_mode = std::mem::transmute::<*const c_void, unsafe extern "C" fn(JvmtiPtr, JInt, JInt, JObject, ...) -> JInt>(*(*j).add(1));
let rc = set_mode(j, 1, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
let rc = set_mode(j, enabled as JInt, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
if rc != 0 {
return Err(format!("SetEventNotificationMode failed ({rc})"));
return Err(format!("SetEventNotificationMode({enabled}) failed ({rc})"));
}
Ok(())
}
pub unsafe fn retransform_class(j: JvmtiPtr, class: JClass) -> JInt {
pub unsafe fn retransform_classes(j: JvmtiPtr, classes: &[JClass]) -> JInt {
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const JClass) -> JInt>(*(*j).add(151));
f(j, 1, &class)
f(j, classes.len() as JInt, classes.as_ptr())
}
pub unsafe fn redefine_classes(j: JvmtiPtr, defs: &[ClassDefinition]) -> JInt {
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const ClassDefinition) -> JInt>(*(*j).add(86));
f(j, defs.len() as JInt, defs.as_ptr())
}
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
+31
View File
@@ -10,14 +10,18 @@ mod worker;
use std::ffi::c_void;
#[cfg(windows)]
const DLL_PROCESS_ATTACH: u32 = 1;
#[cfg(windows)]
extern "system" {
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
}
#[cfg(windows)]
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
#[cfg(windows)]
#[no_mangle]
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
if reason == DLL_PROCESS_ATTACH {
@@ -30,3 +34,30 @@ pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut
}
1
}
/// The JavaVM handed to Agent_OnAttach / Agent_OnLoad by the JVM (unix); read by the worker.
#[cfg(unix)]
pub(crate) static VM: std::sync::atomic::AtomicPtr<c_void> = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
#[cfg(unix)]
fn start_worker(vm: *mut c_void) -> i32 {
VM.store(vm, std::sync::atomic::Ordering::SeqCst);
match std::thread::Builder::new().name("prestige-loader".into()).spawn(worker::run) {
Ok(_) => 0,
Err(_) => -1,
}
}
/// Called by HotSpot when the library is loaded through the attach protocol (JVMTI.agent_load).
#[cfg(unix)]
#[no_mangle]
pub extern "C" fn Agent_OnAttach(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
start_worker(vm)
}
/// Called by the JVM when the library is given as -agentpath at start-up.
#[cfg(unix)]
#[no_mangle]
pub extern "C" fn Agent_OnLoad(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
start_worker(vm)
}
+38 -4
View File
@@ -1,21 +1,55 @@
use std::ffi::c_void;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(windows)]
extern "system" {
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
}
#[cfg(unix)]
#[repr(C)]
struct DlInfo {
dli_fname: *const std::ffi::c_char,
dli_fbase: *mut c_void,
dli_sname: *const std::ffi::c_char,
dli_saddr: *mut c_void,
}
#[cfg(unix)]
extern "C" {
fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
}
#[cfg(unix)]
fn module_path() -> PathBuf {
fn anchor() {}
let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() };
let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) };
if ok != 0 && !info.dli_fname.is_null() {
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()))
} else {
std::env::temp_dir().join("libprestige_native")
}
}
#[cfg(windows)]
fn module_path() -> PathBuf {
let mut buf = [0u16; 1024];
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
PathBuf::from(String::from_utf16_lossy(&buf[..len]))
}
fn log_path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| {
let mut buf = [0u16; 1024];
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
let dll = PathBuf::from(String::from_utf16_lossy(&buf[..len]));
dll.with_file_name("prestige-native.log")
module_path().with_file_name("prestige-native.log")
})
}
+122 -15
View File
@@ -1,15 +1,18 @@
//! The loader worker: JVM discovery, payload installation into Fabric's Knot class loader, native registration, bootstrap.
use crate::bridge::{init_jvmti, native_class_bytes, native_log, native_redefine, native_release};
use crate::bridge::{init_jvmti, native_class_bytes, native_class_bytes_batch, native_log, native_redefine, native_redefine_batch, native_release};
use crate::jni::*;
use crate::log;
use std::ffi::{c_char, c_void};
#[cfg(windows)]
use std::ffi::c_char;
use std::ffi::c_void;
use std::path::PathBuf;
#[cfg(windows)]
use std::time::Duration;
#[cfg(windows)]
extern "system" {
fn GetModuleHandleA(name: *const c_char) -> *mut c_void;
fn GetProcAddress(module: *mut c_void, name: *const c_char) -> *mut c_void;
fn GetCurrentProcessId() -> u32;
}
static PAYLOAD: &[u8] = include_bytes!(env!("PRESTIGE_PAYLOAD_JAR"));
@@ -18,7 +21,7 @@ const NATIVE_BRIDGE: &str = "dev.zprestige.prestige.inject.NativeBridge";
const BOOTSTRAP: &str = "dev.zprestige.prestige.inject.Bootstrap";
pub fn run() {
log!("worker started, pid {}", unsafe { GetCurrentProcessId() });
log!("worker started, pid {}", std::process::id());
match unsafe { install() } {
Ok(()) => log!("bootstrap returned"),
Err(e) => log!("FAILED: {e}"),
@@ -50,7 +53,7 @@ unsafe fn install() -> Result<(), String> {
register_natives(&jni, loader)?;
log!("natives registered");
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader)?;
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader, true)?;
let start = jni.get_static_method_id(bootstrap, "start", "()V");
jni.check("resolve Bootstrap.start")?;
// Bootstrap.start waits for the client, installs the hooks and schedules the client initialisation.
@@ -59,7 +62,18 @@ unsafe fn install() -> Result<(), String> {
Ok(())
}
/// The JavaVM passed to Agent_OnAttach / Agent_OnLoad.
#[cfg(unix)]
unsafe fn find_vm() -> Result<VmPtr, String> {
let vm = crate::VM.load(std::sync::atomic::Ordering::SeqCst) as VmPtr;
if vm.is_null() {
return Err("no JavaVM was passed to the agent".into());
}
Ok(vm)
}
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
#[cfg(windows)]
unsafe fn find_vm() -> Result<VmPtr, String> {
let mut module = std::ptr::null_mut();
for _ in 0..600 {
@@ -91,7 +105,7 @@ unsafe fn find_vm() -> Result<VmPtr, String> {
fn write_payload() -> Result<PathBuf, String> {
let dir = std::env::temp_dir().join("PrestigeLoader");
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
let path = dir.join(format!("prestige-payload-{}.jar", unsafe { GetCurrentProcessId() }));
let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
Ok(path)
}
@@ -99,9 +113,8 @@ fn write_payload() -> Result<PathBuf, String> {
/// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader.
/// Returns a global reference to the Knot target class loader.
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
// A thread attached from native code resolves classes through the system class loader, which holds fabric-loader.
let base = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncherBase");
jni.check("FabricLauncherBase (is this a Fabric instance?)")?;
jni.ensure_local_capacity(4096);
let (base, fabric_loader) = locate_fabric(jni)?;
let get_launcher = jni.get_static_method_id(base, "getLauncher", "()Lnet/fabricmc/loader/impl/launch/FabricLauncher;");
jni.check("FabricLauncherBase.getLauncher")?;
let launcher = jni.call_static_object(base, get_launcher, &[]);
@@ -110,8 +123,7 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
return Err("Fabric launcher is not available yet".into());
}
let launcher_class = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncher");
jni.check("FabricLauncher")?;
let launcher_class = load_fabric_class(jni, "net.fabricmc.loader.impl.launch.FabricLauncher", fabric_loader)?;
let add = jni.get_method_id(launcher_class, "addToClassPath", "(Ljava/nio/file/Path;[Ljava/lang/String;)V");
jni.check("FabricLauncher.addToClassPath")?;
let target = jni.get_method_id(launcher_class, "getTargetClassLoader", "()Ljava/lang/ClassLoader;");
@@ -140,13 +152,98 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
Ok(jni.new_global_ref(loader))
}
/// Class.forName(name, true, loader)
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase";
/// Finds the class loader that can see fabric-loader. The system class loader is tried first (official launcher, most
/// others); launchers that run the game from their own URLClassLoader (Prism/MultiMC, many modpack launchers) keep
/// fabric-loader out of it, so every thread's context class loader is probed too. Returns the FabricLauncherBase class
/// and the loader it was found through (null = FindClass / system loader).
unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
let base = jni.find_class(&FABRIC_BASE.replace('.', "/"));
if jni.take_exception().is_none() && !base.is_null() {
return Ok((base, std::ptr::null_mut()));
}
log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders");
let loaders = thread_loaders(jni)?;
for &loader in &loaders {
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
log!("fabric-loader found through {}", describe(jni, loader));
return Ok((class, loader));
}
}
let seen: Vec<String> = loaders.iter().map(|&l| describe(jni, l)).collect();
Err(format!(
"net.fabricmc.loader is not visible to any class loader ({} probed: {}); is this a Fabric instance?",
seen.len(),
seen.join(" | ")
))
}
/// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader).
unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
let thread_class = jni.find_class("java/lang/Thread");
let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;");
let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;");
let map_class = jni.find_class("java/util/Map");
let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;");
let set_class = jni.find_class("java/util/Set");
let to_array = jni.get_method_id(set_class, "toArray", "()[Ljava/lang/Object;");
jni.check("Thread/Map/Set lookup")?;
let map = jni.call_static_object(thread_class, get_all, &[]);
jni.check("Thread.getAllStackTraces")?;
let set = jni.call_object(jni_non_null(map)?, key_set, &[]);
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
jni.check("thread list")?;
let mut loaders: Vec<JObject> = Vec::new();
for i in 0..jni.get_array_length(jni_non_null(threads)?) {
let thread = jni.get_object_array_element(threads, i);
if thread.is_null() {
continue;
}
let loader = jni.call_object(thread, get_context, &[]);
if jni.take_exception().is_some() || loader.is_null() {
continue;
}
if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) {
loaders.push(loader);
}
}
Ok(loaders)
}
unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> {
if o.is_null() { Err("unexpected null from the JVM".into()) } else { Ok(o) }
}
/// Object.toString() of a class loader for the log.
unsafe fn describe(jni: &Jni, o: JObject) -> String {
let class = jni.get_object_class(o);
let to_string = jni.get_method_id(class, "toString", "()Ljava/lang/String;");
if to_string.is_null() {
jni.exception_clear();
return "<loader>".into();
}
let s = jni.call_object(o, to_string, &[]);
if jni.take_exception().is_some() || s.is_null() { "<loader>".into() } else { jni.get_string_utf(s) }
}
/// Loads a fabric-loader class through the loader found by `locate_fabric` (null = system class loader).
unsafe fn load_fabric_class(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
if loader.is_null() {
let class = jni.find_class(&name.replace('.', "/"));
jni.check(name)?;
return Ok(class);
}
class_for_name(jni, name, loader, true)
}
/// Class.forName(name, initialize, loader)
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject, initialize: bool) -> Result<JClass, String> {
let class_class = jni.find_class("java/lang/Class");
let for_name = jni.get_static_method_id(class_class, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
jni.check("Class.forName")?;
let name_string = jni.new_string(name);
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(true), obj(loader)]);
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(initialize), obj(loader)]);
jni.check(&format!("load {name}"))?;
if class.is_null() {
return Err(format!("{name} not found"));
@@ -155,7 +252,7 @@ unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClas
}
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
let class = class_for_name(jni, NATIVE_BRIDGE, loader)?;
let class = class_for_name(jni, NATIVE_BRIDGE, loader, true)?;
let methods = [
NativeMethod {
name: c"classBytes".as_ptr(),
@@ -167,6 +264,16 @@ unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
fn_ptr: native_redefine as *const () as usize as *mut c_void,
},
NativeMethod {
name: c"classBytesBatch".as_ptr(),
signature: c"([Ljava/lang/Class;)[[B".as_ptr(),
fn_ptr: native_class_bytes_batch as *const () as usize as *mut c_void,
},
NativeMethod {
name: c"redefineBatch".as_ptr(),
signature: c"([Ljava/lang/Class;[[B)I".as_ptr(),
fn_ptr: native_redefine_batch as *const () as usize as *mut c_void,
},
NativeMethod {
name: c"release".as_ptr(),
signature: c"()V".as_ptr(),