Skip to main content

forge_macros/
lib.rs

1use crate::{
2    hook::{HookArgs, HookTransformer},
3    pure_virtual::{PureVirtualFn, VirtualArgs},
4};
5use proc_macro::TokenStream;
6use proc_macro_crate::{FoundCrate, crate_name};
7use proc_macro2::TokenStream as TokenStream2;
8use quote::quote;
9use syn::{DeriveInput, FnArg, ItemFn, parse_macro_input, visit_mut::VisitMut};
10
11fn forge_crate() -> TokenStream2 {
12    match crate_name("mhgu-forge") {
13        Ok(FoundCrate::Itself) => quote! { crate },
14        Ok(FoundCrate::Name(_)) | Err(_) => quote! { ::forge },
15    }
16}
17
18mod hook;
19#[cfg(feature = "config")]
20mod ini;
21mod pure_virtual;
22
23/// Marks a function as the plugins entry point.
24///
25/// ### Example
26/// ```ignore
27/// #[forge::entry]
28/// fn my_main_function() {
29///     // Your code here
30/// }
31/// ```
32#[proc_macro_attribute]
33pub fn entry(_attr: TokenStream, item: TokenStream) -> TokenStream {
34    let inner = parse_macro_input!(item as ItemFn);
35    let inner_name = &inner.sig.ident;
36
37    let expanded = quote! {
38        #[inline(always)]
39        #inner
40
41        #[unsafe(no_mangle)]
42        pub extern "C" fn forge_onLoad(params: *mut ::forge::sys::init::PluginInitParams) {
43            unsafe {
44                (*params).required_version = ::forge::REQUIRED_VERSION;
45            }
46        }
47
48        #[unsafe(no_mangle)]
49        pub extern "C" fn forge_onInit(params: *mut ::forge::sys::init::PluginInitParams) {
50            ::forge::log::init().expect("Failed to initialize logger");
51            #inner_name();
52        }
53    };
54
55    expanded.into()
56}
57
58/// Callback for rendering imgui UI in the main forge window
59///
60/// ### Example
61/// ```ignore
62/// #[forge::imgui_render]
63/// fn my_render() {
64///     // Your code here
65/// }
66/// ```
67#[cfg(feature = "imgui")]
68#[proc_macro_attribute]
69pub fn imgui_render(_attr: TokenStream, item: TokenStream) -> TokenStream {
70    let inner = parse_macro_input!(item as ItemFn);
71    let inner_name = &inner.sig.ident;
72
73    let expanded = quote! {
74        #[inline(always)]
75        #inner
76
77        #[unsafe(no_mangle)]
78        pub extern "C" fn forge_onImGuiRender() {
79            #inner_name();
80        }
81    };
82
83    expanded.into()
84}
85
86/// Callback for rendering imgui UI *outside* of the main forge window.
87/// **Requires** begin/end calls to draw.
88///
89/// ### Example
90/// ```ignore
91/// #[forge::imgui_free_render]
92/// fn my_free_render() {
93///     // Your code here
94/// }
95/// ```
96#[cfg(feature = "imgui")]
97#[proc_macro_attribute]
98pub fn imgui_free_render(_attr: TokenStream, item: TokenStream) -> TokenStream {
99    let inner = parse_macro_input!(item as ItemFn);
100    let inner_name = &inner.sig.ident;
101
102    let expanded = quote! {
103        #[inline(always)]
104        #inner
105
106        #[unsafe(no_mangle)]
107        pub extern "C" fn forge_onImGuiFreeRender() {
108            #inner_name();
109        }
110    };
111
112    expanded.into()
113}
114
115/// Defines a function hook at a fixed offset from a base address.
116///
117/// The annotated function becomes a module of the same name that holds the
118/// hook's static state and can be installed via `forge::install_hook!`.
119///
120/// Inside the body the following pseudo-macros are available:
121/// - `original!(args)` - call the original function (zero or more args).
122/// - `original!()` - call the original function with no arguments.
123/// - `original_function!()` - obtain the raw function pointer without calling it.
124/// - `context!(T)` - borrow the context as `&mut T` (requires context variant of install).
125/// - `context!()` - obtain the raw `*const c_void` context pointer.
126///
127/// ### Example
128/// ```ignore
129/// #[forge::hook(offset = 0x1234)]
130/// fn my_hook(param: u32) -> u32 {
131///     let result = original!(param);
132///     result * 2
133/// }
134///
135/// // Context must outlive the hook (static or Box::leak with "allocator" feature).
136/// static mut MULTIPLIER: u32 = 2;
137///
138/// #[forge::hook(offset = 0x5678)]
139/// fn ctx_hook(value: u32) -> u32 {
140///     let m = context!(u32);
141///     original!(value) * *m
142/// }
143///
144/// #[forge::entry]
145/// fn main() {
146///     let base = forge::mem::text_addr();
147///     forge::install_hook!(base, my_hook);
148///     forge::install_hook!(base, ctx_hook, unsafe { &raw mut MULTIPLIER });
149/// }
150/// ```
151#[proc_macro_attribute]
152pub fn hook(attr: TokenStream, item: TokenStream) -> TokenStream {
153    let args = parse_macro_input!(attr as HookArgs);
154    let func = parse_macro_input!(item as ItemFn);
155
156    let offset = &args.offset;
157    let func_name = &func.sig.ident;
158    let inputs = &func.sig.inputs;
159    let output = &func.sig.output;
160
161    let param_types: Vec<TokenStream2> = inputs
162        .iter()
163        .map(|arg| match arg {
164            FnArg::Typed(pat_type) => {
165                let ty = &pat_type.ty;
166                quote! { #ty }
167            }
168            FnArg::Receiver(_) => {
169                panic!("#[forge::hook] does not support `self` parameters")
170            }
171        })
172        .collect();
173
174    let ret_type = match output {
175        syn::ReturnType::Default => quote! { () },
176        syn::ReturnType::Type(_, ty) => quote! { #ty },
177    };
178
179    let fn_ptr_type = quote! { unsafe extern "C" fn(#(#param_types),*) -> #ret_type };
180
181    let mut body = func.block.clone();
182    HookTransformer.visit_block_mut(&mut body);
183
184    let expanded = quote! {
185        pub mod #func_name {
186            #[allow(unused_imports)]
187            use super::*;
188
189            /// Offset of the hook target from the base address supplied to `install_hook!`.
190            pub const OFFSET: u32 = #offset as u32;
191
192            static mut __HOOK: ::core::mem::MaybeUninit<::forge::sys::hook::Hook> =
193                ::core::mem::MaybeUninit::uninit();
194            static mut __ORIGINAL: *const ::core::ffi::c_void = ::core::ptr::null();
195
196            pub unsafe extern "C" fn __detour(#inputs) #output {
197                let __forge_original: #fn_ptr_type = unsafe {
198                    ::core::mem::transmute(__ORIGINAL)
199                };
200                let __forge_context = unsafe { ::forge::sys::hook::forge_hook_getContext() };
201                #body
202            }
203
204            pub unsafe fn __install(base: u32) {
205                unsafe {
206                    __HOOK.write(::forge::sys::hook::forge_hook_create(
207                        (base + OFFSET) as *const ::core::ffi::c_void,
208                        __detour as *const ::core::ffi::c_void,
209                        ::core::ptr::addr_of_mut!(__ORIGINAL),
210                    ));
211                }
212            }
213
214            pub unsafe fn __install_with_ctx(base: u32, ctx: *const ::core::ffi::c_void) {
215                unsafe {
216                    __HOOK.write(::forge::sys::hook::forge_hook_createWithContext(
217                        (base + OFFSET) as *const ::core::ffi::c_void,
218                        __detour as *const ::core::ffi::c_void,
219                        ::core::ptr::addr_of_mut!(__ORIGINAL),
220                        ctx,
221                    ));
222                }
223            }
224
225            /// Update the context pointer for an already-installed hook.
226            pub unsafe fn __update_ctx(ctx: *const ::core::ffi::c_void) {
227                unsafe {
228                    let result = ::forge::sys::hook::forge_hook_updateContext(
229                        __HOOK.as_mut_ptr(),
230                        ctx,
231                    );
232                    debug_assert_eq!(result, 0, "forge_hook_updateContext failed");
233                }
234            }
235        }
236    };
237
238    expanded.into()
239}
240
241/// Marks a method as a virtual function at a given index in the vtable.
242/// Methods marked with this must be part of a type that implements `HasVtable`, and must take `&self` or `&mut self` as the first parameter.
243/// The method body is replaced with a call to the function pointer at the specified index in the vtable
244///
245/// ### Example
246/// ```ignore
247/// #[derive(forge::HasVtable)]
248/// pub struct MyStruct;
249///
250/// impl MyStruct {
251///     #[forge::pure_virtual(3)]
252///     pub fn my_virtual_func(&self) -> i32 {}
253/// }
254/// ```
255/// Note the lack of an actual implementation of the function.
256#[proc_macro_attribute]
257pub fn pure_virtual(attr: TokenStream, item: TokenStream) -> TokenStream {
258    let args = parse_macro_input!(attr as VirtualArgs);
259    let func = parse_macro_input!(item as PureVirtualFn);
260
261    let func_name = &func.sig.ident;
262    let inputs = &func.sig.inputs;
263    let output = &func.sig.output;
264    let visibility = &func.vis;
265
266    let has_self = !inputs.is_empty()
267        && match &inputs[0] {
268            FnArg::Receiver(receiver) => receiver.reference.is_some(),
269            _ => false,
270        };
271
272    if !has_self {
273        panic!("Functions marked with #[forge::pure_virtual] must have `&self` as their first parameter");
274    }
275
276    let param_types: Vec<TokenStream2> = inputs
277        .iter()
278        .map(|arg| match arg {
279            FnArg::Receiver(receiver) => {
280                let ty = &receiver.ty;
281                quote! { #ty }
282            }
283            FnArg::Typed(pat_type) => {
284                let ty = &pat_type.ty;
285                quote! { #ty }
286            }
287        })
288        .collect();
289
290    let ret_type = match output {
291        syn::ReturnType::Default => quote! { () },
292        syn::ReturnType::Type(_, ty) => quote! { #ty },
293    };
294
295    let fn_ptr_type = quote! { unsafe extern "C" fn(#(#param_types),*) -> #ret_type };
296
297    let index = &args.index;
298    let param_names: Vec<TokenStream2> = inputs
299        .iter()
300        .map(|arg| match arg {
301            FnArg::Receiver(_) => quote! { self },
302            FnArg::Typed(pat_type) => {
303                let pat = &pat_type.pat;
304                quote! { #pat }
305            }
306        })
307        .collect();
308
309    let forge = forge_crate();
310    let expanded = quote! {
311        #visibility fn #func_name(#inputs) #output {
312            let vtable = #forge::sys::cpp::HasVtable::vtable_ptr(self);
313            let addr = unsafe { #forge::sys::cpp::HasVtable::get_virtual_function(self, #index) };
314            let func: #fn_ptr_type = unsafe {
315                ::core::mem::transmute(addr)
316            };
317            unsafe { func(#(#param_names),*) }
318        }
319    };
320
321    expanded.into()
322}
323
324#[proc_macro_derive(HasVtable)]
325pub fn has_vtable_derive(input: TokenStream) -> TokenStream {
326    let input = parse_macro_input!(input as DeriveInput);
327    let type_name = &input.ident;
328    let forge = forge_crate();
329
330    let expanded = quote! {
331        impl #forge::sys::cpp::HasVtable for #type_name {
332            fn vtable_ptr(&self) -> *const *const ::core::ffi::c_void {
333                unsafe {
334                    let ptr = self as *const Self as *const *const *const ::core::ffi::c_void;
335                    *ptr
336                }
337            }
338        }
339    };
340
341    expanded.into()
342}
343
344#[proc_macro_derive(Object)]
345pub fn mt_object_derive(input: TokenStream) -> TokenStream {
346    let input = parse_macro_input!(input as DeriveInput);
347    let type_name = &input.ident;
348    let forge = forge_crate();
349
350    let expanded = quote! {
351        impl #forge::sys::cpp::HasVtable for #type_name {
352            fn vtable_ptr(&self) -> *const *const ::core::ffi::c_void {
353                unsafe {
354                    let ptr = self as *const Self as *const *const *const ::core::ffi::c_void;
355                    *ptr
356                }
357            }
358        }
359
360        impl #forge::mt::object::Object for #type_name {}
361    };
362
363    expanded.into()
364}
365
366#[proc_macro_derive(CacheDti)]
367pub fn cache_dti_derive(input: TokenStream) -> TokenStream {
368    let input = parse_macro_input!(input as DeriveInput);
369    let type_name = &input.ident;
370    let type_name_str = type_name.to_string();
371    let forge = forge_crate();
372
373    let expanded = quote! {
374        impl #forge::mt::dti::CacheDti for #type_name {
375            fn dti() -> Option<&'static #forge::mt::dti::MtDti> {
376                static DTI: core::sync::atomic::AtomicPtr<#forge::mt::dti::MtDti> = core::sync::atomic::AtomicPtr::new(core::ptr::null_mut());
377                let mut ptr = DTI.load(core::sync::atomic::Ordering::Relaxed);
378                if ptr.is_null() {
379                    ptr = #forge::mt::dti::MtDti::find(#type_name_str).map_or(core::ptr::null_mut(), |d| d as *const _ as *mut _);
380                    DTI.store(ptr, core::sync::atomic::Ordering::Relaxed);
381                }
382
383                if ptr.is_null() {
384                    None
385                } else {
386                    Some(unsafe { &*ptr })
387                }
388            }
389        }
390    };
391
392    expanded.into()
393}
394
395/// Derives `IniDeserialize` for a struct, reading each field from an `Ini`.
396///
397/// Field attributes:
398/// - `#[section("Name")]` sets the section for this field and all following
399///   fields until another `#[section]` is seen. Scalar fields are read from this
400///   section; nested struct fields create a sub-section under it.
401/// - `#[alias("name")]` overrides the property key (scalar) or sub-section name
402///   (nested struct); the field name is used otherwise.
403///
404/// Scalar field types must implement `FromStr + Default`. Struct field types must
405/// themselves implement `IniDeserialize` and are deserialized into the
406/// sub-section `<section>.<field>`. Enums are not supported.
407///
408/// ### Example
409/// ```ignore
410/// #[derive(forge::IniDeserialize)]
411/// struct Config {
412///     #[section("Timing")]
413///     delay: i32,
414///     speed: f32,
415///
416///     #[section("General")]
417///     #[alias("MyStruct")]
418///     mystruct: MyStruct, // -> [General.MyStruct]
419/// }
420/// ```
421#[cfg(feature = "config")]
422#[proc_macro_derive(IniDeserialize, attributes(section, alias))]
423pub fn ini_deserialize_derive(input: TokenStream) -> TokenStream {
424    let input = parse_macro_input!(input as DeriveInput);
425    ini::ini_deserialize_derive(input).into()
426}
427
428/// Derives `IniSerialize` for a struct, writing each field into an `Ini`.
429///
430/// This is the inverse of [`macro@IniDeserialize`] and uses the same `#[section]`
431/// and `#[alias]` field attributes with identical section/sub-section semantics.
432///
433/// Scalar field types must implement `ToString`. Struct field types must
434/// themselves implement `IniSerialize` and are written into the sub-section
435/// `<section>.<field>`. Enums are not supported.
436///
437/// ### Example
438/// ```ignore
439/// #[derive(forge::IniSerialize)]
440/// struct Config {
441///     #[section("Timing")]
442///     delay: i32,
443///     speed: f32,
444/// }
445/// ```
446#[cfg(feature = "config")]
447#[proc_macro_derive(IniSerialize, attributes(section, alias))]
448pub fn ini_serialize_derive(input: TokenStream) -> TokenStream {
449    let input = parse_macro_input!(input as DeriveInput);
450    ini::ini_serialize_derive(input).into()
451}