diff options
Diffstat (limited to 'engine-ecs-macros/src/lib.rs')
| -rw-r--r-- | engine-ecs-macros/src/lib.rs | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/engine-ecs-macros/src/lib.rs b/engine-ecs-macros/src/lib.rs new file mode 100644 index 0000000..da9eec3 --- /dev/null +++ b/engine-ecs-macros/src/lib.rs @@ -0,0 +1,373 @@ +#![deny(clippy::all, clippy::pedantic)] +use std::path::PathBuf as FsPathBuf; + +use proc_macro::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::spanned::Spanned; +use syn::{parse, Generics, Ident, Item, ItemEnum, ItemStruct, ItemUnion, Path}; +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() + }); + }; + + 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; + 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 type_reflection() -> Option<&'static #ecs_path::reflection::Type> + { + struct SpecializationTarget<T>(std::marker::PhantomData<T>); + + trait HasReflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type>; + } + + trait NoReflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type>; + } + + impl<T> NoReflection for &SpecializationTarget<T> + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type> + { + None + } + } + + impl<T> HasReflection for SpecializationTarget<T> + where + T: #ecs_path::reflection::Reflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type> + { + Some(T::type_reflection()) + } + } + + (&SpecializationTarget::<Self>(std::marker::PhantomData)) + .type_reflection() + } + + fn name(&self) -> &'static str + { + std::any::type_name::<Self>() + } + } + + impl #impl_generics SystemInput for #item_ident #type_generics + #where_clause + { + } + + impl #ecs_path::component::IntoParts for #item_ident + { + fn into_parts(self) -> #ecs_path::component::Parts + { + #ecs_path::component::Parts::builder() + .name(<Self as Component>::name(&self)) + .type_reflection(<Self as Component>::type_reflection()) + .build(<Self as Component>::id(), self) + } + } + } + } + .into() +} + +/// Generates a `Sole` 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(Sole)] +pub fn sole_derive(input: TokenStream) -> TokenStream +{ + let item: TypeItem = parse::<Item>(input).unwrap().try_into().unwrap(); + + let item_ident = item.ident(); + + 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 sole 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() + }); + }; + + let mod_ident = format_ident!( + "__ecs_priv_sole_impl_{}", + item_ident.to_string().to_lowercase() + ); + + quote! { + mod #mod_ident { + use ::std::any::{Any, TypeId}; + use ::std::sync::{LazyLock, Mutex}; + + use #ecs_path::sole::Sole; + use #ecs_path::uid::Uid; + use #ecs_path::system::Input as SystemInput; + + use super::*; + + #id_var + + impl Sole for #item_ident + { + fn id() -> Uid + { + *#id_var_ident + } + + fn type_reflection() -> Option<&'static #ecs_path::reflection::Type> + { + struct SpecializationTarget<T>(std::marker::PhantomData<T>); + + trait HasReflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type>; + } + + trait NoReflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type>; + } + + impl<T> NoReflection for &SpecializationTarget<T> + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type> + { + None + } + } + + impl<T> HasReflection for SpecializationTarget<T> + where + T: #ecs_path::reflection::Reflection + { + fn type_reflection(&self) + -> Option<&'static #ecs_path::reflection::Type> + { + Some(T::type_reflection()) + } + } + + (&SpecializationTarget::<Self>(std::marker::PhantomData)) + .type_reflection() + } + + fn name(&self) -> &'static str + { + std::any::type_name::<Self>() + } + } + + impl #ecs_path::component::IntoParts for #item_ident + { + fn into_parts(self) -> #ecs_path::component::Parts + { + #ecs_path::component::Parts::builder() + .name(<Self as Sole>::name(&self)) + .type_reflection(<Self as Sole>::type_reflection()) + .build(<Self as Sole>::id(), self) + } + } + } + } + .into() +} + +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 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), + } + } +} + +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 + }) +} |
