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/// # Safety requirement
24///
25/// All field types must be valid for any bit pattern. Numeric primitives (`u8`,
26/// `u32`, `f32`, etc.), fixed-size arrays of numeric types, and `#[repr(C)]`
27/// structs composed of such types are safe. Types with validity invariants
28/// (`bool`, `char`, enums, references) must not be used - read them as their
29/// underlying integer type instead (e.g., `u8` for booleans).
30///
31/// # Example
32///
33/// ```ignore
34/// use procmod_layout::GameStruct;
35///
36/// #[derive(GameStruct)]
37/// struct Player {
38///     #[offset(0x100)]
39///     health: f32,
40///     #[offset(0x200)]
41///     #[pointer_chain(0x10, 0x8)]
42///     damage_mult: f32,
43/// }
44/// ```
45#[proc_macro_derive(GameStruct, attributes(offset, pointer_chain))]
46pub fn derive_game_struct(input: TokenStream) -> TokenStream {
47    let input = parse_macro_input!(input as DeriveInput);
48    match impl_game_struct(&input) {
49        Ok(tokens) => tokens.into(),
50        Err(e) => e.to_compile_error().into(),
51    }
52}
53
54fn impl_game_struct(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
55    let name = &input.ident;
56
57    let fields = match &input.data {
58        Data::Struct(data) => match &data.fields {
59            Fields::Named(f) => &f.named,
60            _ => {
61                return Err(syn::Error::new(
62                    Span::call_site(),
63                    "GameStruct only supports structs with named fields",
64                ))
65            }
66        },
67        _ => {
68            return Err(syn::Error::new(
69                Span::call_site(),
70                "GameStruct can only be derived for structs",
71            ))
72        }
73    };
74
75    let mut field_reads = Vec::new();
76
77    for field in fields {
78        let field_name = field.ident.as_ref().unwrap();
79        let field_ty = &field.ty;
80
81        let offset = parse_offset_attr(field)?;
82        let chain = parse_pointer_chain_attr(field)?;
83
84        let read_expr = if let Some(offsets) = chain {
85            gen_pointer_chain_read(field_ty, offset, &offsets)
86        } else {
87            gen_direct_read(field_ty, offset)
88        };
89
90        field_reads.push(quote! {
91            #field_name: #read_expr
92        });
93    }
94
95    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
96
97    Ok(quote! {
98        impl #impl_generics #name #ty_generics #where_clause {
99            /// Reads this struct from a remote process's memory at the given base address.
100            pub fn read(
101                __procmod_process: &::procmod_layout::Process,
102                __procmod_base: usize,
103            ) -> ::procmod_layout::Result<Self> {
104                Ok(Self {
105                    #(#field_reads),*
106                })
107            }
108        }
109    })
110}
111
112fn parse_offset_attr(field: &syn::Field) -> syn::Result<u64> {
113    for attr in &field.attrs {
114        if attr.path().is_ident("offset") {
115            let lit: LitInt = attr.parse_args()?;
116            return lit.base10_parse();
117        }
118    }
119
120    Err(syn::Error::new_spanned(
121        field.ident.as_ref().unwrap(),
122        "missing #[offset(N)] attribute",
123    ))
124}
125
126fn parse_pointer_chain_attr(field: &syn::Field) -> syn::Result<Option<Vec<u64>>> {
127    for attr in &field.attrs {
128        if attr.path().is_ident("pointer_chain") {
129            match &attr.meta {
130                Meta::List(list) => {
131                    let parsed: syn::punctuated::Punctuated<LitInt, syn::Token![,]> =
132                        list.parse_args_with(syn::punctuated::Punctuated::parse_terminated)?;
133
134                    if parsed.is_empty() {
135                        return Err(syn::Error::new_spanned(
136                            list,
137                            "pointer_chain requires at least one offset",
138                        ));
139                    }
140
141                    let offsets = parsed
142                        .iter()
143                        .map(|lit| lit.base10_parse())
144                        .collect::<syn::Result<Vec<u64>>>()?;
145
146                    return Ok(Some(offsets));
147                }
148                _ => {
149                    return Err(syn::Error::new_spanned(
150                        attr,
151                        "expected #[pointer_chain(offset, ...)]",
152                    ))
153                }
154            }
155        }
156    }
157    Ok(None)
158}
159
160fn gen_direct_read(ty: &syn::Type, offset: u64) -> proc_macro2::TokenStream {
161    let offset_lit = LitInt::new(&format!("{offset}"), Span::call_site());
162    quote! {
163        unsafe { __procmod_process.read::<#ty>(__procmod_base + #offset_lit)? }
164    }
165}
166
167fn gen_pointer_chain_read(
168    ty: &syn::Type,
169    base_offset: u64,
170    chain: &[u64],
171) -> proc_macro2::TokenStream {
172    let base_offset_lit = LitInt::new(&format!("{base_offset}"), Span::call_site());
173
174    let mut steps = Vec::new();
175
176    // read initial pointer
177    let first_var = syn::Ident::new("__ptr_0", Span::call_site());
178    steps.push(quote! {
179        let #first_var: usize = unsafe {
180            __procmod_process.read::<usize>(__procmod_base + #base_offset_lit)?
181        };
182    });
183
184    // follow intermediate pointers (all except last offset)
185    let last_idx = chain.len() - 1;
186    for (i, &offset) in chain[..last_idx].iter().enumerate() {
187        let prev_var = syn::Ident::new(&format!("__ptr_{i}"), Span::call_site());
188        let next_var = syn::Ident::new(&format!("__ptr_{}", i + 1), Span::call_site());
189        let offset_lit = LitInt::new(&format!("{offset}"), Span::call_site());
190        steps.push(quote! {
191            let #next_var: usize = unsafe {
192                __procmod_process.read::<usize>(#prev_var + #offset_lit)?
193            };
194        });
195    }
196
197    // read final value at last chain offset
198    let final_var = syn::Ident::new(&format!("__ptr_{last_idx}"), Span::call_site());
199    let final_offset_lit = LitInt::new(&format!("{}", chain[last_idx]), Span::call_site());
200
201    quote! {
202        {
203            #(#steps)*
204            unsafe { __procmod_process.read::<#ty>(#final_var + #final_offset_lit)? }
205        }
206    }
207}