Skip to main content

padlock_macros/
lib.rs

1// padlock-macros — compile-time struct layout assertions.
2//
3// Provides:
4//   #[padlock::assert_no_padding]   — fails to compile if the struct has padding
5//   #[padlock::assert_size(N)]      — fails to compile if size_of != N
6
7extern crate proc_macro;
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::{format_ident, quote};
12use syn::{Fields, ItemStruct, parse_macro_input};
13
14// ── #[padlock::assert_no_padding] ────────────────────────────────────────────
15
16/// Attribute macro that causes a **compile-time error** if the struct has
17/// any padding bytes.
18///
19/// Padding is detected by asserting that `size_of::<Struct>()` equals the sum
20/// of `size_of::<FieldType>()` for every field. If the compiler inserts padding
21/// (alignment gaps or trailing bytes), the sizes will differ and the assertion
22/// fails.
23///
24/// # Example
25///
26/// ```rust,ignore
27/// #[padlock::assert_no_padding]
28/// #[repr(C)]
29/// struct Packed {
30///     a: u64,
31///     b: u32,
32///     c: u32,
33/// }
34/// // ✓ compiles: 8 + 4 + 4 = 16 = size_of::<Packed>()
35///
36/// #[padlock::assert_no_padding]
37/// struct Padded {
38///     a: u8,
39///     b: u64,  // ← 7 bytes of padding inserted before this
40/// }
41/// // ✗ compile error: 1 + 8 = 9 ≠ 16 = size_of::<Padded>()
42/// ```
43///
44/// # Limitations
45///
46/// - Works for named-field structs only (tuple structs and unit structs are
47///   accepted without checking).
48/// - Generic structs are accepted but the assertion uses concrete field type
49///   tokens; monomorphisation errors may give confusing messages.
50/// - The check operates on `size_of` values, which means repr(Rust) structs
51///   where the compiler reorders fields but eliminates padding will still pass.
52#[proc_macro_attribute]
53pub fn assert_no_padding(_attr: TokenStream, item: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(item as ItemStruct);
55    let expanded = emit_no_padding_assertion(&input);
56    TokenStream::from(quote! {
57        #input
58        #expanded
59    })
60}
61
62fn emit_no_padding_assertion(input: &ItemStruct) -> TokenStream2 {
63    let struct_name = &input.ident;
64    let const_ident = format_ident!(
65        "_PADLOCK_ASSERT_NO_PADDING_{}",
66        struct_name.to_string().to_uppercase()
67    );
68
69    let field_types: Vec<_> = match &input.fields {
70        Fields::Named(nf) => nf.named.iter().map(|f| &f.ty).collect(),
71        Fields::Unnamed(uf) => uf.unnamed.iter().map(|f| &f.ty).collect(),
72        Fields::Unit => {
73            // Unit structs have no fields; size_of is 0, nothing to check.
74            return quote! {
75                const #const_ident: () = ();
76            };
77        }
78    };
79
80    if field_types.is_empty() {
81        return quote! {
82            const #const_ident: () = ();
83        };
84    }
85
86    // Build: size_of::<F1>() + size_of::<F2>() + ...
87    let field_sizes = field_types.iter().map(|ty| {
88        quote! { ::std::mem::size_of::<#ty>() }
89    });
90
91    // The assertion: size_of::<Struct>() == sum(size_of::<FieldType>())
92    // If this fails, the compiler prints something like:
93    //   assertion `left == right` failed
94    //   left: 16
95    //   right: 9
96    // Combined with the const name it's clear which struct triggered it.
97    quote! {
98        const #const_ident: () = {
99            let struct_size = ::std::mem::size_of::<#struct_name>();
100            let field_sum: usize = 0 #( + #field_sizes )*;
101            assert!(
102                struct_size == field_sum,
103                concat!(
104                    "padlock: struct `",
105                    stringify!(#struct_name),
106                    "` has padding — size_of != sum of field sizes. ",
107                    "Reorder fields by descending alignment or add #[repr(packed)]."
108                )
109            );
110        };
111    }
112}
113
114// ── #[padlock::assert_size(N)] ────────────────────────────────────────────────
115
116/// Attribute macro that causes a **compile-time error** if the struct's
117/// `size_of` is not exactly `N` bytes.
118///
119/// Useful for locking down a struct's total size against accidental growth.
120///
121/// # Example
122///
123/// ```rust,ignore
124/// #[padlock::assert_size(64)]
125/// struct CacheLine {
126///     data: [u8; 64],
127/// }
128/// ```
129#[proc_macro_attribute]
130pub fn assert_size(attr: TokenStream, item: TokenStream) -> TokenStream {
131    let input = parse_macro_input!(item as ItemStruct);
132    let expected: syn::LitInt = match syn::parse(attr) {
133        Ok(n) => n,
134        Err(e) => return e.to_compile_error().into(),
135    };
136
137    let struct_name = &input.ident;
138    let const_ident = format_ident!(
139        "_PADLOCK_ASSERT_SIZE_{}",
140        struct_name.to_string().to_uppercase()
141    );
142
143    let expanded = quote! {
144        #input
145
146        const #const_ident: () = {
147            let actual = ::std::mem::size_of::<#struct_name>();
148            let expected: usize = #expected;
149            assert!(
150                actual == expected,
151                concat!(
152                    "padlock: struct `",
153                    stringify!(#struct_name),
154                    "` has unexpected size. Check for accidental padding or field additions."
155                )
156            );
157        };
158    };
159
160    TokenStream::from(expanded)
161}
162
163// ── tests ──────────────────────────────────────────────────────────────────────
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use syn::parse_quote;
169
170    #[test]
171    fn no_padding_assertion_for_unit_struct_is_empty_const() {
172        let item: ItemStruct = parse_quote! { struct Unit; };
173        let ts = emit_no_padding_assertion(&item);
174        let s = ts.to_string();
175        // Should produce a trivial `const ...: () = ();`
176        assert!(s.contains("()"));
177        // Should NOT produce a size_of assertion
178        assert!(!s.contains("size_of"));
179    }
180
181    #[test]
182    fn no_padding_assertion_contains_struct_name() {
183        let item: ItemStruct = parse_quote! {
184            struct MyStruct {
185                a: u64,
186                b: u32,
187            }
188        };
189        let ts = emit_no_padding_assertion(&item);
190        let s = ts.to_string();
191        assert!(
192            s.contains("MY_STRUCT") || s.contains("MyStruct") || s.contains("my_struct"),
193            "expected struct name reference in: {s}"
194        );
195    }
196
197    #[test]
198    fn no_padding_assertion_includes_size_of_fields() {
199        let item: ItemStruct = parse_quote! {
200            struct Foo {
201                a: u8,
202                b: u64,
203            }
204        };
205        let ts = emit_no_padding_assertion(&item);
206        let s = ts.to_string();
207        assert!(s.contains("size_of"), "expected size_of in: {s}");
208        assert!(s.contains("u8"), "expected u8 in: {s}");
209        assert!(s.contains("u64"), "expected u64 in: {s}");
210    }
211
212    #[test]
213    fn no_padding_assertion_empty_named_fields_is_trivial() {
214        // A struct with no fields (struct Foo {}) — edge case
215        let item: ItemStruct = parse_quote! { struct Empty {} };
216        let ts = emit_no_padding_assertion(&item);
217        let s = ts.to_string();
218        assert!(
219            !s.contains("size_of"),
220            "empty struct should not generate size_of check"
221        );
222    }
223
224    #[test]
225    fn no_padding_const_name_is_uppercase() {
226        let item: ItemStruct = parse_quote! {
227            struct FooBar { x: u32 }
228        };
229        let ts = emit_no_padding_assertion(&item);
230        let s = ts.to_string();
231        // The generated const ident should be FOOBAR (uppercase of struct name)
232        assert!(s.contains("FOOBAR"), "expected FOOBAR in const name: {s}");
233    }
234
235    #[test]
236    fn assert_message_contains_struct_name() {
237        let item: ItemStruct = parse_quote! {
238            struct Suspect { a: u8, b: u64 }
239        };
240        let ts = emit_no_padding_assertion(&item);
241        let s = ts.to_string();
242        assert!(
243            s.contains("Suspect"),
244            "expected struct name in assertion message: {s}"
245        );
246    }
247}