summaryrefslogtreecommitdiff
path: root/engine-ecs-macros/src/lib.rs
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2026-05-21 17:55:20 +0200
committerHampusM <hampus@hampusmat.com>2026-05-21 17:55:20 +0200
commit8022e8998290b067b8aa0cb9cba8ba410826bdab (patch)
tree7171e79ce530e03079046ee8fd12167160c45480 /engine-ecs-macros/src/lib.rs
parent412cee02c252f91bcf0b70a3f5cc5fca6d2b4c62 (diff)
chore: rename ecs* crates to engine-ecs*HEADmaster
Diffstat (limited to 'engine-ecs-macros/src/lib.rs')
-rw-r--r--engine-ecs-macros/src/lib.rs316
1 files changed, 316 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..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
+ })
+}