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}