From c331cd6566a6ba43169c4351c73195e1e02ccd03 Mon Sep 17 00:00:00 2001 From: Zicklag Date: Mon, 18 Sep 2023 16:28:04 +0000 Subject: [PATCH] feat: add `Maybe`, fix `HasSchema` impls for generic types, and impl `HasSchema` derive for generic types. (#199) --- framework_crates/bones_asset/src/asset.rs | 32 ++ framework_crates/bones_asset/src/lib.rs | 111 ++++ framework_crates/bones_asset/src/server.rs | 27 +- .../bones_schema/macros/src/lib.rs | 502 ++++++++++-------- .../bones_schema/src/alloc/map.rs | 22 +- .../bones_schema/src/alloc/vec.rs | 22 +- framework_crates/bones_schema/src/ptr.rs | 22 +- framework_crates/bones_schema/tests/tests.rs | 12 + 8 files changed, 503 insertions(+), 247 deletions(-) diff --git a/framework_crates/bones_asset/src/asset.rs b/framework_crates/bones_asset/src/asset.rs index 116cc5c13f..c79baacad1 100644 --- a/framework_crates/bones_asset/src/asset.rs +++ b/framework_crates/bones_asset/src/asset.rs @@ -252,6 +252,38 @@ pub trait AssetLoader: Sync + Send + 'static { fn load(&self, ctx: AssetLoadCtx, bytes: &[u8]) -> anyhow::Result; } +/// A custom asset loader implementation for a metadata asset. +/// +/// This is similar in purpose to implementing [`AssetLoader`], but instead of loading from bytes, +/// it loads from the deserialized [`SchemaRefMut`] of a metadata asset and must be added as a +/// schema type data. +#[derive(HasSchema)] +#[schema(no_clone, no_default)] +pub struct SchemaMetaAssetLoader( + pub fn( + ctx: &mut MetaAssetLoadCtx, + ptr: SchemaRefMut<'_, '_>, + deserialzer: &mut dyn erased_serde::Deserializer, + ) -> anyhow::Result<()>, +); + +impl SchemaMetaAssetLoader { + /// Load the asset + pub fn load<'a, 'de, D>( + &self, + ctx: &mut MetaAssetLoadCtx, + ptr: SchemaRefMut<'a, 'a>, + deserializer: D, + ) -> Result<(), erased_serde::Error> + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + let mut de = ::erase(deserializer); + (self.0)(ctx, ptr, &mut de).map_err(|e| erased_serde::Error::custom(e.to_string())) + } +} + /// The kind of asset a type represents. #[derive(HasSchema)] #[schema(opaque, no_default, no_clone)] diff --git a/framework_crates/bones_asset/src/lib.rs b/framework_crates/bones_asset/src/lib.rs index 6a76929fd2..4e9c6c994b 100644 --- a/framework_crates/bones_asset/src/lib.rs +++ b/framework_crates/bones_asset/src/lib.rs @@ -5,6 +5,8 @@ #![cfg_attr(doc, allow(unknown_lints))] #![deny(rustdoc::all)] +use serde::{de::DeserializeSeed, Deserializer}; + /// Helper to export the same types in the crate root and in the prelude. macro_rules! pub_use { () => { @@ -19,6 +21,7 @@ pub_use!(); /// The prelude. pub mod prelude { pub_use!(); + pub use super::{Maybe, Maybe::*}; } mod asset; @@ -28,3 +31,111 @@ mod io; mod parse; mod path; mod server; + +/// An equivalent to [`Option`] that has a stable memory layout and implements [`HasSchema`]. +#[derive(HasSchema, Clone, Default, Debug)] +#[type_data(SchemaMetaAssetLoader(maybe_loader))] +#[repr(C, u8)] +pub enum Maybe { + /// The value is not set. + #[default] + Unset, + /// The value is set. + Set(T), +} + +impl Maybe { + /// Convert this [`Maybe`] into an [`Option`]. + pub fn option(self) -> Option { + self.into() + } +} + +impl From> for Option { + fn from(value: Maybe) -> Self { + match value { + Maybe::Set(s) => Some(s), + Maybe::Unset => None, + } + } +} + +impl From> for Maybe { + fn from(value: Option) -> Self { + match value { + Some(s) => Maybe::Set(s), + None => Maybe::Unset, + } + } +} + +fn maybe_loader( + ctx: &mut MetaAssetLoadCtx, + ptr: SchemaRefMut<'_, '_>, + deserialzer: &mut dyn erased_serde::Deserializer, +) -> anyhow::Result<()> { + deserialzer.deserialize_option(MaybeVisitor { ctx, ptr })?; + + Ok(()) +} + +struct MaybeVisitor<'a, 'srv> { + ctx: &'a mut MetaAssetLoadCtx<'srv>, + ptr: SchemaRefMut<'a, 'a>, +} + +impl<'a, 'srv, 'de> serde::de::Visitor<'de> for MaybeVisitor<'a, 'srv> { + type Value = (); + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "an optional value") + } + + fn visit_unit(self) -> Result + where + E: serde::de::Error, + { + Ok(()) + } + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(()) + } + + fn visit_some(self, deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Write the enum discriminant for the `Set` variant + // SOUND: we know the discriminant due to the `#[repr(C, u8)]` annotation. + unsafe { + self.ptr.as_ptr().write(1); + } + + // Get the pointer to the enum value + let value_offset = self.ptr.schema().field_offsets()[0].1; + // NOTE: we take the schema of the first argument of the second enum variant of the + // [`Maybe`] enum because we know that the `Set` variant only has one argument at offset 0 + // and we actually want to deserialize the inner type, not a typle of length zero. + let value_schema = self.ptr.schema().kind.as_enum().unwrap().variants[1] + .schema + .kind + .as_struct() + .unwrap() + .fields[0] + .schema; + // SOUND: the schema asserts this is valid. + let value_ref = unsafe { + SchemaRefMut::from_ptr_schema(self.ptr.as_ptr().add(value_offset), value_schema) + }; + + // Load the enum value + SchemaPtrLoadCtx { + ctx: self.ctx, + ptr: value_ref, + } + .deserialize(deserializer) + } +} diff --git a/framework_crates/bones_asset/src/server.rs b/framework_crates/bones_asset/src/server.rs index 181c426552..85978b2f4a 100644 --- a/framework_crates/bones_asset/src/server.rs +++ b/framework_crates/bones_asset/src/server.rs @@ -609,16 +609,22 @@ fn path_is_metadata(path: &Path) -> bool { ext == "yaml" || ext == "yml" || ext == "json" } -use metadata::*; +pub use metadata::*; mod metadata { use serde::de::{DeserializeSeed, Error, VariantAccess, Visitor}; use super::*; + /// Context provided while loading a metadata asset. pub struct MetaAssetLoadCtx<'srv> { + /// The asset server. pub server: &'srv mut AssetServer, + /// The dependency list of this asset. This should be updated by asset loaders as + /// dependencies are added. pub dependencies: &'srv mut Vec, + /// The location that the asset is being loaded from. pub loc: AssetLocRef<'srv>, + /// The schema of the asset being loaded. pub schema: &'static Schema, } @@ -642,9 +648,12 @@ mod metadata { } } - struct SchemaPtrLoadCtx<'a, 'srv, 'ptr, 'prnt> { - ctx: &'a mut MetaAssetLoadCtx<'srv>, - ptr: SchemaRefMut<'ptr, 'prnt>, + /// The load context for a [`SchemaRefMut`]. + pub struct SchemaPtrLoadCtx<'a, 'srv, 'ptr, 'prnt> { + /// The metadata asset load context. + pub ctx: &'a mut MetaAssetLoadCtx<'srv>, + /// The pointer to load. + pub ptr: SchemaRefMut<'ptr, 'prnt>, } impl<'a, 'srv, 'ptr, 'prnt, 'de> DeserializeSeed<'de> for SchemaPtrLoadCtx<'a, 'srv, 'ptr, 'prnt> { @@ -679,8 +688,14 @@ mod metadata { return Ok(()); } - // Use custom deserialize implementation if present. - if let Some(schema_deserialize) = self.ptr.schema().type_data.get::() + // Use custom asset load or deserialize implementation if present. + if let Some(custom_loader) = self.ptr.schema().type_data.get::() + { + return custom_loader + .load(self.ctx, self.ptr, deserializer) + .map_err(|e| D::Error::custom(e.to_string())); + } else if let Some(schema_deserialize) = + self.ptr.schema().type_data.get::() { return schema_deserialize.deserialize(self.ptr, deserializer); } diff --git a/framework_crates/bones_schema/macros/src/lib.rs b/framework_crates/bones_schema/macros/src/lib.rs index 53caf32a28..d2bb3a94d6 100644 --- a/framework_crates/bones_schema/macros/src/lib.rs +++ b/framework_crates/bones_schema/macros/src/lib.rs @@ -1,7 +1,7 @@ use proc_macro::TokenStream; -use proc_macro2::{TokenStream as TokenStream2, TokenTree as TokenTree2}; +use proc_macro2::{Punct, Spacing, TokenStream as TokenStream2, TokenTree as TokenTree2}; use quote::{format_ident, quote, quote_spanned, spanned::Spanned}; -use venial::StructFields; +use venial::{GenericBound, StructFields}; /// Helper macro to bail out of the macro with a compile error. macro_rules! throw { @@ -64,51 +64,32 @@ macro_rules! throw { /// ``` /// /// If this is a problem for your use-case, please open an issue. -#[proc_macro_derive(HasSchema, attributes(schema, derive_type_data, type_data))] +#[proc_macro_derive( + HasSchema, + attributes(schema, derive_type_data, type_data, schema_module) +)] pub fn derive_has_schema(input: TokenStream) -> TokenStream { let input = venial::parse_declaration(input.into()).unwrap(); let name = input.name().expect("Type must have a name"); - let schema_mod = quote!(bones_schema); - let get_flags_for_attr = |attr_name: &str| { - let attrs = input - .attributes() - .iter() - .filter(|attr| attr.path.len() == 1 && attr.path[0].to_string() == attr_name) - .collect::>(); - attrs - .iter() - .map(|attr| match &attr.value { - venial::AttributeValue::Group(_, value) => { - let mut flags = Vec::new(); - - let mut current_flag = proc_macro2::TokenStream::new(); - for token in value { - match token { - TokenTree2::Punct(x) if x.as_char() == ',' => { - flags.push(current_flag.to_string()); - current_flag = Default::default(); - } - x => current_flag.extend(std::iter::once(x.clone())), - } - } - flags.push(current_flag.to_string()); - - flags - } - venial::AttributeValue::Equals(_, _) => { - // TODO: Improve macro error message span. - panic!("Unsupported attribute format"); - } - venial::AttributeValue::Empty => Vec::new(), + // Get the schema module, reading optionally from the `schema_module` attribute, so that we can + // set the module to `crate` when we want to use it within the `bones_schema` crate itself. + let schema_mod = input + .attributes() + .iter() + .find_map(|attr| { + (attr.path.len() == 1 && attr.path[0].to_string() == "schema_module").then(|| { + attr.value + .get_value_tokens() + .iter() + .cloned() + .collect::() }) - .fold(Vec::new(), |mut acc, item| { - acc.extend(item); - acc - }) - }; + }) + .unwrap_or_else(|| quote!(bones_schema)); - let derive_type_data_flags = get_flags_for_attr("derive_type_data"); + // Get the type datas that have been added and derived + let derive_type_data_flags = get_flags_for_attr(&input, "derive_type_data"); let type_datas = { let add_derive_type_datas = derive_type_data_flags.into_iter().map(|ty| { let ty = format_ident!("{ty}"); @@ -135,11 +116,7 @@ pub fn derive_has_schema(input: TokenStream) -> TokenStream { } }; - let schema_flags = get_flags_for_attr("schema"); - - let no_clone = schema_flags.iter().any(|x| x.as_str() == "no_clone"); - let no_default = schema_flags.iter().any(|x| x.as_str() == "no_default"); - + // Try to parse a `#[repr(C)]` or `#[repr(C, u8/u16/u32)]` attribute let mut repr_c = false; let mut repr_c_enum_tag = None; input.attributes().iter().for_each(|attr| { @@ -167,8 +144,13 @@ pub fn derive_has_schema(input: TokenStream) -> TokenStream { } }); + // Collect schema flags + let schema_flags = get_flags_for_attr(&input, "schema"); + let no_clone = schema_flags.iter().any(|x| x.as_str() == "no_clone"); + let no_default = schema_flags.iter().any(|x| x.as_str() == "no_default"); let is_opaque = schema_flags.iter().any(|x| x.as_str() == "opaque") || !repr_c; + // Get the clone and default functions based on the flags let clone_fn = if no_clone { quote!(None) } else { @@ -180,207 +162,275 @@ pub fn derive_has_schema(input: TokenStream) -> TokenStream { quote!(Some(::raw_default)) }; - if is_opaque { - return quote! { - unsafe impl #schema_mod::HasSchema for #name { - fn schema() -> &'static #schema_mod::Schema { - static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new(); - S.get_or_init(|| { - let layout = std::alloc::Layout::new::(); - #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { - name: stringify!(#name).into(), - full_name: concat!(module_path!(), stringify!(#name)).into(), - type_id: Some(std::any::TypeId::of::()), - clone_fn: #clone_fn, - default_fn: #default_fn, - drop_fn: Some(::raw_drop), - // TODO: Allow deriving `hash_fn` and `eq_fn` for `HasSchema`. - // We need to come up with a strategy for deriving the `Hash` and `Eq` - // functions. There are a couple ways of thinking about this. If the - // struct is #[repr(C)] and all of it's fields implement hash and eq, - // then we can automatically implement hash and EQ using all of the - // field's implementations, without having to have the rust struct - // actually implement either. On the other hand, we could have the rust - // struct be required to derive hash and/or eq, and then we just use the - // `RawHash` and `RawEq` traits to get the functions. I think that's - // probably the right direction, but we need to come up with an - // attribute syntax to tell the derive macro to use the Hash and Eq - // rust implmentations. Bevy used `#[reflect(Hash)]`, and we already - // have `#[schema(no_default)]` so maybe we just add a couple flags like - // `#[schema(hash, eq)]` if you want to use the Rust structs - // `Hash` and `Eq` impls. - eq_fn: None, - hash_fn: None, - kind: #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque { - size: layout.size(), - align: layout.align(), - }), - type_data: #type_datas, - }) + // Get the schema kind + let schema_kind = (|| { + if is_opaque { + return quote! { + { + let layout = ::std::alloc::Layout::new::(); + #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque { + size: layout.size(), + align: layout.align(), }) } - } + }; } - .into(); - } - let parse_struct_fields = |fields: &StructFields| { - match fields { - venial::StructFields::Tuple(tuple) => tuple - .fields - .iter() - .map(|(field, _)| { - let ty = &field.ty; - quote_spanned! {field.ty.__span() => - #schema_mod::StructFieldInfo { - name: None, - schema: <#ty as #schema_mod::HasSchema>::schema(), - } + // Helper to parse struct fields from structs or enum variants + let parse_struct_fields = |fields: &StructFields| { + match fields { + venial::StructFields::Tuple(tuple) => tuple + .fields + .iter() + .map(|(field, _)| { + let ty = &field.ty; + quote_spanned! {field.ty.__span() => + #schema_mod::StructFieldInfo { + name: None, + schema: <#ty as #schema_mod::HasSchema>::schema(), + } + } + }) + .collect::>(), + venial::StructFields::Named(named) => named + .fields + .iter() + .map(|(field, _)| { + let name = &field.name; + let ty = &field.ty; + let opaque = field.attributes.iter().any(|attr| { + &attr.path[0].to_string() == "schema" + && &attr.value.get_value_tokens()[0].to_string() == "opaque" + }); + + if opaque { + quote_spanned! {field.ty.__span() => + #schema_mod::StructFieldInfo { + name: Some(stringify!(#name).into()), + schema: { + let layout = ::std::alloc::Layout::new::<#ty>(); + #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { + name: stringify!(#ty).into(), + full_name: concat!(module_path!(), stringify!(#ty)).into(), + kind: #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque { + size: layout.size(), + align: layout.align(), + }), + type_id: Some(std::any::TypeId::of::<#ty>()), + type_data: #type_datas, + clone_fn: #clone_fn, + default_fn: #default_fn, + eq_fn: None, + hash_fn: None, + drop_fn: Some(::raw_drop), + }) + }, + } + } + } else { + quote_spanned! {field.ty.__span() => + #schema_mod::StructFieldInfo { + name: Some(stringify!(#name).into()), + schema: <#ty as #schema_mod::HasSchema>::schema(), + } + } + } + }) + .collect::>(), + venial::StructFields::Unit => Vec::new(), + } + }; + + // Match on the the type we are deriving on and return its SchemaData + match &input { + venial::Declaration::Struct(s) => { + let fields = parse_struct_fields(&s.fields); + + quote! { + #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo { + fields: vec![ + #(#fields),* + ] + }) } - }) - .collect::>(), - venial::StructFields::Named(named) => named - .fields - .iter() - .map(|(field, _)| { - let name = &field.name; - let ty = &field.ty; - let opaque = field.attributes.iter().any(|attr| { - &attr.path[0].to_string() == "schema" - && &attr.value.get_value_tokens()[0].to_string() == "opaque" - }); + } + venial::Declaration::Enum(e) => { + let Some(tag_type) = repr_c_enum_tag else { + throw!( + e, + "Enums deriving HasSchema with a `#[repr(C)]` annotation \ + must also specify an enum tag type like `#[repr(C, u8)]` where \ + `u8` could be either `u16` or `u32` if you need more than 256 enum \ + variants." + ); + }; + let mut variants = Vec::new(); - if opaque { - quote_spanned! {field.ty.__span() => - #schema_mod::StructFieldInfo { - name: Some(stringify!(#name).into()), + for v in e.variants.items() { + let name = v.name.to_string(); + let variant_schema_name = + format!("{}::{}::EnumVariantSchemaData", e.name, name); + let fields = parse_struct_fields(&v.contents); + variants.push(quote! { + VariantInfo { + name: #name.into(), schema: { - let layout = ::std::alloc::Layout::new::<#ty>(); - #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { - name: stringify!(#ty).into(), - full_name: concat!(module_path!(), stringify!(#ty)).into(), - kind: #schema_mod::SchemaKind::Primitive(#schema_mod::Primitive::Opaque { - size: layout.size(), - align: layout.align(), - }), - type_id: Some(std::any::TypeId::of::<#ty>()), - type_data: #type_datas, - clone_fn: #clone_fn, - default_fn: #default_fn, - eq_fn: None, - hash_fn: None, - drop_fn: Some(::raw_drop), + static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new(); + S.get_or_init(|| { + #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { + name: #variant_schema_name.into(), + full_name: concat!(module_path!(), #variant_schema_name).into(), + type_id: None, + kind: #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo { + fields: vec![ + #(#fields),* + ] + }), + type_data: Default::default(), + default_fn: None, + clone_fn: None, + eq_fn: None, + hash_fn: None, + drop_fn: None, + }) }) - }, - } - } - } else { - quote_spanned! {field.ty.__span() => - #schema_mod::StructFieldInfo { - name: Some(stringify!(#name).into()), - schema: <#ty as #schema_mod::HasSchema>::schema(), + } } - } + }) } - }) - .collect::>(), - venial::StructFields::Unit => { - Vec::new() - } - } - }; - let schema_kind = match input { - venial::Declaration::Struct(s) => { - let fields = parse_struct_fields(&s.fields); - - quote! { - #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo { - fields: vec![ - #(#fields),* - ] - }) + quote! { + #schema_mod::SchemaKind::Enum(#schema_mod::EnumSchemaInfo { + tag_type: #schema_mod::EnumTagType::#tag_type, + variants: vec![#(#variants),*], + }) + } } - } - venial::Declaration::Enum(e) => { - let Some(tag_type) = repr_c_enum_tag else { + _ => { throw!( - e, - "Enums deriving HasSchema with a `#[repr(C)]` annotation \ - must also specify an enum tag type like `#[repr(C, u8)]` where \ - `u8` could be either `u16` or `u32` if you need more than 256 enum \ - variants." + input, + "You may only derive HasSchema for structs and enums." ); - }; - let mut variants = Vec::new(); - - for v in e.variants.items() { - let name = v.name.to_string(); - let variant_schema_name = format!("{}::{}::EnumVariantSchemaData", e.name, name); - let fields = parse_struct_fields(&v.contents); - variants.push(quote! { - VariantInfo { - name: #name.into(), - schema: { - static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new(); - S.get_or_init(|| { - #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { - name: #variant_schema_name.into(), - full_name: concat!(module_path!(), #variant_schema_name).into(), - type_id: None, - kind: #schema_mod::SchemaKind::Struct(#schema_mod::StructSchemaInfo { - fields: vec![ - #(#fields),* - ] - }), - type_data: Default::default(), - default_fn: None, - clone_fn: None, - eq_fn: None, - hash_fn: None, - drop_fn: None, - }) - }) - } - } - }) } - - quote! { - #schema_mod::SchemaKind::Enum(#schema_mod::EnumSchemaInfo { - tag_type: #schema_mod::EnumTagType::#tag_type, - variants: vec![#(#variants),*], - }) - } - } - _ => { - throw!( - input, - "You may only derive HasSchema for structs and enums." - ); } + })(); + + let schema_register = quote! { + #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { + name: stringify!(#name).into(), + full_name: concat!(module_path!(), stringify!(#name)).into(), + type_id: Some(::std::any::TypeId::of::()), + kind: #schema_kind, + type_data: #type_datas, + default_fn: #default_fn, + clone_fn: #clone_fn, + eq_fn: None, + hash_fn: None, + drop_fn: Some(::raw_drop), + }) }; - quote! { - unsafe impl #schema_mod::HasSchema for #name { - fn schema() -> &'static #schema_mod::Schema { - static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new(); - S.get_or_init(|| { - #schema_mod::registry::SCHEMA_REGISTRY.register(#schema_mod::SchemaData { - name: stringify!(#name).into(), - full_name: concat!(module_path!(), stringify!(#name)).into(), - type_id: Some(std::any::TypeId::of::()), - kind: #schema_kind, - type_data: #type_datas, - default_fn: #default_fn, - clone_fn: #clone_fn, - eq_fn: None, - hash_fn: None, - drop_fn: Some(::raw_drop), + if let Some(generic_params) = input.generic_params() { + let mut sync_send_generic_params = generic_params.clone(); + for (param, _) in sync_send_generic_params.params.iter_mut() { + let clone_bound = if !no_clone { quote!(+ Clone) } else { quote!() }; + param.bound = Some(GenericBound { + tk_colon: Punct::new(':', Spacing::Joint), + tokens: quote!(HasSchema #clone_bound ).into_iter().collect(), + }); + } + quote! { + unsafe impl #sync_send_generic_params #schema_mod::HasSchema for #name #generic_params { + fn schema() -> &'static #schema_mod::Schema { + // TODO: use faster hashmap and rwlocks from bones_utils. + use ::std::sync::{OnceLock}; + use ::std::any::TypeId; + use bones_utils::{HashMap, parking_lot::RwLock}; + static S: OnceLock>> = OnceLock::new(); + let schema = { + S.get_or_init(Default::default) + .read() + .get(&TypeId::of::()) + .copied() + }; + schema.unwrap_or_else(|| { + let schema = #schema_register; + + S.get_or_init(Default::default) + .write() + .insert(TypeId::of::(), schema); + + schema }) - }) + + } + } + } + } else { + quote! { + unsafe impl #schema_mod::HasSchema for #name { + fn schema() -> &'static #schema_mod::Schema { + static S: ::std::sync::OnceLock<&'static #schema_mod::Schema> = ::std::sync::OnceLock::new(); + S.get_or_init(|| { + #schema_register + }) + } } } } .into() } + +// +// Helpers +// + +/// Look for an attribute with the given name and get all of the comma-separated flags that are +/// in that attribute. +/// +/// For example, with the given struct: +/// +/// ```ignore +/// #[example(test)] +/// #[my_attr(hello, world)] +/// struct Hello; +/// ``` +/// +/// Calling `get_flags_for_attr("my_attr")` would return `vec!["hello", "world"]`. +fn get_flags_for_attr(input: &venial::Declaration, attr_name: &str) -> Vec { + let attrs = input + .attributes() + .iter() + .filter(|attr| attr.path.len() == 1 && attr.path[0].to_string() == attr_name) + .collect::>(); + attrs + .iter() + .map(|attr| match &attr.value { + venial::AttributeValue::Group(_, value) => { + let mut flags = Vec::new(); + + let mut current_flag = proc_macro2::TokenStream::new(); + for token in value { + match token { + TokenTree2::Punct(x) if x.as_char() == ',' => { + flags.push(current_flag.to_string()); + current_flag = Default::default(); + } + x => current_flag.extend(std::iter::once(x.clone())), + } + } + flags.push(current_flag.to_string()); + + flags + } + venial::AttributeValue::Equals(_, _) => { + // TODO: Improve macro error message span. + panic!("Unsupported attribute format"); + } + venial::AttributeValue::Empty => Vec::new(), + }) + .fold(Vec::new(), |mut acc, item| { + acc.extend(item); + acc + }) +} diff --git a/framework_crates/bones_schema/src/alloc/map.rs b/framework_crates/bones_schema/src/alloc/map.rs index 4bdc5be789..d255ea8710 100644 --- a/framework_crates/bones_schema/src/alloc/map.rs +++ b/framework_crates/bones_schema/src/alloc/map.rs @@ -6,7 +6,7 @@ use std::{ sync::OnceLock, }; -use bones_utils::{hashbrown::hash_map, HashMap}; +use bones_utils::{default, hashbrown::hash_map, parking_lot::RwLock, HashMap}; use crate::{ prelude::*, @@ -535,9 +535,15 @@ impl Default for SMap { } unsafe impl HasSchema for SMap { fn schema() -> &'static Schema { - static S: OnceLock<&'static Schema> = OnceLock::new(); - S.get_or_init(|| { - SCHEMA_REGISTRY.register(SchemaData { + static S: OnceLock>> = OnceLock::new(); + let schema = { + S.get_or_init(default) + .read() + .get(&TypeId::of::()) + .copied() + }; + schema.unwrap_or_else(|| { + let schema = SCHEMA_REGISTRY.register(SchemaData { name: type_name::().into(), full_name: format!("{}::{}", module_path!(), type_name::()).into(), kind: SchemaKind::Map { @@ -551,7 +557,13 @@ unsafe impl HasSchema for SMap { hash_fn: Some(SchemaVec::raw_hash), eq_fn: Some(SchemaVec::raw_eq), type_data: Default::default(), - }) + }); + + S.get_or_init(default) + .write() + .insert(TypeId::of::(), schema); + + schema }) } } diff --git a/framework_crates/bones_schema/src/alloc/vec.rs b/framework_crates/bones_schema/src/alloc/vec.rs index da426c8ce9..e61114da6c 100644 --- a/framework_crates/bones_schema/src/alloc/vec.rs +++ b/framework_crates/bones_schema/src/alloc/vec.rs @@ -7,7 +7,7 @@ use std::{ sync::OnceLock, }; -use bones_utils::fxhash::FxHasher; +use bones_utils::{default, fxhash::FxHasher, parking_lot::RwLock, HashMap}; use crate::{prelude::*, raw_fns::*}; @@ -614,9 +614,15 @@ impl std::ops::DerefMut for SVec { unsafe impl HasSchema for SVec { fn schema() -> &'static Schema { - static S: OnceLock<&'static Schema> = OnceLock::new(); - S.get_or_init(|| { - SCHEMA_REGISTRY.register(SchemaData { + static S: OnceLock>> = OnceLock::new(); + let schema = { + S.get_or_init(default) + .read() + .get(&TypeId::of::()) + .copied() + }; + schema.unwrap_or_else(|| { + let schema = SCHEMA_REGISTRY.register(SchemaData { name: type_name::().into(), full_name: format!("{}::{}", module_path!(), type_name::()).into(), kind: SchemaKind::Vec(T::schema()), @@ -627,7 +633,13 @@ unsafe impl HasSchema for SVec { hash_fn: Some(SchemaVec::raw_hash), eq_fn: Some(SchemaVec::raw_eq), type_data: Default::default(), - }) + }); + + S.get_or_init(default) + .write() + .insert(TypeId::of::(), schema); + + schema }) } } diff --git a/framework_crates/bones_schema/src/ptr.rs b/framework_crates/bones_schema/src/ptr.rs index f21c39a665..cc16e042d7 100644 --- a/framework_crates/bones_schema/src/ptr.rs +++ b/framework_crates/bones_schema/src/ptr.rs @@ -14,7 +14,7 @@ use crate::{ prelude::*, raw_fns::{RawClone, RawDefault, RawDrop}, }; -use bones_utils::prelude::*; +use bones_utils::{parking_lot::RwLock, prelude::*}; /// An untyped reference that knows the [`Schema`] of the pointee and that can be cast to a matching /// type. @@ -845,9 +845,15 @@ impl std::fmt::Debug for SBox { } unsafe impl HasSchema for SBox { fn schema() -> &'static Schema { - static S: OnceLock<&'static Schema> = OnceLock::new(); - S.get_or_init(|| { - SCHEMA_REGISTRY.register(SchemaData { + static S: OnceLock>> = OnceLock::new(); + let schema = { + S.get_or_init(default) + .read() + .get(&TypeId::of::()) + .copied() + }; + schema.unwrap_or_else(|| { + let schema = SCHEMA_REGISTRY.register(SchemaData { name: type_name::().into(), full_name: format!("{}::{}", module_path!(), type_name::()).into(), kind: SchemaKind::Box(T::schema()), @@ -858,7 +864,13 @@ unsafe impl HasSchema for SBox { hash_fn: Some(SchemaVec::raw_hash), eq_fn: Some(SchemaVec::raw_eq), type_data: Default::default(), - }) + }); + + S.get_or_init(default) + .write() + .insert(TypeId::of::(), schema); + + schema }) } } diff --git a/framework_crates/bones_schema/tests/tests.rs b/framework_crates/bones_schema/tests/tests.rs index d2729993bb..b84e977502 100644 --- a/framework_crates/bones_schema/tests/tests.rs +++ b/framework_crates/bones_schema/tests/tests.rs @@ -313,3 +313,15 @@ fn schema_layout_matches_rust_layout() { } layout_eq!(A, B); } + +#[derive(HasSchema, Clone)] +#[schema(no_default)] +struct HasGeneric(A); + +#[test] +fn generic_has_schema_impls_dont_match() { + assert_ne!(SMap::::schema(), SMap::::schema()); + assert_ne!(SVec::::schema(), SVec::::schema()); + assert_ne!(SBox::::schema(), SBox::::schema()); + assert_ne!(HasGeneric::::schema(), HasGeneric::::schema()); +}