Skip to main content

irox_structs_derive/
lib.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2025 IROX Contributors
3//
4
5mod shared;
6
7use proc_macro::{Literal, TokenStream};
8
9use quote::{quote, ToTokens};
10use syn::{parse_macro_input, Data, DeriveInput, Fields, FieldsNamed};
11
12use irox_types::{PrimitiveType, Primitives, VariableType};
13
14use irox_derive_helpers::DeriveMethods;
15
16const TYPES_STRICT_SIZING_INCOMPATIBLE: [Primitives; 1] = [Primitives::null];
17
18struct Config {
19    strict_sizing: bool,
20    big_endian: bool,
21}
22
23fn get_endian_method_for_prim(ty: Primitives, read: bool, big_endian: bool) -> String {
24    let rw = if read { "read" } else { "write" };
25    let be = if big_endian { "be" } else { "le" };
26    let base = match ty {
27        Primitives::u8 | Primitives::bool | Primitives::char => "u8".to_string(),
28        Primitives::i8 => "i8".to_string(),
29
30        _ => {
31            format!("{be}_{ty:?}")
32        }
33    };
34    format!("{rw}_{base}")
35}
36
37fn get_endian_method_for_varbl(ty: VariableType, read: bool, big_endian: bool) -> String {
38    let rw = if read { "read" } else { "write" };
39    let be = if big_endian { "be" } else { "le" };
40    let base = match ty {
41        VariableType::str => "str_u32_blob".to_string(),
42        _ => {
43            format!("{be}_{ty:?}")
44        }
45    };
46    format!("{rw}_{base}")
47}
48
49fn create_write_to_fn(n: &FieldsNamed, config: &Config, sizing: &mut StructSizing) -> TokenStream {
50    let mut ts = TokenStream::new();
51    ts.extend::<TokenStream>(
52        quote!(
53            fn write_to<T: irox_structs::MutBits>(&self, out: &mut T) -> Result<(), irox_structs::Error>
54        )
55        .into(),
56    );
57
58    let mut method = TokenStream::new();
59
60    for x in &n.named {
61        let Some(ident) = &x.ident else {
62            return irox_derive_helpers::compile_error(&x, "No ident");
63        };
64        match PrimitiveType::try_from(x) {
65            Ok(field) => {
66                if let Some(size) = field.bytes_length() {
67                    sizing.size += size;
68                }
69                match field {
70                    PrimitiveType::Primitive(input) => {
71                        if config.strict_sizing && TYPES_STRICT_SIZING_INCOMPATIBLE.contains(&input)
72                        {
73                            return irox_derive_helpers::compile_error(
74                                &x,
75                                "Type is not compatible with strict sizing",
76                            );
77                        };
78                        method.add_ident("out");
79                        method.add_punc('.');
80                        method.add_ident(&get_endian_method_for_prim(
81                            input,
82                            false,
83                            config.big_endian,
84                        ));
85                        method.add_parens({
86                            let mut ts = TokenStream::new();
87                            ts.add_ident("self");
88                            ts.add_punc('.');
89                            ts.add_ident(&ident.to_string());
90                            ts
91                        });
92                        method.add_punc('?');
93                        method.add_punc(';');
94                    }
95                    PrimitiveType::Array(input, len) => {
96                        if config.strict_sizing && TYPES_STRICT_SIZING_INCOMPATIBLE.contains(&input)
97                        {
98                            return irox_derive_helpers::compile_error(
99                                &x,
100                                "Type is not compatible with strict sizing",
101                            );
102                        };
103                        method.add_ident("for");
104                        method.add_ident("elem");
105                        method.add_ident("in");
106                        method.add_ident("self");
107                        method.add_punc('.');
108                        method.add_ident(&ident.to_string());
109                        method.wrap_braces({
110                            let mut ts = TokenStream::new();
111                            for _ in 0..len {
112                                ts.add_ident("out");
113                                ts.add_punc('.');
114                                ts.add_ident(&get_endian_method_for_prim(
115                                    input,
116                                    false,
117                                    config.big_endian,
118                                ));
119                                ts.add_parens(TokenStream::create_ident("elem"));
120                                ts.add_punc('?');
121                                ts.add_punc(';');
122                            }
123                            ts
124                        })
125                    }
126                    PrimitiveType::DynamicallySized(dy) => {
127                        if config.strict_sizing {
128                            return irox_derive_helpers::compile_error(
129                                &x,
130                                "Type is not compatible with strict sizing",
131                            );
132                        };
133                        method.add_ident("out");
134                        method.add_punc('.');
135                        method.add_ident(&get_endian_method_for_varbl(
136                            dy,
137                            false,
138                            config.big_endian,
139                        ));
140                        method.add_parens({
141                            let mut ts = TokenStream::new();
142                            ts.add_punc('&');
143                            ts.add_ident("self");
144                            ts.add_punc('.');
145                            ts.add_ident(&ident.to_string());
146                            ts
147                        });
148                        method.add_punc('?');
149                        method.add_punc(';');
150                    }
151                }
152            }
153            Err(_e) => {
154                // <ty as irox_structs::Struct>::write_to(&self.varbl, out)?;
155                let mut ts = TokenStream::new();
156                ts.wrap_generics({
157                    let mut ts = TokenStream::new();
158                    ts.extend::<TokenStream>(x.ty.to_token_stream().into());
159                    ts.add_ident("as");
160                    ts.add_ident("irox_structs");
161                    ts.add_punc2(':', ':');
162                    ts.add_ident("Struct");
163                    ts
164                });
165                ts.add_punc2(':', ':');
166                ts.add_ident("write_to");
167                ts.add_parens({
168                    let mut ts = TokenStream::new();
169                    ts.add_punc('&');
170                    ts.add_ident("self");
171                    ts.add_punc('.');
172                    ts.add_ident(&ident.to_string());
173                    ts.add_punc(',');
174                    ts.add_ident("out");
175                    ts
176                });
177                ts.add_punc('?');
178                ts.add_punc(';');
179                method.extend(ts);
180            }
181        }
182    }
183    method.add_ident("Ok");
184    method.add_parens(TokenStream::create_empty_type());
185    ts.wrap_braces(method);
186    ts
187}
188
189fn create_parse_from_fn(n: &FieldsNamed, config: &Config) -> TokenStream {
190    let mut ts = TokenStream::new();
191    ts.extend::<TokenStream>(quote!(
192        fn parse_from<T: irox_structs::Bits>(input: &mut T) -> Result<Self::ImplType, irox_structs::Error>
193    ).into());
194
195    let mut inits = TokenStream::new();
196
197    for x in &n.named {
198        let Some(ident) = &x.ident else {
199            return irox_derive_helpers::compile_error(&x, "No ident");
200        };
201
202        match PrimitiveType::try_from(x) {
203            Ok(field) => match field {
204                PrimitiveType::Primitive(input) => {
205                    if config.strict_sizing && TYPES_STRICT_SIZING_INCOMPATIBLE.contains(&input) {
206                        return irox_derive_helpers::compile_error(
207                            &x,
208                            "Type is not compatible with strict sizing",
209                        );
210                    };
211                    inits.add_ident(&ident.to_string());
212                    inits.add_punc(':');
213                    inits.add_ident("input");
214                    inits.add_punc('.');
215                    inits.add_ident(&get_endian_method_for_prim(input, true, config.big_endian));
216                    inits.add_parens(TokenStream::new());
217                    inits.add_punc('?');
218                    inits.add_punc(',');
219                }
220                PrimitiveType::Array(input, len) => {
221                    if config.strict_sizing && TYPES_STRICT_SIZING_INCOMPATIBLE.contains(&input) {
222                        return irox_derive_helpers::compile_error(
223                            &x,
224                            "Type is not compatible with strict sizing",
225                        );
226                    };
227                    inits.add_ident(&ident.to_string());
228                    inits.add_punc(':');
229                    inits.wrap_brackets({
230                        let mut ts = TokenStream::new();
231                        for _ in 0..len {
232                            ts.add_ident("input");
233                            ts.add_punc('.');
234                            ts.add_ident(&get_endian_method_for_prim(
235                                input,
236                                true,
237                                config.big_endian,
238                            ));
239                            ts.add_parens(TokenStream::new());
240                            ts.add_punc('?');
241                            ts.add_punc(',');
242                        }
243                        ts
244                    });
245                    inits.add_punc('.');
246                    inits.add_ident("into");
247                    inits.add_parens(TokenStream::new());
248                    inits.add_punc(',');
249                }
250                PrimitiveType::DynamicallySized(ds) => {
251                    if config.strict_sizing {
252                        return irox_derive_helpers::compile_error(
253                            &x,
254                            "Type is not compatible with strict sizing",
255                        );
256                    };
257
258                    inits.add_ident(&ident.to_string());
259                    inits.add_punc(':');
260                    inits.add_ident("input");
261                    inits.add_punc('.');
262                    inits.add_ident(&get_endian_method_for_varbl(ds, true, config.big_endian));
263                    inits.add_parens(TokenStream::new());
264                    inits.add_punc('?');
265                    inits.add_punc(',');
266                }
267            },
268            Err(_e) => {
269                // <ty as irox_structs::Struct>::parse_from(input)?;
270                let ty = x.ty.to_token_stream();
271                inits.add_ident(&ident.to_string());
272                inits.add_punc(':');
273                inits.extend::<TokenStream>(
274                    quote! {
275                        <#ty as irox_structs::Struct>::parse_from(input)?,
276                    }
277                    .into(),
278                );
279            }
280        }
281    }
282
283    let mut method = TokenStream::new();
284    method.add_ident("Ok");
285    method.add_parens({
286        let mut ts = TokenStream::new();
287        ts.add_ident("Self");
288        ts.wrap_braces(inits);
289        ts
290    });
291    ts.wrap_braces(method);
292    ts
293}
294
295#[derive(Default)]
296struct StructSizing {
297    size: usize,
298}
299
300#[proc_macro_derive(Struct, attributes(little_endian, big_endian, strict_sizing))]
301pub fn struct_derive(input: TokenStream) -> TokenStream {
302    let input = parse_macro_input!(input as DeriveInput);
303    let mut config = Config {
304        big_endian: true,
305        strict_sizing: false,
306    };
307
308    for attr in &input.attrs {
309        let Ok(ident) = attr.meta.path().require_ident() else {
310            return irox_derive_helpers::compile_error(
311                &attr,
312                "This attribute is unnamed.".to_string(),
313            );
314        };
315        if ident.eq("little_endian") {
316            config.big_endian = false;
317        } else if ident.eq("big_endian") {
318            config.big_endian = true;
319        } else if ident.eq("strict_sizing") {
320            config.strict_sizing = true;
321        }
322    }
323
324    let struct_name = &input.ident;
325
326    let Data::Struct(s) = input.data else {
327        return irox_derive_helpers::compile_error(&input, "Can only derive on struct type");
328    };
329    let Fields::Named(n) = s.fields else {
330        return irox_derive_helpers::compile_error(&s.fields, "Can only derive on named fields.");
331    };
332
333    let mut ts = TokenStream::new();
334    let mut sizing = StructSizing::default();
335    ts.add_ident("impl");
336    ts.add_path(&["irox_structs", "Struct"]);
337    ts.add_ident("for");
338    ts.add_ident(&struct_name.to_string());
339    ts.wrap_braces({
340        let mut ts = TokenStream::new();
341        ts.add_ident("type");
342        ts.add_ident("ImplType");
343        ts.add_punc('=');
344        ts.add_ident(&struct_name.to_string());
345        ts.add_punc(';');
346
347        ts.extend(create_write_to_fn(&n, &config, &mut sizing));
348        ts.extend(create_parse_from_fn(&n, &config));
349        ts
350    });
351    if config.strict_sizing {
352        ts.add_ident("impl");
353        ts.add_ident(&struct_name.to_string());
354        ts.wrap_braces({
355            let mut ts = TokenStream::new();
356            ts.add_ident("pub");
357            ts.add_ident("const");
358            ts.add_ident("STRUCT_SIZE");
359            ts.add_punc(':');
360            ts.add_ident("usize");
361            ts.add_punc('=');
362            ts.add_literal(Literal::usize_suffixed(sizing.size));
363            ts.add_punc(';');
364            ts
365        });
366    }
367    ts
368}
369
370#[proc_macro_derive(Shared, attributes(shared))]
371pub fn shared_derive(input: TokenStream) -> TokenStream {
372    shared::shared_derive(input)
373}