This commit is contained in:
bbldrizzy committed 2026-10-23 00:28:57 +01:00
1 parent 83aea53e57
commit aa4f6b6ddc
842 files changed
+47284 -91563

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "prestige-injector"
version = "0.1.0"
[[package]]
name = "prestige_native"
version = "0.1.0"
+10
View File
@@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = ["crates/loader", "crates/injector"]
[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
+32
View File
@@ -0,0 +1,32 @@
# Prestige injector (Rust)
`PrestigeInjector.exe` loads `prestige_native.dll` into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x,
Fabric API installed). It uses the same overall method as the openvape bundle, written from scratch in Rust with no external crates.
## How it works
1. **Injector** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
process through `CreateRemoteThread` and confirms the module is mapped.
2. **`prestige_native.dll`** (`DllMain` only spawns a worker thread):
- waits for `jvm.dll`, `JNI_GetCreatedJavaVMs`, attaches, acquires the JVMTI redefine/retransform capabilities;
- writes the embedded payload jar (the remapped `prestige-*.jar`) to `%TEMP%\PrestigeLoader`;
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
(JVMTI retransform capture), runs the hooks through `InPlaceTransformer` and redefines the class. The bundled ClassTransform
inlines hook handlers into the target methods, so normally nothing is added to the class and a plain redefine works. If a
transformer ever does add methods (a running JVM cannot), they are moved into generated `dev.zprestige.prestige.hooks.H*`
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
Finally the Prestige and ClickGUI entrypoints run on the client thread.
Log: `prestige-native.log` beside the DLL, plus `[Prestige-Inject]` lines in the game log.
## Build
```powershell
.\build-injector.ps1 # gradlew remapJar, cargo build --release, copies to dist\
```
## Use
Start a Fabric 1.21.11 instance, then run `dist\PrestigeInjector.exe` (or `PrestigeInjector.exe <pid> [dll]`).
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "prestige-injector"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "PrestigeInjector"
path = "src/main.rs"
+280
View File
@@ -0,0 +1,280 @@
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads prestige_native.dll into the selected one
//! with the classic remote LoadLibraryW thread. The DLL does everything else once it is inside the JVM; the injector tails
//! the DLL's log so every stage (transforming, injecting, initialising) is visible.
//!
//! PrestigeInjector.exe interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector.exe <pid> [dll path] non-interactive
mod win;
use std::fs;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::{Duration, Instant};
const DLL_NAME: &str = "prestige_native.dll";
const WATERMARK: &str = "Cracked by Skid Solutions";
const INVITE_URL: &str = "https://discord.gg/M56rd2wwC5";
/// How long to follow the DLL's progress after it has been loaded.
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
const RESET: &str = "\x1b[0m";
const DIM: &str = "\x1b[90m";
const CYAN: &str = "\x1b[96m";
const GREEN: &str = "\x1b[92m";
const YELLOW: &str = "\x1b[93m";
const RED: &str = "\x1b[91m";
fn default_dll() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(DLL_NAME)))
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
}
fn banner() {
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
println!("{YELLOW} {WATERMARK}{RESET}\n");
}
fn step(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock());
}
fn ok(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock());
}
fn fail(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", clock());
}
/// Seconds since the injector started, for the log prefix.
fn clock() -> String {
use std::sync::OnceLock;
static START: OnceLock<Instant> = OnceLock::new();
format!("{:.1}s", START.get_or_init(Instant::now).elapsed().as_secs_f32())
}
fn main() -> ExitCode {
win::enable_ansi();
let _ = clock();
let args: Vec<String> = std::env::args().skip(1).collect();
let interactive = args.is_empty();
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
[] => {
banner();
invite_prompt();
(pick_process(), default_dll())
}
[pid] => (parse_pid(pid), default_dll()),
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
_ => {
eprintln!("usage: PrestigeInjector.exe [<pid> [dll path]]");
return ExitCode::from(2);
}
};
let Some(pid) = pid else {
// interactive mode quit with Esc, or an unparsable pid was given
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
};
if !interactive {
banner();
}
let code = run(pid, &dll);
if interactive {
print!("\nPress Enter to close...");
let _ = io::stdout().flush();
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
}
code
}
fn parse_pid(text: &str) -> Option<u32> {
text.parse().ok()
}
fn run(pid: u32, dll: &Path) -> ExitCode {
println!();
step("target", &format!("process {pid}"));
let dll = match dll.canonicalize() {
Ok(p) => p,
Err(e) => {
fail("error", &format!("cannot find {}: {e}", dll.display()));
return ExitCode::from(1);
}
};
let dll_display = dll.display().to_string();
step("loader", &dll_display.trim_start_matches(r"\\?\").to_string());
// The DLL appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
let log_path = dll.with_file_name("prestige-native.log");
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
step("checking", "64-bit JVM, process access, DLL not already loaded");
step("injecting", "remote thread LoadLibraryW");
match win::inject(pid, &dll) {
Ok(()) => ok("injected", "prestige_native.dll is loaded in the game process"),
Err(e) => {
fail("failed", &e);
return ExitCode::from(1);
}
}
step("transforming", "following the loader log (attach, class loader, hook transformer)...");
match follow_log(&log_path, log_start) {
Outcome::Done => {
println!();
ok("success", "successfully injected - Prestige is running in the game");
println!("{YELLOW} {WATERMARK}{RESET} {DIM}{INVITE_URL}{RESET}");
ExitCode::SUCCESS
}
Outcome::Failed(line) => {
fail("failed", &format!("injection did not complete: {line}"));
println!("{DIM} details: {} and the game's latest.log{RESET}", log_path.display());
ExitCode::from(1)
}
Outcome::TimedOut => {
fail("timeout", "no completion message from the loader within 3 minutes");
println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display());
ExitCode::from(1)
}
}
}
enum Outcome {
Done,
Failed(String),
TimedOut,
}
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
fn follow_log(path: &Path, start: u64) -> Outcome {
let deadline = Instant::now() + FOLLOW_TIMEOUT;
let mut offset = start;
let mut pending = String::new();
while Instant::now() < deadline {
if let Ok(mut file) = fs::File::open(path) {
if file.seek(SeekFrom::Start(offset)).is_ok() {
let mut chunk = String::new();
if let Ok(n) = file.read_to_string(&mut chunk) {
offset += n as u64;
pending.push_str(&chunk);
}
}
}
while let Some(end) = pending.find('\n') {
let line: String = pending.drain(..=end).collect();
let line = line.trim_end();
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
if lower.contains("failed") || lower.contains("not found") {
fail("loader", message);
return Outcome::Failed(message.to_string());
}
if message.contains("client initialised") {
ok("loader", message);
return Outcome::Done;
}
step("loader", message);
}
std::thread::sleep(Duration::from_millis(200));
}
Outcome::TimedOut
}
fn config_path() -> Option<PathBuf> {
std::env::var_os("APPDATA").map(|dir| PathBuf::from(dir).join("PrestigeInjector").join("settings.txt"))
}
/// First launch: open the Discord invite, then ask whether to open it again on later launches.
fn invite_prompt() {
let Some(path) = config_path() else { return };
let saved = fs::read_to_string(&path).ok();
let show = match saved.as_deref() {
None => true, // first launch
Some(text) => !text.lines().any(|l| l.trim() == "show_invite=false"),
};
if !show {
return;
}
let first = saved.is_none();
step("discord", &format!("opening {INVITE_URL}"));
if !win::open_url(INVITE_URL) {
fail("discord", &format!("could not open a browser, visit {INVITE_URL} manually"));
}
let again = ask_yes_no(if first {
"Show the Discord invite again next time you start the injector? [Y/n] "
} else {
"Keep showing the Discord invite on launch? [Y/n] "
});
if let Some(dir) = path.parent() {
let _ = fs::create_dir_all(dir);
}
let _ = fs::write(&path, format!("show_invite={again}\n"));
println!();
}
fn ask_yes_no(question: &str) -> bool {
print!("{question}");
let _ = io::stdout().flush();
let mut line = String::new();
if io::stdin().read_line(&mut line).is_err() {
return true;
}
!matches!(line.trim().to_ascii_lowercase().as_str(), "n" | "no")
}
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
fn pick_process() -> Option<u32> {
let mut selected = 0usize;
let mut processes = win::java_windows();
let mut last_refresh = Instant::now();
loop {
if selected >= processes.len() {
selected = processes.len().saturating_sub(1);
}
draw(&processes, selected);
let deadline = Instant::now() + Duration::from_millis(50);
while Instant::now() < deadline {
if let Some(key) = win::poll_key() {
match key {
win::Key::Up => selected = selected.saturating_sub(1),
win::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
win::Key::Enter => {
if let Some(p) = processes.get(selected) {
return Some(p.pid);
}
}
win::Key::Escape => return None,
}
draw(&processes, selected);
}
std::thread::sleep(Duration::from_millis(10));
}
if last_refresh.elapsed() >= Duration::from_millis(750) {
processes = win::java_windows();
last_refresh = Instant::now();
}
}
}
fn draw(processes: &[win::JavaWindow], selected: usize) {
let mut out = String::from("\x1b[2J\x1b[H");
out.push_str(&format!("{CYAN}Prestige injector{RESET} - Minecraft 1.21.11 (Fabric, 64-bit) {YELLOW}{WATERMARK}{RESET}\n"));
out.push_str("Up/Down select, Enter inject, Esc quit\n\n");
if processes.is_empty() {
out.push_str(" waiting for a javaw.exe / java.exe window...\n");
}
for (i, p) in processes.iter().enumerate() {
let marker = if i == selected { ">" } else { " " };
out.push_str(&format!("{marker} [{:>6}] {} ({})\n", p.pid, p.title, p.exe));
}
print!("{out}");
let _ = io::stdout().flush();
}
+278
View File
@@ -0,0 +1,278 @@
//! Win32 plumbing of the injector (raw bindings, no external crates).
use std::ffi::c_void;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::time::Duration;
type Handle = *mut c_void;
const PROCESS_CREATE_THREAD: u32 = 0x0002;
const PROCESS_VM_OPERATION: u32 = 0x0008;
const PROCESS_VM_WRITE: u32 = 0x0020;
const PROCESS_VM_READ: u32 = 0x0010;
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const MEM_COMMIT: u32 = 0x1000;
const MEM_RESERVE: u32 = 0x2000;
const MEM_RELEASE: u32 = 0x8000;
const PAGE_READWRITE: u32 = 0x04;
const TH32CS_SNAPMODULE: u32 = 0x8;
const TH32CS_SNAPMODULE32: u32 = 0x10;
const WAIT_OBJECT_0: u32 = 0;
#[repr(C)]
struct ModuleEntry32W {
size: u32,
module_id: u32,
process_id: u32,
glblcnt_usage: u32,
proccnt_usage: u32,
base_addr: *mut u8,
base_size: u32,
module: Handle,
sz_module: [u16; 256],
sz_exe_path: [u16; 260],
}
extern "system" {
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
fn CloseHandle(h: Handle) -> i32;
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle;
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
fn GetModuleHandleW(name: *const u16) -> Handle;
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
fn GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn GetStdHandle(id: u32) -> Handle;
fn GetConsoleMode(h: Handle, mode: *mut u32) -> i32;
fn SetConsoleMode(h: Handle, mode: u32) -> i32;
}
#[link(name = "user32")]
extern "system" {
fn EnumWindows(callback: unsafe extern "system" fn(Handle, isize) -> i32, param: isize) -> i32;
fn IsWindowVisible(hwnd: Handle) -> i32;
fn GetWindowTextW(hwnd: Handle, buf: *mut u16, max: i32) -> i32;
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
}
#[link(name = "shell32")]
extern "system" {
fn ShellExecuteW(hwnd: Handle, verb: *const u16, file: *const u16, params: *const u16, dir: *const u16, show: i32) -> isize;
}
extern "C" {
fn _kbhit() -> i32;
fn _getch() -> i32;
}
pub struct JavaWindow {
pub pid: u32,
pub title: String,
pub exe: String,
}
pub enum Key {
Up,
Down,
Enter,
Escape,
}
pub fn enable_ansi() {
unsafe {
let out = GetStdHandle(-11i32 as u32);
let mut mode = 0;
if GetConsoleMode(out, &mut mode) != 0 {
SetConsoleMode(out, mode | 0x0004);
}
}
}
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
pub fn poll_key() -> Option<Key> {
unsafe {
if _kbhit() == 0 {
return None;
}
match _getch() {
13 => Some(Key::Enter),
27 => Some(Key::Escape),
0 | 0xE0 => match _getch() {
72 => Some(Key::Up),
80 => Some(Key::Down),
_ => None,
},
_ => None,
}
}
}
fn wide(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
}
fn image_name(pid: u32) -> Option<String> {
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if process.is_null() {
return None;
}
let mut buf = [0u16; 1024];
let mut len = buf.len() as u32;
let ok = QueryFullProcessImageNameW(process, 0, buf.as_mut_ptr(), &mut len);
CloseHandle(process);
if ok == 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..len as usize]))
}
}
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
let out = &mut *(param as *mut Vec<(u32, String)>);
if IsWindowVisible(hwnd) == 0 {
return 1;
}
let mut buf = [0u16; 256];
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
if len <= 0 {
return 1;
}
let mut pid = 0;
GetWindowThreadProcessId(hwnd, &mut pid);
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
1
}
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
pub fn java_windows() -> Vec<JavaWindow> {
let mut windows: Vec<(u32, String)> = Vec::new();
unsafe {
EnumWindows(collect, &mut windows as *mut _ as isize);
}
let mut out: Vec<JavaWindow> = Vec::new();
for (pid, title) in windows {
if out.iter().any(|w| w.pid == pid) {
continue;
}
let Some(path) = image_name(pid) else { continue };
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
out.push(JavaWindow { pid, title, exe });
}
}
out
}
fn last_error(what: &str) -> String {
format!("{what} failed (error {})", unsafe { GetLastError() })
}
/// True when the module file name is loaded in the process.
fn module_loaded(pid: u32, file_name: &str) -> bool {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if snapshot.is_null() || snapshot as isize == -1 {
return false;
}
let mut entry: ModuleEntry32W = std::mem::zeroed();
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
let mut found = false;
let mut ok = Module32FirstW(snapshot, &mut entry);
while ok != 0 {
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true;
break;
}
ok = Module32NextW(snapshot, &mut entry);
}
CloseHandle(snapshot);
found
}
}
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
unsafe {
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
let process = OpenProcess(access, 0, pid);
if process.is_null() {
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
}
let result = inject_into(process, pid, dll);
CloseHandle(process);
result
}
}
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
let mut wow64 = 0;
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
}
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
if module_loaded(pid, &file_name) {
return Err(format!("{file_name} is already loaded in process {pid}"));
}
let path = wide(dll);
let bytes = path.len() * 2;
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if remote.is_null() {
return Err(last_error("VirtualAllocEx"));
}
let cleanup = |err: String| {
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
Err(err)
};
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
return cleanup(last_error("WriteProcessMemory"));
}
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
if load_library.is_null() {
return cleanup(last_error("resolve LoadLibraryW"));
}
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
if thread.is_null() {
return cleanup(last_error("CreateRemoteThread"));
}
let wait = WaitForSingleObject(thread, 30_000);
let mut exit = 0;
GetExitCodeThread(thread, &mut exit);
CloseHandle(thread);
if wait != WAIT_OBJECT_0 {
// The thread may still be running inside the target; leave the buffer alone.
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
}
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
// LoadLibraryW's return value is truncated to 32 bits by GetExitCodeThread, so confirm through the module list.
for _ in 0..20 {
if module_loaded(pid, &file_name) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
}
/// Opens a URL in the default web browser.
pub fn open_url(url: &str) -> bool {
let verb: Vec<u16> = "open".encode_utf16().collect();
let target: Vec<u16> = url.encode_utf16().chain(std::iter::once(0)).collect();
// ShellExecute returns a value greater than 32 on success
unsafe { ShellExecuteW(std::ptr::null_mut(), verb.as_ptr(), target.as_ptr(), std::ptr::null(), std::ptr::null(), 1) > 32 }
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "prestige_native"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[lib]
crate-type = ["cdylib"]
+35
View File
@@ -0,0 +1,35 @@
//! Locates the remapped Prestige payload jar (built by `gradlew remapJar`) so lib.rs can embed it with include_bytes!.
use std::env;
use std::fs;
use std::path::PathBuf;
fn main() {
println!("cargo:rerun-if-env-changed=PRESTIGE_PAYLOAD");
let jar = match env::var("PRESTIGE_PAYLOAD") {
Ok(p) => PathBuf::from(p),
Err(_) => newest_remapped_jar(),
};
if !jar.is_file() {
panic!("payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", jar.display());
}
println!("cargo:rerun-if-changed={}", jar.display());
println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", jar.display());
}
fn newest_remapped_jar() -> PathBuf {
let libs = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("../../../build/libs");
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
if let Ok(entries) = fs::read_dir(&libs) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with("prestige-") || !name.ends_with(".jar") || name.ends_with("-dev.jar") || name.ends_with("-sources.jar") {
continue;
}
let time = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().map_or(true, |(t, _)| time > *t) {
best = Some((time, entry.path()));
}
}
}
best.map(|(_, p)| p).unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
}
+106
View File
@@ -0,0 +1,106 @@
//! 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::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);
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
pub unsafe fn init_jvmti(vm: VmPtr) -> Result<(), String> {
let jvmti = get_jvmti(vm)?;
let mut caps = Capabilities::default();
for bit in [CAN_REDEFINE_CLASSES, CAN_REDEFINE_ANY_CLASS, CAN_GENERATE_ALL_CLASS_HOOK_EVENTS, CAN_RETRANSFORM_CLASSES, CAN_RETRANSFORM_ANY_CLASS] {
caps.set(bit);
}
let rc = add_capabilities(jvmti, &caps);
if rc != 0 {
return Err(format!("AddCapabilities failed ({rc})"));
}
enable_class_file_load_hook(jvmti, class_file_load_hook)?;
JVMTI.store(jvmti, Ordering::SeqCst);
Ok(())
}
/// Copies the class bytes the JVM hands to us while a classBytes() call retransforms its target; never modifies anything.
unsafe extern "system" fn class_file_load_hook(
_jvmti: JvmtiPtr,
env: EnvPtr,
class_being_redefined: JClass,
_loader: JObject,
_name: *const c_char,
_domain: JObject,
data_len: JInt,
data: *const u8,
_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() {
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());
}
}
}
/// byte[] classBytes(Class): the bytes the JVM currently holds for the class (HotSpot reconstitutes them for retransformation).
pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, target: JClass) -> JObject {
let jvmti = JVMTI.load(Ordering::SeqCst);
if jvmti.is_null() {
return std::ptr::null_mut();
}
if let Ok(mut slot) = CAPTURED.lock() {
*slot = 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();
}
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);
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());
}
array
}
/// 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);
if jvmti.is_null() || bytes.is_null() {
return -1;
}
let jni = Jni::new(env);
let len = jni.get_array_length(bytes);
let mut buf = vec![0u8; len as usize];
jni.get_byte_array_region(bytes, 0, len, buf.as_mut_ptr());
let def = ClassDefinition { klass: target, class_byte_count: len, class_bytes: buf.as_ptr() };
let rc = redefine_class(jvmti, &def);
if rc != 0 {
crate::log!("RedefineClasses failed ({rc})");
}
rc
}
/// void log(String): lets the Java side write progress lines into prestige-native.log.
pub unsafe extern "system" fn native_log(env: EnvPtr, _cls: JClass, message: JObject) {
if message.is_null() {
return;
}
let text = Jni::new(env).get_string_utf(message);
crate::log!("[java] {text}");
}
+255
View File
@@ -0,0 +1,255 @@
//! Minimal raw JNI / JVMTI bindings. Function-table indices were checked against the JDK 21 jni.h / jvmti.h.
use std::ffi::{c_char, c_void, CStr, CString};
pub type JObject = *mut c_void;
pub type JClass = JObject;
pub type JMethodId = *mut c_void;
pub type JValue = u64;
pub type JInt = i32;
pub type JBool = u8;
/// JNIEnv*: pointer to a pointer to the function table.
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)]
pub struct NativeMethod {
pub name: *const c_char,
pub signature: *const c_char,
pub fn_ptr: *mut c_void,
}
/// Packs a JNI argument into the 8-byte jvalue slot.
pub fn obj(o: JObject) -> JValue {
o as usize as u64
}
pub fn boolean(b: bool) -> JValue {
b as u64
}
pub struct Jni {
pub env: EnvPtr,
}
macro_rules! f {
($self:expr, $idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*(*$self.env).add($idx))
};
}
impl Jni {
/// # Safety
/// `env` must be a valid JNIEnv* of the current thread.
pub unsafe fn new(env: EnvPtr) -> Self {
Jni { env }
}
pub unsafe fn find_class(&self, name: &str) -> JClass {
let n = CString::new(name).unwrap();
f!(self, 6, unsafe extern "system" fn(EnvPtr, *const c_char) -> JClass)(self.env, n.as_ptr())
}
pub unsafe fn exception_occurred(&self) -> JObject {
f!(self, 15, unsafe extern "system" fn(EnvPtr) -> JObject)(self.env)
}
pub unsafe fn exception_clear(&self) {
f!(self, 17, unsafe extern "system" fn(EnvPtr))(self.env)
}
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
}
pub unsafe fn get_object_class(&self, o: JObject) -> JClass {
f!(self, 31, unsafe extern "system" fn(EnvPtr, JObject) -> JClass)(self.env, o)
}
pub unsafe fn get_method_id(&self, c: JClass, name: &str, sig: &str) -> JMethodId {
let (n, s) = (CString::new(name).unwrap(), CString::new(sig).unwrap());
f!(self, 33, unsafe extern "system" fn(EnvPtr, JClass, *const c_char, *const c_char) -> JMethodId)(self.env, c, n.as_ptr(), s.as_ptr())
}
pub unsafe fn new_object(&self, c: JClass, ctor: JMethodId, args: &[JValue]) -> JObject {
f!(self, 30, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue) -> JObject)(self.env, c, ctor, args.as_ptr())
}
pub unsafe fn call_object(&self, o: JObject, m: JMethodId, args: &[JValue]) -> JObject {
f!(self, 36, unsafe extern "system" fn(EnvPtr, JObject, JMethodId, *const JValue) -> JObject)(self.env, o, m, args.as_ptr())
}
pub unsafe fn call_void(&self, o: JObject, m: JMethodId, args: &[JValue]) {
f!(self, 63, unsafe extern "system" fn(EnvPtr, JObject, JMethodId, *const JValue))(self.env, o, m, args.as_ptr())
}
pub unsafe fn get_static_method_id(&self, c: JClass, name: &str, sig: &str) -> JMethodId {
let (n, s) = (CString::new(name).unwrap(), CString::new(sig).unwrap());
f!(self, 113, unsafe extern "system" fn(EnvPtr, JClass, *const c_char, *const c_char) -> JMethodId)(self.env, c, n.as_ptr(), s.as_ptr())
}
pub unsafe fn call_static_object(&self, c: JClass, m: JMethodId, args: &[JValue]) -> JObject {
f!(self, 116, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue) -> JObject)(self.env, c, m, args.as_ptr())
}
pub unsafe fn call_static_void(&self, c: JClass, m: JMethodId, args: &[JValue]) {
f!(self, 143, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue))(self.env, c, m, args.as_ptr())
}
pub unsafe fn new_string(&self, s: &str) -> JObject {
let c = CString::new(s).unwrap();
f!(self, 167, unsafe extern "system" fn(EnvPtr, *const c_char) -> JObject)(self.env, c.as_ptr())
}
pub unsafe fn get_string_utf(&self, s: JObject) -> String {
let chars = f!(self, 169, unsafe extern "system" fn(EnvPtr, JObject, *mut JBool) -> *const c_char)(self.env, s, std::ptr::null_mut());
if chars.is_null() {
return String::new();
}
let out = CStr::from_ptr(chars).to_string_lossy().into_owned();
f!(self, 170, unsafe extern "system" fn(EnvPtr, JObject, *const c_char))(self.env, s, chars);
out
}
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 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)
}
pub unsafe fn new_byte_array(&self, len: JInt) -> JObject {
f!(self, 176, unsafe extern "system" fn(EnvPtr, JInt) -> JObject)(self.env, len)
}
pub unsafe fn get_byte_array_region(&self, a: JObject, start: JInt, len: JInt, buf: *mut u8) {
f!(self, 200, unsafe extern "system" fn(EnvPtr, JObject, JInt, JInt, *mut u8))(self.env, a, start, len, buf)
}
pub unsafe fn set_byte_array_region(&self, a: JObject, start: JInt, len: JInt, buf: *const u8) {
f!(self, 208, unsafe extern "system" fn(EnvPtr, JObject, JInt, JInt, *const u8))(self.env, a, start, len, buf)
}
pub unsafe fn register_natives(&self, c: JClass, methods: &[NativeMethod]) -> JInt {
f!(self, 215, unsafe extern "system" fn(EnvPtr, JClass, *const NativeMethod, JInt) -> JInt)(self.env, c, methods.as_ptr(), methods.len() as JInt)
}
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> {
let ex = self.exception_occurred();
if ex.is_null() {
return None;
}
self.exception_clear();
let cls = self.get_object_class(ex);
let to_string = self.get_method_id(cls, "toString", "()Ljava/lang/String;");
let text = if to_string.is_null() {
"<exception>".to_string()
} else {
let s = self.call_object(ex, to_string, &[]);
self.exception_clear();
if s.is_null() { "<exception>".to_string() } else { self.get_string_utf(s) }
};
Some(text)
}
/// Converts a pending Java exception into an Err carrying `context`.
pub unsafe fn check(&self, context: &str) -> Result<(), String> {
match self.take_exception() {
Some(e) => Err(format!("{context}: {e}")),
None => Ok(()),
}
}
}
/// JavaVM*: pointer to a pointer to the invoke interface.
pub type VmPtr = *mut *const *const c_void;
pub unsafe fn attach_current_thread(vm: VmPtr) -> Result<EnvPtr, String> {
let attach = std::mem::transmute::<*const c_void, unsafe extern "system" fn(VmPtr, *mut EnvPtr, *mut c_void) -> JInt>(*(*vm).add(4));
let mut env: EnvPtr = std::ptr::null_mut();
let rc = attach(vm, &mut env, std::ptr::null_mut());
if rc != JNI_OK || env.is_null() {
return Err(format!("AttachCurrentThread failed ({rc})"));
}
Ok(env)
}
pub unsafe fn get_jvmti(vm: VmPtr) -> Result<JvmtiPtr, String> {
let get_env = std::mem::transmute::<*const c_void, unsafe extern "system" fn(VmPtr, *mut *mut c_void, JInt) -> JInt>(*(*vm).add(6));
let mut out: *mut c_void = std::ptr::null_mut();
let rc = get_env(vm, &mut out, JVMTI_VERSION_1_2);
if rc != JNI_OK || out.is_null() {
return Err(format!("GetEnv(JVMTI) failed ({rc})"));
}
Ok(out as JvmtiPtr)
}
/// jvmtiEnv*: pointer to a pointer to the JVMTI function table (entry n of the spec is index n-1).
pub type JvmtiPtr = *mut *const *const c_void;
#[repr(C)]
pub struct ClassDefinition {
pub klass: JClass,
pub class_byte_count: JInt,
pub class_bytes: *const u8,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
pub struct Capabilities {
pub bits: [u32; 4],
}
impl Capabilities {
pub fn set(&mut self, bit: u32) {
self.bits[(bit / 32) as usize] |= 1 << (bit % 32);
}
}
// capability bit positions, counted in jvmtiCapabilities declaration order
pub const CAN_REDEFINE_CLASSES: u32 = 9;
pub const CAN_REDEFINE_ANY_CLASS: u32 = 21;
pub const CAN_GENERATE_ALL_CLASS_HOOK_EVENTS: u32 = 26;
pub const CAN_RETRANSFORM_CLASSES: u32 = 37;
pub const CAN_RETRANSFORM_ANY_CLASS: u32 = 38;
pub const EVENT_CLASS_FILE_LOAD_HOOK: JInt = 54;
pub type ClassFileLoadHook = unsafe extern "system" fn(
JvmtiPtr,
EnvPtr,
JClass,
JObject,
*const c_char,
JObject,
JInt,
*const u8,
*mut JInt,
*mut *mut u8,
);
pub unsafe fn add_capabilities(j: JvmtiPtr, caps: &Capabilities) -> JInt {
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const Capabilities) -> JInt>(*(*j).add(141));
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> {
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));
let rc = set_callbacks(j, callbacks.as_ptr(), std::mem::size_of_val(&callbacks) as JInt);
if rc != 0 {
return Err(format!("SetEventCallbacks failed ({rc})"));
}
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());
if rc != 0 {
return Err(format!("SetEventNotificationMode failed ({rc})"));
}
Ok(())
}
pub unsafe fn retransform_class(j: JvmtiPtr, class: 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)
}
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const ClassDefinition) -> JInt>(*(*j).add(86));
f(j, 1, def)
}
+32
View File
@@ -0,0 +1,32 @@
//! prestige_native.dll: loaded into a running Minecraft 1.21.11 (Fabric) JVM by PrestigeInjector.exe.
//!
//! The DLL worker waits for the JVM, appends the embedded Prestige payload jar to Fabric's Knot class loader, registers the
//! JVMTI-backed natives of `dev.zprestige.prestige.inject.NativeBridge` and starts `Bootstrap`, which transforms the already
//! loaded game classes and initialises the client.
mod bridge;
mod jni;
mod log;
mod worker;
use std::ffi::c_void;
const DLL_PROCESS_ATTACH: u32 = 1;
extern "system" {
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
}
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
#[no_mangle]
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
if reason == DLL_PROCESS_ATTACH {
unsafe {
MODULE = module;
DisableThreadLibraryCalls(module);
}
// Nothing but thread creation may happen under the loader lock.
let _ = std::thread::Builder::new().name("prestige-loader".into()).spawn(worker::run);
}
1
}
+33
View File
@@ -0,0 +1,33 @@
use std::ffi::c_void;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
extern "system" {
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
}
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")
})
}
/// Appends a line to prestige-native.log beside the DLL.
pub fn log(message: &str) {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) {
let _ = writeln!(file, "[{secs}] {message}");
}
}
#[macro_export]
macro_rules! log {
($($arg:tt)*) => { $crate::log::log(&format!($($arg)*)) };
}
+175
View File
@@ -0,0 +1,175 @@
//! 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};
use crate::jni::*;
use crate::log;
use std::ffi::{c_char, c_void};
use std::path::PathBuf;
use std::time::Duration;
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"));
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() });
match unsafe { install() } {
Ok(()) => log!("bootstrap returned"),
Err(e) => log!("FAILED: {e}"),
}
}
unsafe fn install() -> Result<(), String> {
let vm = find_vm()?;
let env = attach_current_thread(vm)?;
let jni = Jni::new(env);
log!("attached to the JVM");
init_jvmti(vm)?;
log!("JVMTI ready");
let jar = write_payload()?;
log!("payload written to {}", jar.display());
let loader = add_to_knot(&jni, &jar)?;
log!("payload appended to the Fabric Knot class loader");
register_natives(&jni, loader)?;
log!("natives registered");
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader)?;
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.
jni.call_static_void(bootstrap, start, &[]);
jni.check("Bootstrap.start")?;
Ok(())
}
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
unsafe fn find_vm() -> Result<VmPtr, String> {
let mut module = std::ptr::null_mut();
for _ in 0..600 {
module = GetModuleHandleA(c"jvm.dll".as_ptr());
if !module.is_null() {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
if module.is_null() {
return Err("jvm.dll is not loaded in this process".into());
}
let get_vms = GetProcAddress(module, c"JNI_GetCreatedJavaVMs".as_ptr());
if get_vms.is_null() {
return Err("JNI_GetCreatedJavaVMs export not found".into());
}
let get_vms: unsafe extern "system" fn(*mut VmPtr, JInt, *mut JInt) -> JInt = std::mem::transmute(get_vms);
for _ in 0..600 {
let mut vm: VmPtr = std::ptr::null_mut();
let mut count: JInt = 0;
if get_vms(&mut vm, 1, &mut count) == JNI_OK && count > 0 && !vm.is_null() {
return Ok(vm);
}
std::thread::sleep(Duration::from_millis(100));
}
Err("no JavaVM was created".into())
}
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() }));
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
Ok(path)
}
/// 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?)")?;
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, &[]);
jni.check("getLauncher()")?;
if launcher.is_null() {
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 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;");
jni.check("FabricLauncher.getTargetClassLoader")?;
// Path path = new java.io.File(jar).toPath()
let file_class = jni.find_class("java/io/File");
let file_ctor = jni.get_method_id(file_class, "<init>", "(Ljava/lang/String;)V");
let to_path = jni.get_method_id(file_class, "toPath", "()Ljava/nio/file/Path;");
jni.check("java.io.File")?;
let jar_string = jni.new_string(&jar.to_string_lossy());
let file = jni.new_object(file_class, file_ctor, &[obj(jar_string)]);
let path = jni.call_object(file, to_path, &[]);
jni.check("File.toPath")?;
let string_class = jni.find_class("java/lang/String");
let prefixes = jni.new_object_array(0, string_class, std::ptr::null_mut());
jni.call_void(launcher, add, &[obj(path), obj(prefixes)]);
jni.check("addToClassPath")?;
let loader = jni.call_object(launcher, target, &[]);
jni.check("getTargetClassLoader()")?;
if loader.is_null() {
return Err("Knot target class loader is null".into());
}
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> {
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)]);
jni.check(&format!("load {name}"))?;
if class.is_null() {
return Err(format!("{name} not found"));
}
Ok(class)
}
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
let class = class_for_name(jni, NATIVE_BRIDGE, loader)?;
let methods = [
NativeMethod {
name: c"classBytes".as_ptr(),
signature: c"(Ljava/lang/Class;)[B".as_ptr(),
fn_ptr: native_class_bytes as *const () as usize as *mut c_void,
},
NativeMethod {
name: c"redefine".as_ptr(),
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
fn_ptr: native_redefine as *const () as usize as *mut c_void,
},
NativeMethod {
name: c"log".as_ptr(),
signature: c"(Ljava/lang/String;)V".as_ptr(),
fn_ptr: native_log as *const () as usize as *mut c_void,
},
];
let rc = jni.register_natives(class, &methods);
if rc != JNI_OK {
let _ = jni.take_exception();
return Err(format!("RegisterNatives failed ({rc})"));
}
Ok(())
}