Guide sa pag-develop ng plugin
Tutulungan ka ng guide na ito sa paggawa ng mga plugins para sa Hachimi Edge. Ang mga plugin ay isang dynamic library na nagpapalawak ng functionality ng Hachimi sa pamamagitan ng well-defined na C-compaible API
Choice sa programming language
Maaaring gawin ang mga plugin gamit ang anumang language na makaka-produce ng C-compatible dynamic library (.so sa Android, .dll sa Windows). Kasama dito ang:
- Rust (inirerekomenda - ginagamit dito ang Rust sa mga example)
- C/C++
- Zig
- Go (gamit ang cgo)
- Anumang language na may support sa C FFI
- Assembly (Kung isa ka talagang masokista)
Ginagamit ng guide na ito ang Rust dahil ginawa mismo ang Hachimi Edge gamit ang Rust. Gayunpaman, ang API ay C compatible, kaya maaari mong gamitin ang anumang programming language na gusto mo. Siguraduhin lamang na ang iyong hachimi_init function ay nai-export gamit ang C calling convention.
Mga kinakailangan
Bago ka makapagsimula, dapat may:
- May experience ka sa pinili mong programming language.
- May kaalaman ka sa istraktura ng laro.
- May naka-install na development toolchain para sa target platform.
Babala
Huwag lumikha ng mga malisyosong plugin na nagnanakaw ng data o nakakasama sa ibang players.
Istraktura ng plugin
Entry point
Dapat mag-export ng hachimi_init na function ang bawat plugin. Gumawa ng Cargo.toml:
toml
[package]
name = "hachimi_myplugin"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]At sa src/lib.rs:
rust
use std::ffi::{c_char, c_void};
#[repr(C)]
pub struct Vtable {
// Function pointers (tignan ang API Reference sa ibaba)
}
#[repr(i32)]
pub enum InitResult {
Error = 0,
Ok = 1,
}
static mut VTABLE: Option<&'static Vtable> = None;
#[no_mangle]
pub extern "C" fn hachimi_init(vtable: *const Vtable, version: i32) -> InitResult {
if vtable.is_null() {
return InitResult::Error;
}
if version < 2 {
return InitResult::Error;
}
unsafe {
VTABLE = Some(&*vtable);
}
// I-initialize ang iyong plugin dito
InitResult::Ok
}Ang vtable
Ang vtable ay isang structure na naglalaman ng function pointers sa APi ng Hachimi. Nakukuha mo ito sa hachimi_init at iimbak ito para sa paggamit sa plugin.
Kasalukuyang API Version: 2
Palaging suriin ang version parameter para siguraduhin ang compatibility:
rust
#[no_mangle]
pub extern "C" fn hachimi_init(vtable: *const Vtable, version: i32) -> InitResult {
if vtable.is_null() {
return InitResult::Error;
}
if version < 2 {
// Masyadong luma ang API
return InitResult::Error;
}
// Store vtable safely
unsafe {
VTABLE = Some(&*vtable);
}
InitResult::Ok
}API reference
Hachimi instance
rust
use std::ffi::c_void;
unsafe fn get_hachimi_and_interceptor() -> (*const c_void, *const c_void) {
let vtable = VTABLE.unwrap();
let hachimi = (vtable.hachimi_instance)();
let interceptor = (vtable.hachimi_get_interceptor)(hachimi);
(hachimi, interceptor)
}Interceptor (pag-hook ng function)
Nagbibigay-daan sa iyo ang interceptor na mag-hook at i-modify ang mga function sa laro:
rust
use std::ffi::c_void;
unsafe fn hook_function(
interceptor: *const c_void,
original_addr: *mut c_void,
hook_addr: *mut c_void
) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.interceptor_hook)(interceptor, original_addr, hook_addr)
}
unsafe fn hook_vtable_entry(
interceptor: *const c_void,
vtable_ptr: *mut *mut c_void,
index: usize,
hook_addr: *mut c_void
) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.interceptor_hook_vtable)(interceptor, vtable_ptr, index, hook_addr)
}
unsafe fn get_trampoline(
interceptor: *const c_void,
hook_addr: *mut c_void
) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.interceptor_get_trampoline_addr)(interceptor, hook_addr)
}
unsafe fn unhook_function(
interceptor: *const c_void,
hook_addr: *mut c_void
) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.interceptor_unhook)(interceptor, hook_addr)
}IL2CPP functions
I-access ang IL2CPP runtime ng Unity:
rust
use std::ffi::{c_char, c_void, CStr, CString};
unsafe fn resolve_il2cpp_symbol(name: &str) -> *mut c_void {
let vtable = VTABLE.unwrap();
let name_cstr = CString::new(name).unwrap();
(vtable.il2cpp_resolve_symbol)(name_cstr.as_ptr())
}
unsafe fn resolve_il2cpp_icall(name: &str) -> *mut c_void {
let vtable = VTABLE.unwrap();
let name_cstr = CString::new(name).unwrap();
(vtable.il2cpp_resolve_icall)(name_cstr.as_ptr())
}
unsafe fn get_assembly_image(name: &str) -> *const c_void {
let vtable = VTABLE.unwrap();
let name_cstr = CString::new(name).unwrap();
(vtable.il2cpp_get_assembly_image)(name_cstr.as_ptr())
}
unsafe fn get_class(
image: *const c_void,
namespace: &str,
class_name: &str
) -> *mut c_void {
let vtable = VTABLE.unwrap();
let ns_cstr = CString::new(namespace).unwrap();
let class_cstr = CString::new(class_name).unwrap();
(vtable.il2cpp_get_class)(image, ns_cstr.as_ptr(), class_cstr.as_ptr())
}
unsafe fn get_method(
klass: *mut c_void,
method_name: &str,
arg_count: i32
) -> *const c_void {
let vtable = VTABLE.unwrap();
let method_cstr = CString::new(method_name).unwrap();
(vtable.il2cpp_get_method)(klass, method_cstr.as_ptr(), arg_count)
}
unsafe fn get_methods(klass: *mut c_void) -> impl Iterator<Item = *const c_void> {
let mut iter: *mut c_void = std::ptr::null_mut();
std::iter::from_fn(move || {
let vtable = VTABLE.unwrap();
let method = (vtable.il2cpp_class_get_methods)(klass, &mut iter);
if method.is_null() {
None
} else {
Some(method)
}
})
}
unsafe fn get_method_addr(
klass: *mut c_void,
method_name: &str,
arg_count: i32
) -> *mut c_void {
let vtable = VTABLE.unwrap();
let method_cstr = CString::new(method_name).unwrap();
(vtable.il2cpp_get_method_addr)(klass, method_cstr.as_ptr(), arg_count)
}
unsafe fn object_new(klass: *const c_void) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_object_new)(klass)
}
unsafe fn get_field(klass: *mut c_void, field_name: &str) -> *mut c_void {
let vtable = VTABLE.unwrap();
let field_cstr = CString::new(field_name).unwrap();
(vtable.il2cpp_get_field_from_name)(klass, field_cstr.as_ptr())
}
unsafe fn get_field_value<T>(
object: *mut c_void,
field: *mut c_void,
out_value: *mut T
) {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_get_field_value)(object, field, out_value as *mut c_void);
}
unsafe fn set_field_value<T>(
object: *mut c_void,
field: *mut c_void,
value: *const T
) {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_set_field_value)(object, field, value as *const c_void);
}
unsafe fn unbox(object: *mut c_void) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_unbox)(object)
}
unsafe fn get_main_thread() -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_get_main_thread)()
}
unsafe fn create_array(element_class: *mut c_void, length: usize) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_create_array)(element_class, length)
}
unsafe fn get_singleton_instance(klass: *mut c_void) -> *mut c_void {
let vtable = VTABLE.unwrap();
(vtable.il2cpp_get_singleton_like_instance)(klass)
}Logging
rust
use std::ffi::CString;
// Mga log level: 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Trace
unsafe fn log(level: i32, tag: &str, message: &str) {
let vtable = VTABLE.unwrap();
let tag_cstr = CString::new(tag).unwrap();
let msg_cstr = CString::new(message).unwrap();
(vtable.log)(level, tag_cstr.as_ptr(), msg_cstr.as_ptr());
}
// Helper functions
unsafe fn log_info(tag: &str, message: &str) {
log(3, tag, message);
}
unsafe fn log_error(tag: &str, message: &str) {
log(1, tag, message);
}
unsafe fn log_warn(tag: &str, message: &str) {
log(2, tag, message);
}Gui integration
Menu items
rust
use std::ffi::{c_char, c_void, CString};
// Mag-load ng PNG icon
const ICON_BYTES: &[u8] = include_bytes!("../icon.png");
// Callback type
type MenuCallback = extern "C" fn(userdata: *mut c_void);
extern "C" fn my_menu_callback(_userdata: *mut c_void) {
unsafe {
log_info("MyPlugin", "Menu item clicked!");
}
}
unsafe fn register_menu_item(name: &str, callback: MenuCallback) -> bool {
let vtable = VTABLE.unwrap();
let name_cstr = CString::new(name).unwrap();
(vtable.gui_register_menu_item)(
name_cstr.as_ptr(),
Some(callback),
std::ptr::null_mut()
)
}
unsafe fn register_menu_item_with_icon(
name: &str,
icon_data: &[u8],
callback: MenuCallback
) -> bool {
let vtable = VTABLE.unwrap();
let name_cstr = CString::new(name).unwrap();
if !(vtable.gui_register_menu_item)(
name_cstr.as_ptr(),
Some(callback),
std::ptr::null_mut()
) {
return false;
}
(vtable.gui_register_menu_item_icon)(
name_cstr.as_ptr(),
std::ptr::null(),
icon_data.as_ptr(),
icon_data.len()
)
}
// Usage:
// register_menu_item_with_icon("My Plugin", ICON_BYTES, my_menu_callback);Menu sections
rust
type MenuSectionCallback = extern "C" fn(ui: *mut c_void, userdata: *mut c_void);
extern "C" fn my_section_callback(ui: *mut c_void, _userdata: *mut c_void) {
unsafe {
let vtable = VTABLE.unwrap();
// Build your UI
let heading = CString::new("My Plugin Settings").unwrap();
(vtable.gui_ui_heading)(ui, heading.as_ptr());
(vtable.gui_ui_separator)(ui);
// Add widgets...
}
}
unsafe fn register_menu_section(callback: MenuSectionCallback) -> bool {
let vtable = VTABLE.unwrap();
(vtable.gui_register_menu_section)(Some(callback), std::ptr::null_mut())
}
unsafe fn register_menu_section_with_icon(
title: &str,
icon_data: &[u8],
callback: MenuSectionCallback
) -> bool {
let vtable = VTABLE.unwrap();
let title_cstr = CString::new(title).unwrap();
(vtable.gui_register_menu_section_with_icon)(
title_cstr.as_ptr(),
std::ptr::null(),
icon_data.as_ptr(),
icon_data.len(),
Some(callback),
std::ptr::null_mut()
)
}
// Usage:
// register_menu_section_with_icon("My Settings", ICON_BYTES, my_section_callback);Mga widget sa UI
Mga available na function para sa pag-build ng iyong GUI:
rust
use std::ffi::CString;
unsafe fn ui_heading(ui: *mut c_void, text: &str) {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_heading)(ui, text_cstr.as_ptr());
}
unsafe fn ui_label(ui: *mut c_void, text: &str) {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_label)(ui, text_cstr.as_ptr());
}
unsafe fn ui_small(ui: *mut c_void, text: &str) {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_small)(ui, text_cstr.as_ptr());
}
unsafe fn ui_colored_label(ui: *mut c_void, r: u8, g: u8, b: u8, a: u8, text: &str) {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_colored_label)(ui, r, g, b, a, text_cstr.as_ptr());
}
unsafe fn ui_separator(ui: *mut c_void) {
let vtable = VTABLE.unwrap();
(vtable.gui_ui_separator)(ui);
}
unsafe fn ui_button(ui: *mut c_void, text: &str) -> bool {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_button)(ui, text_cstr.as_ptr())
}
unsafe fn ui_small_button(ui: *mut c_void, text: &str) -> bool {
let vtable = VTABLE.unwrap();
let text_cstr = CString::new(text).unwrap();
(vtable.gui_ui_small_button)(ui, text_cstr.as_ptr())
}
unsafe fn ui_checkbox(ui: *mut c_void, label: &str, value: &mut bool) -> bool {
let vtable = VTABLE.unwrap();
let label_cstr = CString::new(label).unwrap();
(vtable.gui_ui_checkbox)(ui, label_cstr.as_ptr(), value as *mut bool)
}
unsafe fn ui_text_edit(ui: *mut c_void, buffer: &mut [u8]) -> bool {
let vtable = VTABLE.unwrap();
(vtable.gui_ui_text_edit_singleline)(
ui,
buffer.as_mut_ptr() as *mut c_char,
buffer.len()
)
}Mga abiso
rust
unsafe fn show_notification(message: &str) -> bool {
let vtable = VTABLE.unwrap();
let msg_cstr = CString::new(message).unwrap();
(vtable.gui_show_notification)(msg_cstr.as_ptr())
}Pag-load ng DEX sa Android (api v2+)
Mag-load at mag-execute ng Java/Kotlin code sa Android:
rust
unsafe fn load_dex(dex_data: &[u8], class_name: &str) -> u64 {
let vtable = VTABLE.unwrap();
let class_cstr = CString::new(class_name).unwrap();
(vtable.android_dex_load)(
dex_data.as_ptr(),
dex_data.len(),
class_cstr.as_ptr()
)
}
unsafe fn dex_call_static_noargs(handle: u64, method: &str, signature: &str) -> bool {
let vtable = VTABLE.unwrap();
let method_cstr = CString::new(method).unwrap();
let sig_cstr = CString::new(signature).unwrap();
(vtable.android_dex_call_static_noargs)(
handle,
method_cstr.as_ptr(),
sig_cstr.as_ptr()
)
}
unsafe fn dex_call_static_string(
handle: u64,
method: &str,
signature: &str,
arg: &str
) -> bool {
let vtable = VTABLE.unwrap();
let method_cstr = CString::new(method).unwrap();
let sig_cstr = CString::new(signature).unwrap();
let arg_cstr = CString::new(arg).unwrap();
(vtable.android_dex_call_static_string)(
handle,
method_cstr.as_ptr(),
sig_cstr.as_ptr(),
arg_cstr.as_ptr()
)
}
unsafe fn unload_dex(handle: u64) -> bool {
let vtable = VTABLE.unwrap();
(vtable.android_dex_unload)(handle)
}Java example (DEX side)
Dapat mag-expose ng static methods ang iyong Java/Kotlin code na tumtugma sa signatures na kino-call mo mula sa Rust. Ito ay isang minimal na Java example na maaari mong i-compile sa isang DEX at i-load nang may mga helper sa itaas:
java
package dev.hachimi;
import android.util.Log;
public class DexExample {
private static final String TAG = "HachimiDexExample";
public static void hello() {
Log.i(TAG, "Hello from DEX");
}
public static void setVisibleString(String value) {
Log.i(TAG, "setVisibleString: " + value);
}
}Kapag ilo-load, gamitin ang fully-qualified class name:
class_name:"dev.hachimi.DexExample"hello()signature:"()V"setVisibleString(String)signature:"(Ljava/lang/String;)V"
I-build ang DEX
bash
ANDROID_JAR=~/Android/Sdk/platforms/android-34/android.jar
rm -rf /tmp/hachimi_dex
mkdir -p /tmp/hachimi_dex/classes
javac -source 1.8 -target 1.8 \
-classpath "$ANDROID_JAR" \
-d /tmp/hachimi_dex/classes \
java/DexExample.java
jar cf /tmp/hachimi_dex/classes.jar -C /tmp/hachimi_dex/classes .
d8 --lib "$ANDROID_JAR" \
--output /tmp/hachimi_dex/out \
/tmp/hachimi_dex/classes.jar
cp /tmp/hachimi_dex/out/classes.dex assets/dex_example.dexAt i-load ito mula sa Rust:
rust
let dex_bytes = include_bytes!("../assets/dex_example.dex");
let handle = load_dex(dex_bytes, "dev.hachimi.DexExample");
dex_call_static_noargs(handle, "hello", "()V");
dex_call_static_string(handle, "setVisibleString", "(Ljava/lang/String;)V", "true");Kumpletong example plugin
Ito ang gumagana at kumpletong halimbawa:
rust
use std::ffi::{c_char, c_void, CString};
use std::sync::Once;
#[repr(i32)]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum InitResult {
Error = 0,
Ok = 1,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct Vtable {
pub hachimi_instance: unsafe extern "C" fn() -> *const c_void,
pub hachimi_get_interceptor: unsafe extern "C" fn(this: *const c_void) -> *const c_void,
pub interceptor_hook: unsafe extern "C" fn(
this: *const c_void,
orig_addr: *mut c_void,
hook_addr: *mut c_void,
) -> *mut c_void,
pub interceptor_hook_vtable: unsafe extern "C" fn(
this: *const c_void,
vtable: *mut *mut c_void,
vtable_index: usize,
hook_addr: *mut c_void,
) -> *mut c_void,
pub interceptor_get_trampoline_addr:
unsafe extern "C" fn(this: *const c_void, hook_addr: *mut c_void) -> *mut c_void,
pub interceptor_unhook:
unsafe extern "C" fn(this: *const c_void, hook_addr: *mut c_void) -> *mut c_void,
pub il2cpp_resolve_symbol: unsafe extern "C" fn(name: *const c_char) -> *mut c_void,
pub il2cpp_get_assembly_image:
unsafe extern "C" fn(assembly_name: *const c_char) -> *const c_void,
pub il2cpp_get_class: unsafe extern "C" fn(
image: *const c_void,
namespace: *const c_char,
class_name: *const c_char,
) -> *mut c_void,
pub il2cpp_get_method: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
args_count: i32,
) -> *const c_void,
pub il2cpp_get_method_overload: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
params: *const c_void,
param_count: usize,
) -> *const c_void,
pub il2cpp_get_method_addr: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
args_count: i32,
) -> *mut c_void,
pub il2cpp_get_method_overload_addr: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
params: *const c_void,
param_count: usize,
) -> *mut c_void,
pub il2cpp_get_method_cached: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
args_count: i32,
) -> *const c_void,
pub il2cpp_get_method_addr_cached: unsafe extern "C" fn(
class: *mut c_void,
name: *const c_char,
args_count: i32,
) -> *mut c_void,
pub il2cpp_find_nested_class:
unsafe extern "C" fn(class: *mut c_void, name: *const c_char) -> *mut c_void,
pub il2cpp_resolve_icall:
unsafe extern "C" fn(name: *const c_char) -> *mut c_void,
pub il2cpp_class_get_methods:
unsafe extern "C" fn(klass: *mut c_void, iter: *mut *mut c_void) -> *const c_void,
pub il2cpp_get_field_from_name:
unsafe extern "C" fn(class: *mut c_void, name: *const c_char) -> *mut c_void,
pub il2cpp_get_field_value: unsafe extern "C" fn(
obj: *mut c_void,
field: *mut c_void,
out_value: *mut c_void,
),
pub il2cpp_set_field_value: unsafe extern "C" fn(
obj: *mut c_void,
field: *mut c_void,
value: *const c_void,
),
pub il2cpp_get_static_field_value:
unsafe extern "C" fn(field: *mut c_void, out_value: *mut c_void),
pub il2cpp_set_static_field_value:
unsafe extern "C" fn(field: *mut c_void, value: *const c_void),
pub il2cpp_object_new: unsafe extern "C" fn(klass: *const c_void) -> *mut c_void,
pub il2cpp_unbox: unsafe extern "C" fn(obj: *mut c_void) -> *mut c_void,
pub il2cpp_get_main_thread: unsafe extern "C" fn() -> *mut c_void,
pub il2cpp_get_attached_threads:
unsafe extern "C" fn(out_size: *mut usize) -> *mut *mut c_void,
pub il2cpp_schedule_on_thread:
unsafe extern "C" fn(thread: *mut c_void, callback: unsafe extern "C" fn()),
pub il2cpp_create_array:
unsafe extern "C" fn(element_type: *mut c_void, length: usize) -> *mut c_void,
pub il2cpp_get_singleton_like_instance:
unsafe extern "C" fn(class: *mut c_void) -> *mut c_void,
pub log: unsafe extern "C" fn(level: i32, target: *const c_char, message: *const c_char),
pub gui_register_menu_item: unsafe extern "C" fn(
label: *const c_char,
callback: Option<extern "C" fn(*mut c_void)>,
userdata: *mut c_void,
) -> bool,
pub gui_register_menu_section: unsafe extern "C" fn(
callback: Option<extern "C" fn(*mut c_void, *mut c_void)>,
userdata: *mut c_void,
) -> bool,
pub gui_show_notification: unsafe extern "C" fn(message: *const c_char) -> bool,
pub gui_ui_heading: unsafe extern "C" fn(ui: *mut c_void, text: *const c_char) -> bool,
pub gui_ui_label: unsafe extern "C" fn(ui: *mut c_void, text: *const c_char) -> bool,
pub gui_ui_small: unsafe extern "C" fn(ui: *mut c_void, text: *const c_char) -> bool,
pub gui_ui_separator: unsafe extern "C" fn(ui: *mut c_void) -> bool,
pub gui_ui_button: unsafe extern "C" fn(ui: *mut c_void, text: *const c_char) -> bool,
pub gui_ui_small_button: unsafe extern "C" fn(ui: *mut c_void, text: *const c_char) -> bool,
pub gui_ui_checkbox:
unsafe extern "C" fn(ui: *mut c_void, text: *const c_char, value: *mut bool) -> bool,
pub gui_ui_text_edit_singleline: unsafe extern "C" fn(
ui: *mut c_void,
buffer: *mut c_char,
buffer_len: usize,
) -> bool,
pub gui_ui_horizontal: unsafe extern "C" fn(
ui: *mut c_void,
callback: Option<extern "C" fn(*mut c_void, *mut c_void)>,
userdata: *mut c_void,
) -> bool,
pub gui_ui_grid: unsafe extern "C" fn(
ui: *mut c_void,
id: *const c_char,
columns: usize,
spacing_x: f32,
spacing_y: f32,
callback: Option<extern "C" fn(*mut c_void, *mut c_void)>,
userdata: *mut c_void,
) -> bool,
pub gui_ui_end_row: unsafe extern "C" fn(ui: *mut c_void) -> bool,
pub gui_ui_colored_label: unsafe extern "C" fn(
ui: *mut c_void,
r: u8,
g: u8,
b: u8,
a: u8,
text: *const c_char,
) -> bool,
pub gui_register_menu_item_icon: unsafe extern "C" fn(
label: *const c_char,
icon_uri: *const c_char,
icon_ptr: *const u8,
icon_len: usize,
) -> bool,
pub gui_register_menu_section_with_icon: unsafe extern "C" fn(
title: *const c_char,
icon_uri: *const c_char,
icon_ptr: *const u8,
icon_len: usize,
callback: Option<extern "C" fn(*mut c_void, *mut c_void)>,
userdata: *mut c_void,
) -> bool,
pub android_dex_load: unsafe extern "C" fn(
dex_ptr: *const u8,
dex_len: usize,
class_name: *const c_char,
) -> u64,
pub android_dex_unload: unsafe extern "C" fn(handle: u64) -> bool,
pub android_dex_call_static_noargs:
unsafe extern "C" fn(handle: u64, method: *const c_char, sig: *const c_char) -> bool,
pub android_dex_call_static_string: unsafe extern "C" fn(
handle: u64,
method: *const c_char,
sig: *const c_char,
arg: *const c_char,
) -> bool,
}
static INIT: Once = Once::new();
static mut VTABLE_PTR: *const Vtable = std::ptr::null();
static mut API_VERSION: i32 = 0;
extern "C" fn on_menu_click(_userdata: *mut c_void) {
unsafe {
let vtable = VTABLE_PTR.as_ref();
if let Some(vtable) = vtable {
let message = CString::new("Hello from the example plugin!").unwrap();
(vtable.gui_show_notification)(message.as_ptr());
}
}
}
extern "C" fn on_menu_section(ui: *mut c_void, _userdata: *mut c_void) {
unsafe {
let vtable = VTABLE_PTR.as_ref();
if let Some(vtable) = vtable {
let heading = CString::new("Example Plugin").unwrap();
(vtable.gui_ui_heading)(ui, heading.as_ptr());
(vtable.gui_ui_separator)(ui);
let label = CString::new("This section is rendered by the plugin.").unwrap();
(vtable.gui_ui_label)(ui, label.as_ptr());
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn hachimi_init(vtable: *const Vtable, version: i32) -> InitResult {
if vtable.is_null() || version < 2 {
return InitResult::Error;
}
unsafe {
VTABLE_PTR = vtable;
API_VERSION = version;
}
INIT.call_once(|| unsafe {
let vtable = &*vtable;
let title = CString::new("Example Plugin").unwrap();
(vtable.gui_register_menu_item)(title.as_ptr(), Some(on_menu_click), std::ptr::null_mut());
(vtable.gui_register_menu_section)(Some(on_menu_section), std::ptr::null_mut());
});
InitResult::Ok
}Best practices
- Pagsusuri ng version: Palaging suriin ang API version sa
hachimi_init - Pag-handle ng error: I-return ang
InitResult::Errorkapag mabigo ang initialization - Logging: Gamitin ang logging API para sa pag-debug at user feedback.
Mga halimbawa sa pag-hook at il2cpp
Pag-hook ng game functions
Halimbawa ng pag-hook ng mga game function:
rust
type UpdateFunc = unsafe extern "C" fn(*mut c_void);
unsafe extern "C" fn my_update_hook(this: *mut c_void) {
if let Some(vtable) = VTABLE {
let (_, interceptor) = get_hachimi_and_interceptor();
// Get original function
let trampoline = (vtable.interceptor_get_trampoline_addr)(
interceptor,
my_update_hook as *mut c_void
);
let original: UpdateFunc = std::mem::transmute(trampoline);
// Do something before
let tag = CString::new("MyPlugin").unwrap();
let msg = CString::new("Update called").unwrap();
(vtable.log)(4, tag.as_ptr(), msg.as_ptr());
// Call original
original(this);
// Do something after
}
}
// In hachimi_init (real example used in Hachimi):
// UnityEngine.Texture2D.ReadPixels (args = 3)
unsafe {
let vtable = VTABLE.unwrap();
let (_, interceptor) = get_hachimi_and_interceptor();
let image_name = CString::new("UnityEngine.CoreModule").unwrap();
let image = (vtable.il2cpp_get_assembly_image)(image_name.as_ptr());
let namespace = CString::new("UnityEngine").unwrap();
let class_name = CString::new("Texture2D").unwrap();
let klass = (vtable.il2cpp_get_class)(image, namespace.as_ptr(), class_name.as_ptr());
let method_name = CString::new("ReadPixels").unwrap();
let readpixels_addr = (vtable.il2cpp_get_method_addr)(klass, method_name.as_ptr(), 3);
(vtable.interceptor_hook)(
interceptor,
readpixels_addr,
my_update_hook as *mut c_void
);
}Paggalaw sa mga il2cpp objects
rust
unsafe fn example_il2cpp_usage() {
if let Some(vtable) = VTABLE {
// Get a class
let image_name = CString::new("UnityEngine.CoreModule").unwrap();
let image = (vtable.il2cpp_get_assembly_image)(image_name.as_ptr());
let namespace = CString::new("UnityEngine").unwrap();
let class_name = CString::new("Object").unwrap();
let klass = (vtable.il2cpp_get_class)(image, namespace.as_ptr(), class_name.as_ptr());
if klass.is_null() {
return;
}
// Example: get a singleton-like instance (if available)
let instance = (vtable.il2cpp_get_singleton_like_instance)(klass);
if instance.is_null() {
return;
}
// Example: instantiate a new object
let new_obj = object_new(klass);
// Example: resolve a method address
let method_name = CString::new("get_name").unwrap();
let method_addr = (vtable.il2cpp_get_method_addr)(klass, method_name.as_ptr(), 0);
if method_addr.is_null() {
return;
}
// Example: find a specific method overload by index
// This is useful when multiple methods have the same name
if let Some(method_info) = get_methods(klass).nth(2) {
// The first field of MethodInfo is the actual function pointer
let specific_addr = *(method_info as *const *mut c_void);
}
// Cast method_addr to a function pointer if you intend to call it
}
}