Skip to main content

procmod_layout_derive/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, Meta};
5
6/// Derives a `read` method that maps a remote process's memory into a Rust struct.
7///
8/// Each field must have an `#[offset(N)]` attribute specifying its byte offset
9/// from the base address. Fields may optionally have a `#[pointer_chain(a, b, ...)]`
10/// attribute to follow a chain of pointers before reading the final value.
11///
12/// The generated method signature is:
13///
14/// ```ignore
15/// pub fn read(process: &procmod_layout::Process, base: usize) -> procmod_layout::Result<Self>
16/// ```
17///
18/// # Attributes
19///
20/// - `#[offset(N)]` - byte offset from base address (required on every field)
21/// - `#[pointer_chain(a, b, ...)]` - intermediate pointer offsets to follow before reading
22///
23/// # Example
24///
25/// ```ignore
26/// use procmod_layout::GameStruct;
27///
28/// #[derive(GameStruct)]
29/// struct Player {
30///     #[offset(0x100)]
31///     health: f32,
32///     #[offset(0x200)]
33///     #[pointer_chain(0x10, 0x8)]
34///     damage_mult: f32,
35/// }
36/// ```
37#[proc_macro_derive(GameStruct, attributes(offset, pointer_chain))]
38pub fn derive_game_struct(input: TokenStream) -> TokenStream {
39    let input = parse_macro_input!(input as DeriveInput);
40    match impl_game_struct(&input) {
41        Ok(tokens) => tokens.into(),
42        Err(e) => e.to_compile_error().into(),
43    }
44}
45
46fn impl_game_struct(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
47    let name = &input.ident;
48
49    let fields = match &input.data {
50        Data::Struct(data) => match &data.fields {
51            Fields::Named(f) => &f.named,
52            _ => {
53                return Err(syn::Error::new(
54                    Span::call_site(),
55                    "GameStruct only supports structs with named fields",
56                ))
57            }
58        },
59        _ => {
60            return Err(syn::Error::new(
61                Span::call_site(),
62                "GameStruct can only be derived for structs",
63            ))
64        }
65    };
66
67    let mut field_reads = Vec::new();
68
69    for field in fields {
70        let field_name = field.ident.as_ref().unwrap();
71        let field_ty = &field.ty;
72
73        let offset = parse_offset_attr(field)?;
74        let chain = parse_pointer_chain_attr(field)?;
75
76        let read_expr = if let Some(offsets) = chain {
77            gen_pointer_chain_read(field_ty, offset, &offsets)
78        } else {
79            gen_direct_read(field_ty, offset)
80        };
81
82        field_reads.push(quote! {
83            #field_name: #read_expr
84        });
85    }
86
87    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
88
89    Ok(quote! {
90        impl #impl_generics #name #ty_generics #where_clause {
91            /// Reads this struct from a remote process's memory at the given base address.
92            pub fn read(
93                __procmod_process: &::procmod_layout::Process,
94                __procmod_base: usize,
95            ) -> ::procmod_layout::Result<Self> {
96                Ok(Self {
97                    #(#field_reads),*
98                })
99            }
100        }
101    })
102}
103
104fn parse_offset_attr(field: &syn::Field) -> syn::Result<u64> {
105    for attr in &field.attrs {
106        if attr.path().is_ident("offset") {
107            let lit: LitInt = attr.parse_args()?;
108            return lit.base10_parse();
109        }
110    }
111
112    Err(syn::Error::new_spanned(
113        field.ident.as_ref().unwrap(),
114        "missing #[offset(N)] attribute",
115    ))
116}
117
118fn parse_pointer_chain_attr(field: &syn::Field) -> syn::Result<Option<Vec<u64>>> {
119    for attr in &field.attrs {
120        if attr.path().is_ident("pointer_chain") {
121            match &attr.meta {
122                Meta::List(list) => {
123                    let parsed: syn::punctuated::Punctuated<LitInt, syn::Token![,]> =
124                        list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
125
126                    if parsed.is_empty() {
127                        return Err(syn::Error::new_spanned(
128                            list,
129                            "pointer_chain requires at least one offset",
130                        ));
131                    }
132
133                    let offsets = parsed
134                        .iter()
135                        .map(|lit| lit.base10_parse())
136                        .collect::<syn::Result<Vec<u64>>>()?;
137
138                    return Ok(Some(offsets));
139                }
140                _ => {
141                    return Err(syn::Error::new_spanned(
142                        attr,
143                        "expected #[pointer_chain(offset, ...)]",
144                    ))
145                }
146            }
147        }
148    }
149    Ok(None)
150}
151
152fn gen_direct_read(ty: &syn::Type, offset: u64) -> proc_macro2::TokenStream {
153    let offset_lit = LitInt::new(&format!("{offset}"), Span::call_site());
154    quote! {
155        unsafe { __procmod_process.read::<#ty>(__procmod_base + #offset_lit)? }
156    }
157}
158
159fn gen_pointer_chain_read(
160    ty: &syn::Type,
161    base_offset: u64,
162    chain: &[u64],
163) -> proc_macro2::TokenStream {
164    let base_offset_lit = LitInt::new(&format!("{base_offset}"), Span::call_site());
165
166    let mut steps = Vec::new();
167
168    // read initial pointer
169    let first_var = syn::Ident::new("__ptr_0", Span::call_site());
170    steps.push(quote! {
171        let #first_var: usize = unsafe {
172            __procmod_process.read::<usize>(__procmod_base + #base_offset_lit)?
173        };
174    });
175
176    // follow intermediate pointers (all except last offset)
177    let last_idx = chain.len() - 1;
178    for (i, &offset) in chain[..last_idx].iter().enumerate() {
179        let prev_var = syn::Ident::new(&format!("__ptr_{i}"), Span::call_site());
180        let next_var = syn::Ident::new(&format!("__ptr_{}", i + 1), Span::call_site());
181        let offset_lit = LitInt::new(&format!("{offset}"), Span::call_site());
182        steps.push(quote! {
183            let #next_var: usize = unsafe {
184                __procmod_process.read::<usize>(#prev_var + #offset_lit)?
185            };
186        });
187    }
188
189    // read final value at last chain offset
190    let final_var = syn::Ident::new(&format!("__ptr_{last_idx}"), Span::call_site());
191    let final_offset_lit = LitInt::new(&format!("{}", chain[last_idx]), Span::call_site());
192
193    quote! {
194        {
195            #(#steps)*
196            unsafe { __procmod_process.read::<#ty>(#final_var + #final_offset_lit)? }
197        }
198    }
199}