Repository navigation
Expand file tree
/
Copy pathhook.rs
More file actions
335 lines (310 loc) · 12.9 KB
/
Copy pathhook.rs
File metadata and controls
335 lines (310 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//! HarmonyLib wrapper. Each `Hook` represents one prefix/postfix
//! patch on a managed method. The Rust callback is an `extern
//! "C" fn` pointer; the shim wraps it in a managed delegate so
//! HarmonyLib can target it.
//!
//! K8s slot: Def=HookDef, Registry=HookRegistry
//! (HOOK_REGISTRY singleton),
//! Instance=PatchHandle, Controller=trampoline + Drop
use std::ffi::{CString, c_void};
use crate::bridge::{self, PatchHandle};
/// One installed Harmony patch. Its unpatch is recorded on
/// modforge's shutdown registry when the patch is made: dropping
/// the `Hook` unpatches now, [`Hook::keep`] leaves the patch on
/// until the generation shuts down, and a `Hook` held in a static
/// is still unpatched at shutdown.
pub struct Hook {
handle: PatchHandle,
pub class_name: String,
pub method_name: String,
pub when: HookWhen,
undo: modforge::shutdown::Undo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookWhen {
Prefix,
Postfix,
}
impl Hook {
/// The one place a `Hook` is made: right after the shim
/// returned a live patch handle, record its unpatch.
fn new(handle: PatchHandle, class_name: &str, method_name: &str, when: HookWhen) -> Hook {
let undo = modforge::shutdown::SHUTDOWN_REGISTRY.record(
"hook",
format!("{class_name}.{method_name}"),
100,
move || {
if let Some(bridge) = bridge::get() {
(bridge.harmony_unpatch)(handle);
}
},
);
Hook {
handle,
class_name: class_name.to_string(),
method_name: method_name.to_string(),
when,
undo,
}
}
pub fn handle(&self) -> PatchHandle {
self.handle
}
/// Leave the patch on until the generation shuts down.
pub fn keep(self) {
self.undo.keep();
}
}
/// Install a prefix patch. The trampoline must be an `extern "C"
/// fn(*const c_void) -> i32`. Returning non-zero from a prefix
/// tells HarmonyLib to skip the original method.
pub fn patch_prefix(
class_name: &str,
method_name: &str,
prefix_fn: extern "C" fn(*const c_void) -> i32,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let handle = (bridge.harmony_patch_prefix)(c_class.as_ptr(), c_method.as_ptr(), prefix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_prefix({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Prefix))
}
/// Which object a `patch_prefix_ctx` callback receives as its
/// context handle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookCtx {
/// `__instance` of the patched (instance) method.
Instance = 0,
/// The method's first argument, REFERENCE type only. For a
/// value-type first argument the shim refuses (a boxed copy
/// would be handed over and every mutation silently lost;
/// live-verified 2026-07-04 on the struct Injury). Use
/// `Args0` for value types.
Arg0 = 1,
/// The method's first argument via Harmony's `__args` array,
/// which writes element changes back to the real arguments
/// after the patch. Works for value types (mutating the boxed
/// element's fields lands in the real argument on write-back).
/// Needs Harmony 2.1+ (`__args`); the survivalist shim embeds
/// 2.4.2.
Args0 = 2,
}
/// Install a prefix patch whose callback receives a context
/// object (per `ctx`) as a FRESH handle in the `*const c_void`
/// parameter: cast the pointer value to i32 and wrap with
/// `MonoObject::from_handle` (Drop releases the handle; the
/// callback owns it). 0 means the context object was null.
/// Returning non-zero still skips the original method.
pub fn patch_prefix_ctx(
class_name: &str,
method_name: &str,
ctx: HookCtx,
prefix_fn: extern "C" fn(*const c_void) -> i32,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let handle = (bridge.harmony_patch_prefix_ctx)(
c_class.as_ptr(),
c_method.as_ptr(),
ctx as i32,
prefix_fn,
);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_prefix_ctx({class_name}, {method_name}, {ctx:?}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Prefix))
}
/// Install a prefix patch whose callback receives BOTH the
/// patched method's `__instance` (a FRESH handle in the first
/// parameter, same ownership contract as `patch_prefix_ctx`;
/// 0 for a static method) and the call's arguments serialized
/// to JSON UTF-8 in the second (Harmony's `__args`: primitives,
/// enums as numbers, strings by value; other objects as
/// `{"handle": n}` handles the callback also owns). The JSON
/// pointer is only valid for the duration of the callback.
/// Returning non-zero skips the original method. Needs a v7+
/// shim.
pub fn patch_prefix_instance_args(
class_name: &str,
method_name: &str,
prefix_fn: extern "C" fn(*const c_void, *const std::os::raw::c_char) -> i32,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let patch = bridge.harmony_patch_prefix_instance_args.ok_or_else(|| {
"harmony_patch_prefix_instance_args: shim is pre-v7; rebuild and redeploy the C# shim"
.to_string()
})?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let handle = patch(c_class.as_ptr(), c_method.as_ptr(), prefix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_prefix_instance_args({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Prefix))
}
/// Install a postfix patch on a method returning `float` that can
/// replace its result. The callback receives the `__instance` as
/// a FRESH handle (same ownership contract as `patch_prefix_ctx`;
/// 0 for a static method), the arguments as JSON (same convention
/// and ownership as `patch_prefix_instance_args`; the pointer is
/// valid only during the callback), and the original result, and
/// returns the result the caller sees. Needs a v8+ shim.
pub fn patch_postfix_float_result(
class_name: &str,
method_name: &str,
postfix_fn: extern "C" fn(*const c_void, *const std::os::raw::c_char, f32) -> f32,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let patch = bridge.harmony_patch_postfix_float_result.ok_or_else(|| {
"harmony_patch_postfix_float_result: shim is pre-v8; rebuild and redeploy the C# shim"
.to_string()
})?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let handle = patch(c_class.as_ptr(), c_method.as_ptr(), postfix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_postfix_float_result({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Postfix))
}
/// Install a postfix patch on a method returning `int` that can
/// replace its result, same callback contract as
/// [`patch_postfix_float_result`]. `filter` is a JSON object
/// `{"<arg index>": value}` (number for enum or integer arguments,
/// bool for bool ones) that the shim checks before calling Rust;
/// the callback runs only when every listed argument matches. Use
/// it on hot methods. Needs a v9+ shim.
pub fn patch_postfix_int_result(
class_name: &str,
method_name: &str,
filter: &serde_json::Value,
postfix_fn: extern "C" fn(*const c_void, *const std::os::raw::c_char, i32) -> i32,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let patch = bridge.harmony_patch_postfix_int_result.ok_or_else(|| {
"harmony_patch_postfix_int_result: shim is pre-v9; rebuild and redeploy the C# shim"
.to_string()
})?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let c_filter = CString::new(filter.to_string()).map_err(|e| format!("bad filter: {e}"))?;
let handle = patch(c_class.as_ptr(), c_method.as_ptr(), c_filter.as_ptr(), postfix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_postfix_int_result({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Postfix))
}
/// Install a postfix patch on a method of ANY return type that can
/// replace its result. The callback receives the `__instance` as a
/// FRESH handle (same ownership contract as `patch_prefix_ctx`; 0 for
/// a static method), the arguments and the original result as JSON
/// (the args convention of `patch_prefix_instance_args`; handles in
/// them are owned by the callback; valid only during the callback),
/// and an output buffer. Write the replacement result there as JSON
/// (`{"$handle": n}` for a live object) with [`write_result`] and
/// return what it returns, or return -1 to keep the original. The
/// `filter` is the same as [`patch_postfix_int_result`]'s. Needs a
/// v10+ shim.
pub fn patch_postfix_result(
class_name: &str,
method_name: &str,
filter: &serde_json::Value,
postfix_fn: bridge::PostfixResultFn,
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let patch = bridge.harmony_patch_postfix_result.ok_or_else(|| {
"harmony_patch_postfix_result: shim is pre-v10; rebuild and redeploy the C# shim".to_string()
})?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let c_filter = CString::new(filter.to_string()).map_err(|e| format!("bad filter: {e}"))?;
let handle = patch(c_class.as_ptr(), c_method.as_ptr(), c_filter.as_ptr(), postfix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_postfix_result({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Postfix))
}
/// Write a replacement result into a `patch_postfix_result`
/// callback's output buffer. Returns the length to hand back to
/// the shim, or -1 (keep the original) when it does not fit.
///
/// # Safety
/// `out` and `cap` must be the buffer the shim passed to the
/// callback.
pub unsafe fn write_result(out: *mut std::os::raw::c_char, cap: i32, value: &serde_json::Value) -> i32 {
let s = value.to_string();
if out.is_null() || s.len() > cap.max(0) as usize {
return -1;
}
// SAFETY: caller guarantees out..out+cap is the shim's buffer.
unsafe { std::ptr::copy_nonoverlapping(s.as_ptr(), out as *mut u8, s.len()) };
s.len() as i32
}
/// Install a postfix patch. The trampoline must be an `extern
/// "C" fn(*const c_void)`.
pub fn patch_postfix(
class_name: &str,
method_name: &str,
postfix_fn: extern "C" fn(*const c_void),
) -> Result<Hook, String> {
let bridge = bridge::try_get()?;
let c_class = CString::new(class_name).map_err(|e| format!("bad class: {e}"))?;
let c_method = CString::new(method_name).map_err(|e| format!("bad method: {e}"))?;
let handle = (bridge.harmony_patch_postfix)(c_class.as_ptr(), c_method.as_ptr(), postfix_fn);
if handle.0 == 0 {
return Err(format!(
"harmony_patch_postfix({class_name}, {method_name}) failed"
));
}
Ok(Hook::new(handle, class_name, method_name, HookWhen::Postfix))
}
/// Live hooks, as seen on modforge's shutdown registry (kind
/// "hook"). Holds no list of its own: every hook's unpatch was
/// recorded there when it was made.
pub struct HookRegistry;
impl HookRegistry {
pub const fn new() -> Self {
Self
}
/// Same as [`Hook::keep`]: leave the patch on until the
/// generation shuts down.
pub fn register(&self, hook: Hook) {
hook.keep();
}
/// Unpatch every live hook now.
pub fn shutdown_all(&self) {
modforge::shutdown::SHUTDOWN_REGISTRY.undo_all("hook");
}
/// `Class.Method` of every live hook, oldest first.
pub fn list(&self) -> Vec<String> {
modforge::shutdown::SHUTDOWN_REGISTRY.list("hook")
}
pub fn len(&self) -> usize {
self.list().len()
}
pub fn is_empty(&self) -> bool {
self.list().is_empty()
}
}
impl Default for HookRegistry {
fn default() -> Self {
Self::new()
}
}
pub static HOOK_REGISTRY: HookRegistry = HookRegistry::new();