summaryrefslogtreecommitdiff
path: root/engine-ecs-macros/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'engine-ecs-macros/src/lib.rs')
-rw-r--r--engine-ecs-macros/src/lib.rs373
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
+ })
+}