Skip to main content

rta_derive/
lib.rs

1#![doc = include_str!("../README.md")]
2
3extern crate proc_macro;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{parse_macro_input, Data, DeriveInput};
8
9/// Derives the `rta::RTA` trait for a struct `T`
10///
11/// ## Important
12///
13/// - type `T` must use `repr(C)`
14/// - should not implement Drop (to avoid undefined behaviour)
15///
16/// ## Why?
17///
18/// `#[derive(RTA)]` implementation, computes `core::mem::size_of::<T>()` and a compile time `HASH`,
19/// which is used as unique and deterministic id for a given type `T`
20///
21/// This is to track any changes in the implementation of type `T`
22#[proc_macro_derive(RTA)]
23pub fn derive_rta(input: TokenStream) -> TokenStream {
24    let input = parse_macro_input!(input as DeriveInput);
25    let ident = input.ident;
26
27    if !has_repr_c(&input.attrs) {
28        return syn::Error::new_spanned(ident, "RTA derive error: struct must use #[repr(C)] for stable layout")
29            .to_compile_error()
30            .into();
31    }
32
33    let mut hash = hasher::hash(0, ident.to_string().as_bytes());
34
35    let fields = match input.data {
36        Data::Struct(s) => s.fields,
37        _ => {
38            return syn::Error::new_spanned(ident, "RTA derive error: RTA can only be derived for a struct")
39                .to_compile_error()
40                .into();
41        }
42    };
43
44    let field_sizes: Vec<_> = fields
45        .iter()
46        .map(|f| {
47            let ty = &f.ty;
48            quote! { core::mem::size_of::<#ty>() }
49        })
50        .collect();
51
52    for field in fields {
53        if let Some(ident) = field.ident {
54            hash = hasher::hash(hash, b"|");
55            hash = hasher::hash(hash, ident.to_string().as_bytes());
56        }
57
58        let ty = field.ty;
59        let ty_str = quote!(#ty).to_string();
60
61        hash = hasher::hash(hash, b":");
62        hash = hasher::hash(hash, ty_str.as_bytes());
63    }
64
65    let expanded = quote! {
66        unsafe impl rta::RTA for #ident {
67            const HASH: u64 = #hash;
68            const SIZE: usize = core::mem::size_of::<Self>();
69        }
70
71        const _: () = {
72            let field_sum =
73                0 #( + #field_sizes )*;
74
75            assert!(
76                core::mem::size_of::<#ident>() > 0,
77                concat!(
78                    "RTA derive error: struct `",
79                    stringify!(#ident),
80                    "` cannot be zero-sized"
81                )
82            );
83
84            assert!(
85                core::mem::size_of::<#ident>() == field_sum,
86                concat!(
87                    "RTA derive error: struct `",
88                    stringify!(#ident),
89                    "` contains padding. ",
90                    "All fields must pack exactly with #[repr(C)]. ",
91                    "Reorder fields or add explicit padding fields."
92                )
93            );
94
95            assert!(
96                core::mem::size_of::<#ident>() % 8 == 0,
97                concat!(
98                    "RTA derive error: struct `",
99                    stringify!(#ident),
100                    "` size must be a multiple of 8 bytes ",
101                    "Add padding fields or reorder members."
102                )
103            );
104        };
105    };
106
107    TokenStream::from(expanded)
108}
109
110fn has_repr_c(attrs: &[syn::Attribute]) -> bool {
111    attrs.iter().any(|attr| {
112        if !attr.path().is_ident("repr") {
113            return false;
114        }
115
116        attr.parse_args_with(syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated)
117            .map(|idents| idents.iter().any(|i| i == "C"))
118            .unwrap_or(false)
119    })
120}
121
122mod hasher {
123    const FNV_PRIME: u64 = 0x100000001b3;
124    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
125
126    /// custom impl of `fnv1a` hasher
127    pub const fn hash(mut hash: u64, bytes: &[u8]) -> u64 {
128        if hash == 0 {
129            hash = FNV_OFFSET;
130        }
131
132        let mut i = 0;
133        while i < bytes.len() {
134            hash ^= bytes[i] as u64;
135            hash = hash.wrapping_mul(FNV_PRIME);
136            i += 1;
137        }
138
139        hash
140    }
141
142    #[cfg(test)]
143    mod tests {
144        use super::hash;
145
146        #[test]
147        fn same_input_same_hash() {
148            let h1 = hash(0, b"u64");
149            let h2 = hash(0, b"u64");
150            assert_eq!(h1, h2);
151        }
152
153        #[test]
154        fn different_inputs_different_hashes() {
155            let h1 = hash(0, b"u64");
156            let h2 = hash(0, b"u32");
157            assert_ne!(h1, h2);
158        }
159
160        #[test]
161        fn order_sensitive_hashing() {
162            let h1 = hash(0, b"u64u32");
163            let h2 = hash(0, b"u32u64");
164            assert_ne!(h1, h2);
165        }
166
167        #[test]
168        fn hasher_incremental_equals_one_shot() {
169            let mut h = hash(0, b"u64");
170            h = hash(h, b"u32");
171
172            let one_shot = hash(0, b"u64u32");
173
174            assert_eq!(h, one_shot);
175        }
176
177        #[test]
178        fn hasher_empty_is_offset_basis() {
179            let h = hash(0, b"");
180            assert_eq!(h, 0xcbf29ce484222325);
181        }
182    }
183}