summaryrefslogtreecommitdiff
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
parentd0cac9159e6ddfe5dbb9b32036e7258f6e51d47e (diff)
feat(engine): add macro for reflection
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml2
-rw-r--r--engine-macros/Cargo.toml13
-rw-r--r--engine-macros/src/lib.rs174
-rw-r--r--engine/Cargo.toml1
-rw-r--r--engine/src/lib.rs1
-rw-r--r--engine/src/reflection.rs147
7 files changed, 347 insertions, 1 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 1b57c31..83cd013 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -524,6 +524,7 @@ dependencies = [
"cfg_aliases",
"crossbeam-channel",
"ecs",
+ "engine-macros",
"glutin",
"image",
"nu-ansi-term",
@@ -540,6 +541,15 @@ dependencies = [
]
[[package]]
+name = "engine-macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.100",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 21eb777..ac12a44 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2021"
[workspace]
-members = ["engine", "ecs", "ecs-macros", "util-macros", "opengl-bindings"]
+members = ["engine", "engine-macros", "ecs", "ecs-macros", "util-macros", "opengl-bindings"]
[dependencies]
engine = { path = "./engine" }
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(),
+ }
+ }
+}
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
index 8e46bef..4e4600d 100644
--- a/engine/Cargo.toml
+++ b/engine/Cargo.toml
@@ -18,6 +18,7 @@ nu-ansi-term = "0.46.0"
ecs = { path = "../ecs" }
util-macros = { path = "../util-macros" }
opengl-bindings = { path = "../opengl-bindings" }
+engine-macros = { path = "../engine-macros" }
[dependencies.winit]
version = "0.30.11"
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
index 75bc921..b470cdc 100644
--- a/engine/src/lib.rs
+++ b/engine/src/lib.rs
@@ -33,6 +33,7 @@ pub mod math;
pub mod mesh;
pub mod model;
pub mod projection;
+pub mod reflection;
pub mod renderer;
pub mod texture;
pub mod transform;
diff --git a/engine/src/reflection.rs b/engine/src/reflection.rs
new file mode 100644
index 0000000..5bd2aef
--- /dev/null
+++ b/engine/src/reflection.rs
@@ -0,0 +1,147 @@
+use std::alloc::Layout;
+use std::any::TypeId;
+
+pub use engine_macros::Reflection;
+
+pub trait With: 'static
+{
+ const REFLECTION: &Reflection;
+
+ fn reflection() -> &'static Reflection
+ where
+ Self: Sized;
+
+ fn get_reflection(&self) -> &'static Reflection;
+}
+
+#[derive(Debug, Clone)]
+#[non_exhaustive]
+pub enum Reflection
+{
+ Struct(Struct),
+ Array(Array),
+ Slice(Slice),
+ Literal,
+}
+
+#[derive(Debug, Clone)]
+pub struct Struct
+{
+ pub fields: &'static [StructField],
+}
+
+#[derive(Debug, Clone)]
+pub struct StructField
+{
+ pub name: &'static str,
+ pub index: usize,
+ pub layout: Layout,
+ pub byte_offset: usize,
+ pub type_id: TypeId,
+ pub type_name: &'static str,
+ pub reflection: &'static Reflection,
+}
+
+#[derive(Debug, Clone)]
+pub struct Array
+{
+ pub item_reflection: &'static Reflection,
+ pub length: usize,
+}
+
+#[derive(Debug, Clone)]
+pub struct Slice
+{
+ pub item_reflection: &'static Reflection,
+}
+
+macro_rules! impl_with_for_literals {
+ ($($literal: ty),*) => {
+ $(
+ impl With for $literal
+ {
+ const REFLECTION: &Reflection = &Reflection::Literal;
+
+ fn reflection() -> &'static Reflection
+ where
+ Self: Sized
+ {
+ Self::REFLECTION
+ }
+
+ fn get_reflection(&self) -> &'static Reflection
+ {
+ Self::reflection()
+ }
+ }
+ )*
+ };
+}
+
+impl_with_for_literals!(
+ u8,
+ i8,
+ u16,
+ i16,
+ u32,
+ i32,
+ u64,
+ i64,
+ u128,
+ i128,
+ f32,
+ f64,
+ usize,
+ isize,
+ &'static str
+);
+
+impl<T: With, const LEN: usize> With for [T; LEN]
+{
+ const REFLECTION: &Reflection = &Reflection::Array(Array {
+ item_reflection: T::REFLECTION,
+ length: LEN,
+ });
+
+ fn reflection() -> &'static Reflection
+ where
+ Self: Sized,
+ {
+ Self::REFLECTION
+ }
+
+ fn get_reflection(&self) -> &'static Reflection
+ {
+ Self::reflection()
+ }
+}
+
+impl<T: With> With for &'static [T]
+{
+ const REFLECTION: &Reflection =
+ &Reflection::Slice(Slice { item_reflection: T::REFLECTION });
+
+ fn reflection() -> &'static Reflection
+ where
+ Self: Sized,
+ {
+ Self::REFLECTION
+ }
+
+ fn get_reflection(&self) -> &'static Reflection
+ {
+ Self::reflection()
+ }
+}
+
+// Used by the Reflection derive macro
+#[doc(hidden)]
+pub mod __private
+{
+ pub const fn get_type_reflection<T>() -> &'static super::Reflection
+ where
+ T: super::With,
+ {
+ T::REFLECTION
+ }
+}