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

+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(),