diff options
Diffstat (limited to 'engine-ecs-macros')
| -rw-r--r-- | engine-ecs-macros/Cargo.toml | 14 | ||||
| -rw-r--r-- | engine-ecs-macros/src/lib.rs | 316 |
2 files changed, 330 insertions, 0 deletions
diff --git a/engine-ecs-macros/Cargo.toml b/engine-ecs-macros/Cargo.toml new file mode 100644 index 0000000..3a2e27b --- /dev/null +++ b/engine-ecs-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "engine-ecs-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.35" +syn = { version = "2.0.51", features = ["full"] } +proc-macro2 = "1.0.78" +toml = "0.8.12" + diff --git a/engine-ecs-macros/src/lib.rs b/engine-ecs-macros/src/lib.rs new file mode 100644 index 0000000..bdbba25 --- /dev/null +++ b/engine-ecs-macros/src/lib.rs @@ -0,0 +1,316 @@ +#![deny(clippy::all, clippy::pedantic)] +use std::path::PathBuf as FsPathBuf; + +use proc_macro::TokenStream; +use quote::{ToTokens, format_ident, quote}; +use syn::spanned::Spanned; +use syn::{ + Attribute, + Generics, + Ident, + Item, + ItemEnum, + ItemStruct, + ItemUnion, + Path, + parse, +}; +use toml::value::{Table as TomlTable, Value as TomlValue}; + +macro_rules! syn_path { + ($first_segment: ident $(::$segment: ident)*) => { + ::syn::Path { + leading_colon: None, + segments: ::syn::punctuated::Punctuated::from_iter([ + syn_path_segment!($first_segment), + $(syn_path_segment!($segment),)* + ]) + } + }; +} + +macro_rules! syn_path_segment { + ($segment: ident) => { + ::syn::PathSegment { + ident: ::proc_macro2::Ident::new( + stringify!($segment), + ::proc_macro2::Span::call_site(), + ), + arguments: ::syn::PathArguments::None, + } + }; +} + +/// Generates a `Component` implementation. +/// +/// # Panics +/// Will panic if: +/// - Not attributed to a type item +/// - The attributed-to type item is generic +/// - If parsing the user crate's `Cargo.toml` file fails. +#[proc_macro_derive(Component)] +pub fn component_derive(input: TokenStream) -> TokenStream +{ + let item: TypeItem = parse::<Item>(input).unwrap().try_into().unwrap(); + + let item_ident = item.ident(); + + let (impl_generics, type_generics, where_clause) = item.generics().split_for_impl(); + + let ecs_path = find_engine_ecs_crate_path().unwrap_or_else(|| syn_path!(ecs)); + + assert!( + item.generics().params.is_empty(), + "Generic types are not supported as components" + ); + + let id_var_ident = format_ident!("{}_ID", item_ident.to_string().to_uppercase()); + + let id_var = quote! { + static #id_var_ident: LazyLock<Uid> = LazyLock::new(|| { + Uid::new_unique(UidKind::Component) + }); + }; + + let mod_ident = format_ident!( + "__ecs_priv_component_impl_{}", + item_ident.to_string().to_lowercase() + ); + + quote! { + mod #mod_ident { + use ::std::any::{Any, TypeId}; + use ::std::sync::{LazyLock, Mutex}; + + use #ecs_path::component::Component; + use #ecs_path::uid::{Uid, Kind as UidKind}; + use #ecs_path::system::Input as SystemInput; + + use super::*; + + #id_var + + impl #impl_generics Component for #item_ident #type_generics + #where_clause + { + fn id() -> Uid + { + *#id_var_ident + } + + fn name(&self) -> &'static str + { + std::any::type_name::<Self>() + } + } + + impl #impl_generics SystemInput for #item_ident #type_generics + #where_clause + { + } + } + } + .into() +} + +/// Generates a `Sole` implementation. +/// +/// # Panics +/// Will panic if not attributed to a type item or if parsing the user crate's +/// `Cargo.toml` file fails. +#[proc_macro_derive(Sole, attributes(sole))] +pub fn sole_derive(input: TokenStream) -> TokenStream +{ + let item: TypeItem = parse::<Item>(input).unwrap().try_into().unwrap(); + + let item_ident = item.ident(); + + let sole_attr = item.attribute::<SoleAttribute>("sole").unwrap_or_default(); + + let drop_last = sole_attr.drop_last; + + let (impl_generics, type_generics, where_clause) = item.generics().split_for_impl(); + + let ecs_path = find_engine_ecs_crate_path().unwrap_or_else(|| syn_path!(ecs)); + + quote! { + impl #impl_generics #ecs_path::sole::Sole for #item_ident #type_generics + #where_clause + { + fn drop_last(&self) -> bool + { + #drop_last + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any + { + self + } + + fn as_any(&self) -> &dyn std::any::Any + { + self + } + } + } + .into() +} + +trait FromAttribute: Sized +{ + fn from_attribute(attribute: &Attribute) -> Result<Self, syn::Error>; +} + +enum TypeItem +{ + Struct(ItemStruct), + Enum(ItemEnum), + Union(ItemUnion), +} + +impl TypeItem +{ + fn ident(&self) -> &Ident + { + match self { + Self::Struct(struct_item) => &struct_item.ident, + Self::Enum(enum_item) => &enum_item.ident, + Self::Union(union_item) => &union_item.ident, + } + } + + fn attribute<Attr: FromAttribute>(&self, attr_ident: &str) -> Option<Attr> + { + let item_attrs = match &self { + Self::Struct(struct_item) => &struct_item.attrs, + &Self::Enum(enum_item) => &enum_item.attrs, + &Self::Union(union_item) => &union_item.attrs, + }; + + let mut attr: Option<&Attribute> = None; + + for item_attr in item_attrs { + assert!(attr.is_none(), "Expected only one {attr_ident} attribute"); + + if item_attr.path().get_ident()? == attr_ident { + attr = Some(item_attr); + } + } + + Some(Attr::from_attribute(attr?).unwrap()) + } + + fn generics(&self) -> &Generics + { + match self { + Self::Struct(struct_item) => &struct_item.generics, + Self::Enum(enum_item) => &enum_item.generics, + Self::Union(union_item) => &union_item.generics, + } + } +} + +impl TryFrom<Item> for TypeItem +{ + type Error = syn::Error; + + fn try_from(item: Item) -> Result<Self, Self::Error> + { + match item { + Item::Struct(struct_item) => Ok(Self::Struct(struct_item)), + Item::Enum(enum_item) => Ok(Self::Enum(enum_item)), + Item::Union(union_item) => Ok(Self::Union(union_item)), + _ => Err(syn::Error::new( + item.span(), + "Expected a struct, a enum or a union", + )), + } + } +} + +impl ToTokens for TypeItem +{ + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) + { + match self { + Self::Struct(struct_item) => struct_item.to_tokens(tokens), + Self::Enum(enum_item) => enum_item.to_tokens(tokens), + Self::Union(union_item) => union_item.to_tokens(tokens), + } + } +} + +#[derive(Debug, Default)] +struct SoleAttribute +{ + drop_last: bool, +} + +impl FromAttribute for SoleAttribute +{ + fn from_attribute(attribute: &Attribute) -> Result<Self, syn::Error> + { + let mut drop_last = false; + + attribute.parse_nested_meta(|meta| { + if meta + .path + .get_ident() + .is_some_and(|flag| flag == "drop_last") + { + drop_last = true; + + return Ok(()); + } + + Err(meta.error("Unrecognized token")) + })?; + + Ok(Self { drop_last }) + } +} + +fn find_engine_ecs_crate_path() -> Option<Path> +{ + let cargo_manifest_dir = FsPathBuf::from(std::env::var("CARGO_MANIFEST_DIR").ok()?); + + let cargo_crate_name = std::env::var("CARGO_CRATE_NAME").ok()?; + let cargo_pkg_name = std::env::var("CARGO_PKG_NAME").ok()?; + + if cargo_pkg_name == "engine-ecs" && cargo_crate_name != "engine_ecs" { + // Macro is used by a engine-ecs crate example/test/benchmark + return Some(syn_path!(engine_ecs)); + } + + let crate_manifest = std::fs::read_to_string(cargo_manifest_dir.join("Cargo.toml")) + .ok()? + .parse::<TomlTable>() + .expect("Failed to parse crate manifest file"); + + let package = match crate_manifest.get("package")? { + TomlValue::Table(package) => Some(package), + _ => None, + }?; + + let package_name = match package.get("name")? { + TomlValue::String(package_name) => Some(package_name), + _ => None, + }?; + + if package_name == "engine-ecs" { + return Some(syn_path!(crate)); + } + + let crate_dependencies = match crate_manifest.get("dependencies")? { + TomlValue::Table(dependencies) => Some(dependencies), + _ => None, + }?; + + crate_dependencies.iter().find_map(|(crate_dep_name, _)| { + if crate_dep_name == "engine" { + return Some(syn_path!(engine::ecs)); + } + + None + }) +} |
