bones_schema_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Punct, Spacing, TokenStream as TokenStream2, TokenTree as TokenTree2};
3use quote::{format_ident, quote, quote_spanned, spanned::Spanned};
4use venial::{GenericBound, StructFields};
5
6/// Helper macro to bail out of the macro with a compile error.
7macro_rules! throw {
8    ($hasSpan:expr, $err:literal) => {
9        let span = $hasSpan.__span();
10        return quote_spanned!(span =>
11            compile_error!($err);
12        ).into();
13    };
14}
15
16/// Derive macro for the HasSchema trait.
17///
18/// ## Usage with `#[repr(C)]`
19/// HasSchema works with the `#[repr(C)]` annotation to fully implement its
20/// features.
21///
22/// If there is no `#[repr(C)]` annotation, the SchemaKind of your type's schema
23/// will be Opaque.
24///
25/// This means if you don't know the kind of type, like in the case of a
26/// `SchemaBox`, you'll be unable to read the fields.
27///
28/// This applies to bones' lua scripting since `SchemaBox` is effectively the
29/// "lua type".
30///
31/// If you intend a type to be opaque even though it has `#[repr(C)]` you can
32/// use `#[schema(opaque)]` to force an opaque schema representation.
33///
34/// Keep in mind, enums deriving HasSchema with a `#[repr(C)]` annotation must
35/// also specify an enum tag type like `#[repr(u8)]` where `u8` could be either
36/// `u16` or `u32` if you need more than 256 enum variants.
37///
38/// ## no_default & no_clone attributes
39/// HasSchema derive requires the type to implement Default & Clone, if either
40/// of these cannot be implemented you can use the no_default & no_clone schema
41/// attributes respectively to ignore these requirements.
42/// ```ignore
43/// #[derive(HasSchema, Default)]
44/// #[schema(no_clone)]
45/// struct DoesntImplClone;
46///
47/// #[derive(HasSchema, Clone)]
48/// #[schema(no_default)]
49/// struct DoesntImplDefault;
50/// ```
51/// The caveat for putting no_default on your type is that it cannot be created
52/// from a Schema. This is necessary if you want to create your type within a
53/// bones lua script.
54///
55/// Since the fields that need to be initialized to create a complete version of
56/// your type cannot be determined, Schema needs a default function to
57/// initialize the data properly.
58///
59/// The caveat for putting no_clone on your type is that it cannot be cloned in
60/// the form of a SchemaBox.
61///
62/// This is critical in the case of bones' networking which will panic if your
63/// type is in the world and does not implement clone during the network
64/// rollback.
65///
66/// ## hash & eq attributes
67/// In order for a Schema to implement Hash and Eq, the derive requires opt in
68/// attributes instead of the opt out attributes required for Clone and Default
69/// since you are more likely to leave Hash and Eq out than you are to implement
70/// it.
71/// ```ignore
72/// #[derive(HasSchema, Clone, Default, PartialEq, Eq, Hash)]
73/// #[schema(hash, eq)]
74/// struct DoesImplHashAndEq;
75/// ```
76///
77/// ## type_data attribute
78/// This attribute takes an expression and stores that value in what is
79/// basically a type keyed map accessible from your type's Schema.
80///
81/// ## derive_type_data attribute
82/// This attribute is simply a shortcut equivalent to using the type_data
83/// attribute with any type's `FromType<YourHasSchemaType>` implementation like
84/// so:
85/// ```ignore
86/// #[derive(HasSchema, Clone, Default)]
87/// #[type_data(<OtherType as FromType<Data>>::from_type())]
88/// struct Data;
89/// ```
90/// Simply specify a type instead of an expression:
91/// ```ignore
92/// #[derive(HasSchema, Clone, Default)]
93/// #[derive_type_data(OtherType)] // OtherType implements FromType<Data>
94/// struct Data;
95/// ```
96/// ## Known Limitations
97///
98/// Currently it isn't possible to construct a struct that contains itself. For
99/// example, this will not work:
100///
101/// ```ignore
102/// #[derive(HasSchema)]
103/// struct Data {
104///     others: Vec<Data>,
105/// }
106/// ```
107///
108/// If this is a problem for your use-case, please open an issue.
109#[proc_macro_derive(
110    HasSchema,
111    attributes(schema, derive_type_data, type_data, schema_module)
112)]
113pub fn derive_has_schema(input: TokenStream) -> TokenStream {
114    let input = venial::parse_declaration(input.into()).unwrap();
115    let name = input.name().expect("Type must have a name");
116
117    // Get the schema module, reading optionally from the `schema_module` attribute, so that we can
118    // set the module to `crate` when we want to use it within the `bones_schema` crate itself.
119    let schema_mod = input
120        .attributes()
121        .iter()
122        .find_map(|attr| {
123            (attr.path.len() == 1 && attr.path[0].to_string() == "schema_module").then(|| {
124                attr.value
125                    .get_value_tokens()
126                    .iter()
127                    .cloned()
128                    .collect::<TokenStream2>()
129            })
130        })
131        .unwrap_or_else(|| quote!(bones_schema));
132
133    // Get the type datas that have been added and derived
134    let derive_type_data_flags = get_flags_for_attr(&input, "derive_type_data");
135    let type_datas = {
136        let add_derive_type_datas = derive_type_data_flags.into_iter().map(|ty| {
137            let ty = format_ident!("{ty}");
138            quote! {
139                tds.insert(<#ty as #schema_mod::FromType<#name>>::from_type()).unwrap();
140            }
141        });
142        let add_type_datas = input
143            .attributes()
144            .iter()
145            .filter(|x| x.path.len() == 1 && x.path[0].to_string() == "type_data")
146            .map(|x| x.get_value_tokens())
147            .map(|x| x.iter().cloned().collect::<TokenStream2>());
148
149        quote! {
150            {
151                let tds = #schema_mod::alloc::TypeDatas::default();
152                #(#add_derive_type_datas),*
153                #(
154                    tds.insert(#add_type_datas).unwrap();
155                ),*
156                tds
157            }
158        }
159    };
160
161    // Collect repr tags
162    let mut repr_flags = get_flags_for_attr(&input, "repr");
163    repr_flags.iter_mut().for_each(|x| *x = x.to_lowercase());
164    let repr_c = repr_flags.iter().any(|x| x == "c");
165    let primitive_repr = repr_flags.iter().find_map(|x| match x.as_ref() {
166        "u8" => Some(quote!(U8)),
167        "u16" => Some(quote!(U16)),
168        "u32" => Some(quote!(U32)),
169        _ => None,
170    });
171
172    // Collect schema flags
173    let schema_flags = get_flags_for_attr(&input, "schema");
174    let no_clone = schema_flags.iter().any(|x| x.as_str() == "no_clone");
175    let no_default = schema_flags.iter().any(|x| x.as_str() == "no_default");
176    let has_hash = schema_flags.iter().any(|x| x.as_str() == "hash");
177    let has_eq = schema_flags.iter().any(|x| x.as_str() == "eq");
178    let is_opaque = schema_flags.iter().any(|x| x.as_str() == "opaque")
179        || !(repr_c || primitive_repr.is_some());
180
181    // Get the clone and default functions based on the flags
182    let clone_fn = if no_clone {
183        quote!(None)
184    } else {
185        quote!(Some(<Self as #schema_mod::raw_fns::RawClone>::raw_clone_cb()))
186    };
187    let default_fn = if no_default {
188        quote!(None)
189    } else {
190        quote!(Some(<Self as #schema_mod::raw_fns::RawDefault>::raw_default_cb()))
191    };
192    let hash_fn = if !has_hash {
193        quote!(None)
194    } else {
195        quote!(Some(<Self as #schema_mod::raw_fns::RawHash>::raw_hash_cb()))
196    };
197    let eq_fn = if !has_eq {
198        quote!(None)
199    } else {
200        quote!(Some(<Self as #schema_mod::raw_fns::RawEq>::raw_eq_cb()))
201    };
202
203    // Get the schema kind
204    let schema_kind = (|| {
205        if is_opaque {
206            return quote! {
207                {
208                    let layout = ::std::alloc::Layout::new::<Self>();
209                    #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque {
210                        size: layout.size(),
211                        align: layout.align(),
212                    })
213                }
214            };
215        }
216
217        // Helper to parse struct fields from structs or enum variants
218        let parse_struct_fields = |fields: &StructFields| {
219            match fields {
220                venial::StructFields::Tuple(tuple) => tuple
221                    .fields
222                    .iter()
223                    .map(|(field, _)| {
224                        let ty = &field.ty;
225                        quote_spanned! {field.ty.__span() =>
226                            #schema_mod::StructFieldInfo {
227                                name: None,
228                                schema: <#ty as #schema_mod::HasSchema>::schema(),
229                            }
230                        }
231                    })
232                    .collect::<Vec<_>>(),
233                venial::StructFields::Named(named) => named
234                    .fields
235                    .iter()
236                    .map(|(field, _)| {
237                        let name = &field.name;
238                        let ty = &field.ty;
239                        let opaque = field.attributes.iter().any(|attr| {
240                            &attr.path[0].to_string() == "schema"
241                                && &attr.value.get_value_tokens()[0].to_string() == "opaque"
242                        });
243
244                        if opaque {
245                            quote_spanned! {field.ty.__span() =>
246                                #schema_mod::StructFieldInfo {
247                                    name: Some(stringify!(#name).into()),
248                                    schema: {
249                                        let layout = ::std::alloc::Layout::new::<#ty>();
250                                        #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData {
251                                            name: stringify!(#ty).into(),
252                                            full_name: concat!(module_path!(), "::", stringify!(#ty)).into(),
253                                            kind: #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque {
254                                                size: layout.size(),
255                                                align: layout.align(),
256                                            }),
257                                            type_id: Some(std::any::TypeId::of::<#ty>()),
258                                            type_data: #type_datas,
259                                            clone_fn: #clone_fn,
260                                            default_fn: #default_fn,
261                                            eq_fn: #eq_fn,
262                                            hash_fn: #hash_fn,
263                                            drop_fn: Some(<Self as #schema_mod::raw_fns::RawDrop>::raw_drop_cb()),
264                                        })
265                                    },
266                                }
267                            }
268                        } else {
269                            quote_spanned! {field.ty.__span() =>
270                                #schema_mod::StructFieldInfo {
271                                    name: Some(stringify!(#name).into()),
272                                    schema: <#ty as #schema_mod::HasSchema>::schema(),
273                                }
274                            }
275                        }
276                    })
277                    .collect::<Vec<_>>(),
278                venial::StructFields::Unit => Vec::new(),
279            }
280        };
281
282        // Match on the the type we are deriving on and return its SchemaData
283        match &input {
284            venial::Declaration::Struct(s) => {
285                let fields = parse_struct_fields(&s.fields);
286
287                quote! {
288                    #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo {
289                        fields: vec![
290                            #(#fields),*
291                        ]
292                    })
293                }
294            }
295            venial::Declaration::Enum(e) => {
296                let Some(tag_type) = primitive_repr else {
297                    throw!(
298                        e,
299                        "Enums deriving HasSchema with a `#[repr(C)]` annotation \
300                        must also specify an enum tag type like `#[repr(C, u8)]` where \
301                        `u8` could be either `u16` or `u32` if you need more than 256 enum \
302                        variants."
303                    );
304                };
305                let mut variants = Vec::new();
306
307                for v in e.variants.items() {
308                    let name = v.name.to_string();
309                    let variant_schema_name = format!("{}::{}", e.name, name);
310                    let fields = parse_struct_fields(&v.contents);
311
312                    let register_schema = if input.generic_params().is_some() {
313                        quote! {
314                            static S: OnceLock<RwLock<HashMap<TypeId, &'static Schema>>> = OnceLock::new();
315                            let schema = {
316                                S.get_or_init(Default::default)
317                                    .read()
318                                    .get(&TypeId::of::<Self>())
319                                    .copied()
320                            };
321                            schema.unwrap_or_else(|| {
322                                let schema = compute_schema();
323
324                                S.get_or_init(Default::default)
325                                    .write()
326                                    .insert(TypeId::of::<Self>(), schema);
327
328                                schema
329                            })
330                        }
331                    } else {
332                        quote! {
333                            static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new();
334                            S.get_or_init(compute_schema)
335                        }
336                    };
337
338                    variants.push(quote! {
339                        #schema_mod::VariantInfo {
340                            name: #name.into(),
341                            schema: {
342                                let compute_schema = || {
343                                    #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData {
344                                        name: #variant_schema_name.into(),
345                                        full_name: concat!(module_path!(), "::", #variant_schema_name).into(),
346                                        type_id: None,
347                                        kind: #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo {
348                                            fields: vec![
349                                                #(#fields),*
350                                            ]
351                                        }),
352                                        type_data: Default::default(),
353                                        default_fn: None,
354                                        clone_fn: None,
355                                        eq_fn: None,
356                                        hash_fn: None,
357                                        drop_fn: None,
358                                    })
359                                };
360                                #register_schema
361                            }
362                        }
363                    })
364                }
365
366                quote! {
367                    #schema_mod::SchemaKind::Enum(#schema_mod::EnumSchemaInfo {
368                        tag_type: #schema_mod::EnumTagType::#tag_type,
369                        variants: vec![#(#variants),*],
370                    })
371                }
372            }
373            _ => {
374                throw!(
375                    input,
376                    "You may only derive HasSchema for structs and enums."
377                );
378            }
379        }
380    })();
381
382    let schema_register = quote! {
383        #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData {
384            name: stringify!(#name).into(),
385            full_name: concat!(module_path!(), "::", stringify!(#name)).into(),
386            type_id: Some(::std::any::TypeId::of::<Self>()),
387            kind: #schema_kind,
388            type_data: #type_datas,
389            default_fn: #default_fn,
390            clone_fn: #clone_fn,
391            eq_fn: #eq_fn,
392            hash_fn: #hash_fn,
393            drop_fn: Some(<Self as #schema_mod::raw_fns::RawDrop>::raw_drop_cb()),
394        })
395    };
396
397    if let Some(generic_params) = input.generic_params() {
398        let mut sync_send_generic_params = generic_params.clone();
399        for (param, _) in sync_send_generic_params.params.iter_mut() {
400            let clone_bound = if !no_clone { quote!(+ Clone) } else { quote!() };
401            param.bound = Some(GenericBound {
402                tk_colon: Punct::new(':', Spacing::Joint),
403                tokens: quote!(HasSchema #clone_bound ).into_iter().collect(),
404            });
405        }
406        quote! {
407            unsafe impl #sync_send_generic_params #schema_mod::HasSchema for #name #generic_params {
408                fn schema() -> &'static #schema_mod::Schema {
409                    use ::std::sync::{OnceLock};
410                    use ::std::any::TypeId;
411                    use bones_utils::HashMap;
412                    use parking_lot::RwLock;
413                    static S: OnceLock<RwLock<HashMap<TypeId, &'static Schema>>> = OnceLock::new();
414                    let schema = {
415                        S.get_or_init(Default::default)
416                            .read()
417                            .get(&TypeId::of::<Self>())
418                            .copied()
419                    };
420                    schema.unwrap_or_else(|| {
421                        let schema = #schema_register;
422
423                        S.get_or_init(Default::default)
424                            .write()
425                            .insert(TypeId::of::<Self>(), schema);
426
427                        schema
428                    })
429
430                }
431            }
432        }
433    } else {
434        quote! {
435            unsafe impl #schema_mod::HasSchema for #name {
436                fn schema() -> &'static #schema_mod::Schema {
437                    static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new();
438                    S.get_or_init(|| {
439                        #schema_register
440                    })
441                }
442            }
443        }
444    }
445    .into()
446}
447
448//
449// Helpers
450//
451
452/// Look for an attribute with the given name and get all of the comma-separated flags that are
453/// in that attribute.
454///
455/// For example, with the given struct:
456///
457/// ```ignore
458/// #[example(test)]
459/// #[my_attr(hello, world)]
460/// struct Hello;
461/// ```
462///
463/// Calling `get_flags_for_attr("my_attr")` would return `vec!["hello", "world"]`.
464fn get_flags_for_attr(input: &venial::Declaration, attr_name: &str) -> Vec<String> {
465    let attrs = input
466        .attributes()
467        .iter()
468        .filter(|attr| attr.path.len() == 1 && attr.path[0].to_string() == attr_name)
469        .collect::<Vec<_>>();
470    attrs
471        .iter()
472        .map(|attr| match &attr.value {
473            venial::AttributeValue::Group(_, value) => {
474                let mut flags = Vec::new();
475
476                let mut current_flag = proc_macro2::TokenStream::new();
477                for token in value {
478                    match token {
479                        TokenTree2::Punct(x) if x.as_char() == ',' => {
480                            flags.push(current_flag.to_string());
481                            current_flag = Default::default();
482                        }
483                        x => current_flag.extend(std::iter::once(x.clone())),
484                    }
485                }
486                flags.push(current_flag.to_string());
487
488                flags
489            }
490            venial::AttributeValue::Equals(_, _) => {
491                // TODO: Improve macro error message span.
492                panic!("Unsupported attribute format");
493            }
494            venial::AttributeValue::Empty => Vec::new(),
495        })
496        .fold(Vec::new(), |mut acc, item| {
497            acc.extend(item);
498            acc
499        })
500}