summaryrefslogtreecommitdiff
path: root/engine-macros
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2026-03-17 13:18:56 +0100
committerHampusM <hampus@hampusmat.com>2026-03-17 13:19:14 +0100
commit21aafa3b070425e1fa84d2c6bb322e75cb52dfd5 (patch)
treef90303f4761136e5885b8ceda4ec2c92dfd00f55 /engine-macros
parentd0cac9159e6ddfe5dbb9b32036e7258f6e51d47e (diff)
feat(engine): add macro for reflection
Diffstat (limited to 'engine-macros')
-rw-r--r--engine-macros/Cargo.toml13
-rw-r--r--engine-macros/src/lib.rs174
2 files changed, 187 insertions, 0 deletions
diff --git a/engine-macros/Cargo.toml b/engine-macros/Cargo.toml
new file mode 100644
index 0000000..7470567
--- /dev/null
+++ b/engine-macros/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "engine-macros"
+version = "0.1.0"
+edition = "2024"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.35"
+syn = { version = "2.0.51", features = ["full"] }
+proc-macro2 = "1.0.78"
+
diff --git a/engine-macros/src/lib.rs b/engine-macros/src/lib.rs
new file mode 100644
index 0000000..ad6c15f
--- /dev/null
+++ b/engine-macros/src/lib.rs
@@ -0,0 +1,174 @@
+#![deny(clippy::all, clippy::pedantic)]
+
+use proc_macro::TokenStream;
+use quote::{ToTokens, format_ident, quote};
+use syn::punctuated::Punctuated;
+use syn::{
+ Item,
+ LitStr,
+ Path as SynPath,
+ PredicateType,
+ Token,
+ TraitBound,
+ TypeParamBound,
+ WhereClause,
+ WherePredicate,
+ parse,
+};
+
+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,
+ }
+ };
+}
+
+#[proc_macro_derive(Reflection)]
+pub fn reflection_derive(input: TokenStream) -> TokenStream
+{
+ let input = parse::<Item>(input).unwrap();
+
+ let input = match input {
+ Item::Struct(input) => input,
+ Item::Enum(_) => unimplemented!(),
+ _ => panic!("Invalid input"),
+ };
+
+ let engine_crate_path = find_engine_crate_path().unwrap();
+
+ let input_ident = input.ident;
+
+ let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
+
+ let mut where_clause = where_clause.cloned().unwrap_or_else(|| WhereClause {
+ where_token: <Token![where]>::default(),
+ predicates: Punctuated::new(),
+ });
+
+ where_clause
+ .predicates
+ .extend(input.fields.iter().map(|field| {
+ WherePredicate::Type(PredicateType {
+ lifetimes: None,
+ bounded_ty: field.ty.clone(),
+ colon_token: <Token![:]>::default(),
+ bounds: [TypeParamBound::Trait(TraitBound {
+ paren_token: None,
+ modifier: syn::TraitBoundModifier::None,
+ lifetimes: None,
+ path: engine_crate_path.join(syn_path!(reflection::With)),
+ })]
+ .into_iter()
+ .collect(),
+ })
+ }));
+
+ let fields = input.fields.into_iter().enumerate().map(|(index, field)| {
+ let field_ident = field.ident.unwrap_or_else(|| format_ident!("{index}"));
+
+ let field_type = &field.ty;
+
+ let field_name = LitStr::new(&field_ident.to_string(), field_ident.span());
+
+ // since std::any::type_name as const is not stable yet
+ let field_type_name = field_type.to_token_stream().to_string();
+
+ quote! {
+ #engine_crate_path::reflection::StructField {
+ name: #field_name,
+ index: #index,
+ layout: std::alloc::Layout::new::<#field_type>(),
+ byte_offset: std::mem::offset_of!(Self, #field_ident),
+ type_id: std::any::TypeId::of::<#field_type>(),
+ type_name: #field_type_name,
+ reflection:
+ #engine_crate_path::reflection::__private::get_type_reflection::<
+ #field_type
+ >()
+ }
+ }
+ });
+
+ quote! {
+ impl #impl_generics #engine_crate_path::reflection::With for
+ #input_ident #type_generics #where_clause
+ {
+ const REFLECTION: &#engine_crate_path::reflection::Reflection =
+ &const {
+ #engine_crate_path::reflection::Reflection::Struct(
+ #engine_crate_path::reflection::Struct {
+ fields: &[
+ #(#fields),*
+ ]
+ }
+ )
+ };
+
+ fn reflection() -> &'static #engine_crate_path::reflection::Reflection
+ {
+ Self::REFLECTION
+ }
+
+ fn get_reflection(&self) -> &'static #engine_crate_path::reflection::Reflection
+ {
+ Self::reflection()
+ }
+ }
+ }
+ .into()
+}
+
+fn find_engine_crate_path() -> Option<SynPath>
+{
+ 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" && cargo_crate_name != "engine" {
+ // Macro is used by a crate example/test/benchmark
+ return Some(syn_path!(engine));
+ }
+
+ if cargo_crate_name == "engine" {
+ return Some(syn_path!(crate));
+ }
+
+ Some(syn_path!(engine))
+}
+
+trait SynPathExt
+{
+ fn join(&self, other: Self) -> Self;
+}
+
+impl SynPathExt for SynPath
+{
+ fn join(&self, other: Self) -> Self
+ {
+ Self {
+ leading_colon: self.leading_colon.clone(),
+ segments: self
+ .segments
+ .iter()
+ .chain(&other.segments)
+ .cloned()
+ .collect(),
+ }
+ }
+}