#![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::(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 = 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(std::marker::PhantomData); 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 NoReflection for &SpecializationTarget { fn type_reflection(&self) -> Option<&'static #ecs_path::reflection::Type> { None } } impl HasReflection for SpecializationTarget where T: #ecs_path::reflection::Reflection { fn type_reflection(&self) -> Option<&'static #ecs_path::reflection::Type> { Some(T::type_reflection()) } } (&SpecializationTarget::(std::marker::PhantomData)) .type_reflection() } fn name(&self) -> &'static str { std::any::type_name::() } } 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.name()) .type_reflection(::type_reflection()) .build(Self::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::(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 = 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(std::marker::PhantomData); 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 NoReflection for &SpecializationTarget { fn type_reflection(&self) -> Option<&'static #ecs_path::reflection::Type> { None } } impl HasReflection for SpecializationTarget where T: #ecs_path::reflection::Reflection { fn type_reflection(&self) -> Option<&'static #ecs_path::reflection::Type> { Some(T::type_reflection()) } } (&SpecializationTarget::(std::marker::PhantomData)) .type_reflection() } fn name(&self) -> &'static str { std::any::type_name::() } } impl #ecs_path::component::IntoParts for #item_ident { fn into_parts(self) -> #ecs_path::component::Parts { #ecs_path::component::Parts::builder() .name(self.name()) .type_reflection(::type_reflection()) .build(Self::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 for TypeItem { type Error = syn::Error; fn try_from(item: Item) -> Result { 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 { 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::() .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 }) }