procmod_layout_derive/
lib.rs1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::quote;
4use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, Meta};
5
6#[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 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 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 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 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}