summaryrefslogtreecommitdiff
path: root/engine-macros
diff options
context:
space:
mode:
Diffstat (limited to 'engine-macros')
-rw-r--r--engine-macros/Cargo.toml13
-rw-r--r--engine-macros/src/lib.rs10
-rw-r--r--engine-macros/src/reflection.rs75
-rw-r--r--engine-macros/src/reflection/default_value.rs42
-rw-r--r--engine-macros/src/reflection/enum_impl.rs507
-rw-r--r--engine-macros/src/reflection/field.rs100
-rw-r--r--engine-macros/src/reflection/options_attr.rs39
-rw-r--r--engine-macros/src/reflection/struct_impl.rs209
-rw-r--r--engine-macros/src/reflection/visibility.rs45
-rw-r--r--engine-macros/src/util.rs65
10 files changed, 1105 insertions, 0 deletions
diff --git a/engine-macros/Cargo.toml b/engine-macros/Cargo.toml
new file mode 100644
index 0000000..11e3c4d
--- /dev/null
+++ b/engine-macros/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "engine-macros"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+quote = "1.0.35"
+syn = { version = "2.0.51", features = ["full", "extra-traits" ] }
+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..deddd15
--- /dev/null
+++ b/engine-macros/src/lib.rs
@@ -0,0 +1,10 @@
+#![deny(clippy::all, clippy::pedantic)]
+
+mod reflection;
+mod util;
+
+#[proc_macro_derive(Reflection, attributes(reflection))]
+pub fn reflection_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream
+{
+ reflection::derive(input.into()).into()
+}
diff --git a/engine-macros/src/reflection.rs b/engine-macros/src/reflection.rs
new file mode 100644
index 0000000..ad1df04
--- /dev/null
+++ b/engine-macros/src/reflection.rs
@@ -0,0 +1,75 @@
+use crate::reflection::options_attr::OptionsAttr;
+
+mod default_value;
+mod enum_impl;
+mod field;
+mod options_attr;
+mod struct_impl;
+mod visibility;
+
+pub fn derive(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream
+{
+ let input = syn::parse2::<syn::Item>(input).unwrap();
+
+ let options = get_options(&input);
+
+ match input {
+ syn::Item::Struct(input) => struct_impl::generate(&input, &options),
+ syn::Item::Enum(input) => enum_impl::generate(&input, &options),
+ _ => panic!("Invalid input"),
+ }
+}
+
+pub fn get_options(input: &syn::Item) -> OptionsAttr
+{
+ let (attrs, generic_params) = match &input {
+ syn::Item::Struct(input) => (input.attrs.as_slice(), &input.generics.params),
+ syn::Item::Enum(input) => (input.attrs.as_slice(), &input.generics.params),
+ _ => (
+ const { &[] }.as_slice(),
+ &syn::punctuated::Punctuated::<syn::GenericParam, syn::Token![,]>::new(),
+ ),
+ };
+
+ if attrs
+ .iter()
+ .filter(|attr| attr.path().is_ident("reflection"))
+ .count()
+ > 1
+ {
+ panic!("Multiple 'reflection' attributes are not allowed");
+ }
+
+ let options = match attrs
+ .iter()
+ .find_map(|attr| {
+ if attr.path().is_ident("reflection") {
+ return Some(attr.parse_args::<OptionsAttr>());
+ }
+
+ None
+ })
+ .transpose()
+ {
+ Ok(attr) => attr.unwrap_or_default(),
+ Err(err) => {
+ panic!("{err}");
+ }
+ };
+
+ assert!(
+ !options.impl_with_generics.is_empty() || generic_params.is_empty(),
+ concat!(
+ "Generic types deriving Reflection must specify which ",
+ "generics to use in the generated impl(s) using the 'reflection' attribute\n",
+ "For example: #[reflection(impl_with_generics(<u32, String>, <u8, u16>))]"
+ )
+ );
+
+ assert!(
+ options.impl_with_generics.is_empty() || !generic_params.is_empty(),
+ "Specifying 'impl_with_generics' for non-generic types is not allowed"
+ );
+
+ options
+}
diff --git a/engine-macros/src/reflection/default_value.rs b/engine-macros/src/reflection/default_value.rs
new file mode 100644
index 0000000..6784129
--- /dev/null
+++ b/engine-macros/src/reflection/default_value.rs
@@ -0,0 +1,42 @@
+use quote::quote;
+
+pub fn gen_get_default_value_fn(
+ type_ident: &proc_macro2::Ident,
+ generic_args: Option<&syn::AngleBracketedGenericArguments>,
+) -> proc_macro2::TokenStream
+{
+ quote! {
+ struct SpecializationTarget<T>(std::marker::PhantomData<T>);
+
+ trait HasDefaultValue
+ {
+ fn default_value_fn(&self) -> Option<fn() -> Box<dyn std::any::Any>>;
+ }
+
+ trait NoDefaultValue
+ {
+ fn default_value_fn(&self) -> Option<fn() -> Box<dyn std::any::Any>>;
+ }
+
+ impl<T> NoDefaultValue for &SpecializationTarget<T>
+ {
+ fn default_value_fn(&self) -> Option<fn() -> Box<dyn std::any::Any>>
+ {
+ None
+ }
+ }
+
+ impl<T> HasDefaultValue for SpecializationTarget<T>
+ where
+ T: std::default::Default + 'static
+ {
+ fn default_value_fn(&self) -> Option<fn() -> Box<dyn std::any::Any>>
+ {
+ Some(|| Box::new(T::default()))
+ }
+ }
+
+ (&SpecializationTarget::<#type_ident #generic_args>(std::marker::PhantomData))
+ .default_value_fn()
+ }
+}
diff --git a/engine-macros/src/reflection/enum_impl.rs b/engine-macros/src/reflection/enum_impl.rs
new file mode 100644
index 0000000..96efd71
--- /dev/null
+++ b/engine-macros/src/reflection/enum_impl.rs
@@ -0,0 +1,507 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+use crate::reflection::default_value::gen_get_default_value_fn;
+use crate::reflection::options_attr::OptionsAttr;
+use crate::util::find_engine_crate_path;
+
+pub fn generate(input: &syn::ItemEnum, options: &OptionsAttr)
+ -> proc_macro2::TokenStream
+{
+ let engine_crate_path = find_engine_crate_path().unwrap();
+
+ let variant_lookup_match_arms = input
+ .variants
+ .iter()
+ .enumerate()
+ .map(|(index, variant)| {
+ let variant_ident = &variant.ident;
+
+ let pattern = match variant.fields {
+ syn::Fields::Unit => quote! { Self::#variant_ident },
+ syn::Fields::Named(_) => quote! { Self::#variant_ident { .. } },
+ syn::Fields::Unnamed(_) => quote! { Self::#variant_ident(..) },
+ };
+
+ quote! {
+ #pattern => &enum_reflection.variants[#index]
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let is_unit_only = input
+ .variants
+ .iter()
+ .all(|variant| matches!(variant.fields, syn::Fields::Unit));
+
+ let impls: &mut dyn Iterator<Item = proc_macro2::TokenStream> =
+ if input.generics.params.is_empty() {
+ &mut [generate_impls(
+ input,
+ None,
+ is_unit_only,
+ &variant_lookup_match_arms,
+ &engine_crate_path,
+ )]
+ .into_iter()
+ } else {
+ &mut options.impl_with_generics.iter().map(|generic_args| {
+ generate_impls(
+ input,
+ Some(generic_args),
+ is_unit_only,
+ &variant_lookup_match_arms,
+ &engine_crate_path,
+ )
+ })
+ };
+
+ quote! {
+ #(#impls)*
+ }
+}
+
+fn generate_impls(
+ input: &syn::ItemEnum,
+ generic_args: Option<&syn::AngleBracketedGenericArguments>,
+ is_unit_only: bool,
+ variant_lookup_match_arms: &[proc_macro2::TokenStream],
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ let get_default_value_fn = gen_get_default_value_fn(&input.ident, generic_args);
+
+ let variants = generate_variants(&input.variants, engine_crate_path);
+
+ let generics_type_aliases = input
+ .generics
+ .type_params()
+ .zip(
+ generic_args
+ .iter()
+ .flat_map(|generic_args| &generic_args.args)
+ .filter_map(|generic_arg| match generic_arg {
+ syn::GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ }),
+ )
+ .map(|(type_param, generic_arg_type)| {
+ let type_param_ident = &type_param.ident;
+
+ quote! {
+ type #type_param_ident = #generic_arg_type;
+ }
+ });
+
+ let input_ident = &input.ident;
+
+ quote! {
+ unsafe impl #engine_crate_path::reflection::Reflection for
+ #input_ident #generic_args
+ {
+ const TYPE_REFLECTION: &#engine_crate_path::reflection::Type =
+ &const {
+ #(#generics_type_aliases)*
+
+ #engine_crate_path::reflection::Type::Enum(
+ #engine_crate_path::reflection::Enum {
+ variants: &[#(#variants),*],
+ is_unit_only: #is_unit_only,
+ get_default_value: || {
+ #get_default_value_fn
+ },
+ cast_dyn_any: |ptr| ptr.cast::<Self>(),
+ get_variant_index: |target| {
+ use std::any::Any;
+ use #engine_crate_path::reflection::Reflection;
+ use #engine_crate_path::reflection::EnumReflectionExt;
+
+ let target = target.downcast_ref::<Self>()?;
+
+ let enum_reflection = unsafe {
+ <Self as Reflection>::TYPE_REFLECTION
+ .as_enum()
+ .unwrap_unchecked()
+ };
+
+ Some(enum_reflection.variants
+ .element_offset(
+ match target {
+ #(#variant_lookup_match_arms),*
+ }
+ )
+ .unwrap())
+ }
+ }
+ )
+ };
+ }
+
+ unsafe impl #engine_crate_path::reflection::EnumReflectionExt for
+ #input_ident #generic_args
+ {
+ fn get_variant_reflection(&self)
+ -> &'static #engine_crate_path::reflection::EnumVariant
+ {
+ let enum_reflection = unsafe {
+ <Self as #engine_crate_path::reflection::Reflection>::TYPE_REFLECTION
+ .as_enum()
+ .unwrap_unchecked()
+ };
+
+ match self {
+ #(#variant_lookup_match_arms),*
+ }
+ }
+ }
+ }
+}
+
+fn generate_variants<'a>(
+ input_variants: &'a syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>,
+ engine_crate_path: &'a syn::Path,
+) -> impl Iterator<Item = proc_macro2::TokenStream> + use<'a>
+{
+ input_variants.iter().map(move |variant| {
+ let variant_name =
+ syn::LitStr::new(&variant.ident.to_string(), variant.ident.span());
+
+ let fields = gen_variant_fields(variant, engine_crate_path);
+
+ let variant_field_vars = variant.fields.iter().enumerate().map(
+ |(variant_field_index, variant_field)| {
+ let var_ident = match variant_field.ident.as_ref() {
+ Some(field_ident) => format_ident!("field_{field_ident}"),
+ None => format_ident!("field_{}", variant_field_index),
+ };
+
+ let field_type = &variant_field.ty;
+
+ quote! {
+ let #var_ident = fields
+ .next()
+ .ok_or(EnumVariantWriteNewToError::TooFewFields)?
+ .downcast::<#field_type>()
+ .map_err(|_| EnumVariantWriteNewToError::WrongFieldType)?;
+ }
+ },
+ );
+
+ let variant_construction =
+ match &variant.fields {
+ syn::Fields::Unit => {
+ let variant_ident = &variant.ident;
+
+ quote! { Self::#variant_ident }
+ }
+ syn::Fields::Named(named_fields) => {
+ let variant_ident = &variant.ident;
+
+ let fields = named_fields.named.iter().map(|field| {
+ let Some(field_ident) = field.ident.as_ref() else {
+ unreachable!();
+ };
+
+ let field_var_ident = format_ident!("field_{field_ident}");
+
+ quote! { #field_ident: *#field_var_ident }
+ });
+
+ quote! {
+ Self::#variant_ident {
+ #(#fields),*
+ }
+ }
+ }
+ syn::Fields::Unnamed(unnamed_fields) => {
+ let variant_ident = &variant.ident;
+
+ let fields = unnamed_fields.unnamed.iter().enumerate().map(
+ |(field_index, _)| {
+ let field_var_ident = format_ident!("field_{field_index}");
+
+ quote! { *#field_var_ident }
+ },
+ );
+
+ quote! {
+ Self::#variant_ident(#(#fields),*)
+ }
+ }
+ };
+
+ let VariantTryGetFieldFns {
+ immutable: try_get_field_immutable_fn,
+ mutable: try_get_field_mutable_fn,
+ } = gen_variant_try_get_field_functions(variant, engine_crate_path);
+
+ quote! {
+ #engine_crate_path::reflection::EnumVariant {
+ name: #variant_name,
+ fields: #fields,
+ try_write_new_to: |dst, fields| {
+ use std::any::Any;
+ use #engine_crate_path::reflection::EnumVariantWriteNewToError;
+
+ let dst = dst
+ .downcast_mut()
+ .ok_or(EnumVariantWriteNewToError::WrongDstType)?;
+
+ #(#variant_field_vars)*;
+
+ let created = #variant_construction;
+
+ if fields.next().is_some() {
+ return Err(EnumVariantWriteNewToError::TooManyFields);
+ }
+
+ *dst = created;
+
+ Ok(())
+ },
+ try_get_field: #try_get_field_immutable_fn,
+ try_get_field_mut: #try_get_field_mutable_fn,
+ }
+ }
+ })
+}
+
+pub fn generate_enum_variant_field(
+ field: &syn::Field,
+ field_index: usize,
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ let field_ident = &field.ident;
+
+ let field_type = &field.ty;
+
+ let field_name = if let Some(field_ident) = field_ident {
+ let field_name = syn::LitStr::new(&field_ident.to_string(), field_ident.span());
+
+ quote! { Some(#field_name) }
+ } else {
+ quote! { None }
+ };
+
+ let get_type_fn_body =
+ { gen_get_optional_type_reflection(field_type, engine_crate_path) };
+
+ quote! {
+ #engine_crate_path::reflection::EnumVariantField {
+ name: #field_name,
+ index: #field_index,
+ type_id: std::any::TypeId::of::<#field_type>(),
+ get_type_name: #engine_crate_path::reflection::FnWithDebug::new(|| {
+ std::any::type_name::<#field_type>()
+ }),
+ get_type: #engine_crate_path::reflection::FnWithDebug::new(|| {
+ #get_type_fn_body
+ }),
+ }
+ }
+}
+
+fn gen_get_optional_type_reflection(
+ field_type: &syn::Type,
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ quote! {
+ struct SpecializationTarget<Field>(std::marker::PhantomData<Field>);
+
+ trait FieldHasReflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>;
+ }
+
+ trait FieldDoesNotHaveReflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>;
+ }
+
+ impl<Field> FieldDoesNotHaveReflection for &SpecializationTarget<Field>
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>
+ {
+ None
+ }
+ }
+
+ impl<Field> FieldHasReflection for SpecializationTarget<Field>
+ where
+ Field: #engine_crate_path::reflection::Reflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>
+ {
+ Some(Field::type_reflection())
+ }
+ }
+
+ (&SpecializationTarget::<#field_type>(std::marker::PhantomData))
+ .field_type_reflection()
+ }
+}
+
+fn gen_variant_fields(
+ variant: &syn::Variant,
+ engine_crate_path: &syn::Path,
+) -> TokenStream
+{
+ match &variant.fields {
+ syn::Fields::Unit => quote! { None },
+ syn::Fields::Named(named_fields) => {
+ let fields = named_fields.named.iter().enumerate().map(
+ |(variant_field_index, variant_field)| {
+ generate_enum_variant_field(
+ variant_field,
+ variant_field_index,
+ engine_crate_path,
+ )
+ },
+ );
+
+ quote! {
+ Some(#engine_crate_path::reflection::EnumVariantFields::Named {
+ fields: &[#(#fields),*]
+ })
+ }
+ }
+ syn::Fields::Unnamed(unnamed_fields) => {
+ let fields = unnamed_fields.unnamed.iter().enumerate().map(
+ |(variant_field_index, variant_field)| {
+ generate_enum_variant_field(
+ variant_field,
+ variant_field_index,
+ engine_crate_path,
+ )
+ },
+ );
+
+ quote! {
+ Some(#engine_crate_path::reflection::EnumVariantFields::Unnamed {
+ fields: &[#(#fields),*]
+ })
+ }
+ }
+ }
+}
+
+struct VariantTryGetFieldFns
+{
+ immutable: TokenStream,
+ mutable: TokenStream,
+}
+
+fn gen_variant_try_get_field_functions(
+ variant: &syn::Variant,
+ engine_crate_path: &syn::Path,
+) -> VariantTryGetFieldFns
+{
+ let field_index_match_arms = match &variant.fields {
+ syn::Fields::Unit => quote! {},
+ syn::Fields::Named(syn::FieldsNamed { named: named_fields, .. }) => {
+ let match_arms =
+ named_fields.iter().enumerate().map(|(field_index, field)| {
+ let Some(field_ident) = &field.ident else {
+ unreachable!();
+ };
+
+ quote! { #field_index => { #field_ident } }
+ });
+
+ quote! { #(#match_arms)* }
+ }
+ syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: unnamed_fields, .. }) => {
+ let match_arms = unnamed_fields.iter().enumerate().map(|(field_index, _)| {
+ let field_ident = format_ident!("field_{field_index}");
+
+ quote! { #field_index => { #field_ident } }
+ });
+
+ quote! { #(#match_arms)* }
+ }
+ };
+
+ let variant_ident = &variant.ident;
+
+ let variant_pattern = match &variant.fields {
+ syn::Fields::Unit => quote! { Self::#variant_ident },
+ syn::Fields::Named(syn::FieldsNamed { named: named_fields, .. }) => {
+ let field_idents = named_fields.iter().map(|field| {
+ let Some(field_ident) = &field.ident else {
+ unreachable!();
+ };
+
+ field_ident
+ });
+
+ quote! { Self::#variant_ident { #(#field_idents),* } }
+ }
+ syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: unnamed_fields, .. }) => {
+ let field_idents = unnamed_fields
+ .iter()
+ .enumerate()
+ .map(|(field_index, _)| format_ident!("field_{field_index}"));
+
+ quote! { Self::#variant_ident(#(#field_idents),*) }
+ }
+ };
+
+ VariantTryGetFieldFns {
+ immutable: quote! {
+ |target, field_index| { {
+ #![allow(unreachable_code)]
+
+ use std::any::Any;
+ use #engine_crate_path::reflection::GetError;
+
+ let target = target
+ .downcast_ref::<Self>()
+ .ok_or(GetError::WrongTargetType)?;
+
+ let #variant_pattern = target else {
+ return Err(GetError::WrongTargetEnumVariant);
+ };
+
+ let field: &dyn Any = match field_index {
+ #field_index_match_arms
+ _ => {
+ return Err(GetError::IndexOutOfBounds);
+ }
+ };
+
+ Ok(field)
+ } }
+ },
+ mutable: quote! {
+ |target, field_index| { {
+ #![allow(unreachable_code)]
+
+ use std::any::Any;
+ use #engine_crate_path::reflection::GetError;
+
+ let target = target
+ .downcast_mut::<Self>()
+ .ok_or(GetError::WrongTargetType)?;
+
+ let #variant_pattern = target else {
+ return Err(GetError::WrongTargetEnumVariant);
+ };
+
+ let field: &mut dyn Any = match field_index {
+ #field_index_match_arms
+ _ => {
+ return Err(GetError::IndexOutOfBounds);
+ }
+ };
+
+ Ok(field)
+ } }
+ },
+ }
+}
diff --git a/engine-macros/src/reflection/field.rs b/engine-macros/src/reflection/field.rs
new file mode 100644
index 0000000..105a35b
--- /dev/null
+++ b/engine-macros/src/reflection/field.rs
@@ -0,0 +1,100 @@
+use quote::quote;
+
+use crate::reflection::visibility::generate as generate_visibility;
+
+pub struct ReflectionFieldGenOptions<'a>
+{
+ pub field_vis_override: Option<syn::Visibility>,
+ pub gen_get_byte_offset: &'a dyn Fn(&syn::Field) -> proc_macro2::TokenStream,
+}
+
+pub fn generate(
+ field: &syn::Field,
+ field_index: usize,
+ engine_crate_path: &syn::Path,
+ options: &ReflectionFieldGenOptions<'_>,
+) -> proc_macro2::TokenStream
+{
+ let field_ident = &field.ident;
+
+ let field_type = &field.ty;
+
+ let field_name = if let Some(field_ident) = field_ident {
+ let field_name = syn::LitStr::new(&field_ident.to_string(), field_ident.span());
+
+ quote! { Some(#field_name) }
+ } else {
+ quote! { None }
+ };
+
+ let field_byte_offset = (options.gen_get_byte_offset)(field);
+
+ let field_vis = options.field_vis_override.as_ref().unwrap_or(&field.vis);
+
+ let field_reflection_vis = generate_visibility(field_vis, engine_crate_path);
+
+ let get_type_fn_body =
+ { gen_get_optional_type_reflection(field_type, engine_crate_path) };
+
+ quote! {
+ #engine_crate_path::reflection::Field {
+ name: #field_name,
+ index: #field_index,
+ layout: std::alloc::Layout::new::<#field_type>(),
+ byte_offset: #field_byte_offset,
+ type_id: std::any::TypeId::of::<#field_type>(),
+ get_type_name: #engine_crate_path::reflection::FnWithDebug::new(|| {
+ std::any::type_name::<#field_type>()
+ }),
+ get_type: #engine_crate_path::reflection::FnWithDebug::new(|| {
+ #get_type_fn_body
+ }),
+ visibility: #field_reflection_vis
+ }
+ }
+}
+
+fn gen_get_optional_type_reflection(
+ field_type: &syn::Type,
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ quote! {
+ struct SpecializationTarget<Field>(std::marker::PhantomData<Field>);
+
+ trait FieldHasReflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>;
+ }
+
+ trait FieldDoesNotHaveReflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>;
+ }
+
+ impl<Field> FieldDoesNotHaveReflection for &SpecializationTarget<Field>
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>
+ {
+ None
+ }
+ }
+
+ impl<Field> FieldHasReflection for SpecializationTarget<Field>
+ where
+ Field: #engine_crate_path::reflection::Reflection
+ {
+ fn field_type_reflection(&self)
+ -> Option<&'static #engine_crate_path::reflection::Type>
+ {
+ Some(Field::type_reflection())
+ }
+ }
+
+ (&SpecializationTarget::<#field_type>(std::marker::PhantomData))
+ .field_type_reflection()
+ }
+}
diff --git a/engine-macros/src/reflection/options_attr.rs b/engine-macros/src/reflection/options_attr.rs
new file mode 100644
index 0000000..6fc2eea
--- /dev/null
+++ b/engine-macros/src/reflection/options_attr.rs
@@ -0,0 +1,39 @@
+#[derive(Debug, Default)]
+pub struct OptionsAttr
+{
+ pub impl_with_generics:
+ syn::punctuated::Punctuated<syn::AngleBracketedGenericArguments, syn::Token![,]>,
+}
+
+impl syn::parse::Parse for OptionsAttr
+{
+ fn parse(input: syn::parse::ParseStream) -> syn::Result<Self>
+ {
+ let params = input.parse_terminated(
+ syn::parse::ParseBuffer::parse::<syn::Meta>,
+ syn::Token![,],
+ )?;
+
+ let mut impl_with_generics = syn::punctuated::Punctuated::new();
+
+ for param in params {
+ let param = param.require_list()?;
+
+ if !param.path.is_ident("impl_with_generics") {
+ return Err(syn::Error::new_spanned(
+ param,
+ "Unrecognized attribute parameter",
+ ));
+ }
+
+ impl_with_generics = param.parse_args_with(
+ syn::punctuated::Punctuated::<
+ syn::AngleBracketedGenericArguments,
+ syn::Token![,],
+ >::parse_terminated,
+ )?;
+ }
+
+ Ok(Self { impl_with_generics })
+ }
+}
diff --git a/engine-macros/src/reflection/struct_impl.rs b/engine-macros/src/reflection/struct_impl.rs
new file mode 100644
index 0000000..27d62ba
--- /dev/null
+++ b/engine-macros/src/reflection/struct_impl.rs
@@ -0,0 +1,209 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+
+use crate::reflection::default_value::gen_get_default_value_fn;
+use crate::reflection::field::{generate as generate_field, ReflectionFieldGenOptions};
+use crate::reflection::options_attr::OptionsAttr;
+use crate::util::find_engine_crate_path;
+
+pub fn generate(
+ input: &syn::ItemStruct,
+ options: &OptionsAttr,
+) -> proc_macro2::TokenStream
+{
+ let engine_crate_path = find_engine_crate_path().unwrap();
+
+ if input.generics.params.is_empty() {
+ return gen_impl(input, None, &engine_crate_path);
+ }
+
+ let impls = options
+ .impl_with_generics
+ .iter()
+ .map(|generic_args| gen_impl(input, Some(generic_args), &engine_crate_path));
+
+ quote! {
+ #(#impls)*
+ }
+}
+
+fn gen_impl(
+ input: &syn::ItemStruct,
+ generic_args: Option<&syn::AngleBracketedGenericArguments>,
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ let fields = input.fields.iter().enumerate().map(|(field_index, field)| {
+ generate_field(
+ field,
+ field_index,
+ engine_crate_path,
+ &ReflectionFieldGenOptions {
+ field_vis_override: None,
+ gen_get_byte_offset: &|field| {
+ if let Some(field_ident) = &field.ident {
+ quote! { std::mem::offset_of!(Self, #field_ident) }
+ } else {
+ quote! { std::mem::offset_of!(Self, #field_index) }
+ }
+ },
+ },
+ )
+ });
+
+ let get_default_value_fn = gen_get_default_value_fn(&input.ident, generic_args);
+
+ let input_ident = &input.ident;
+
+ let generics_type_aliases = input
+ .generics
+ .type_params()
+ .zip(
+ generic_args
+ .iter()
+ .flat_map(|generic_args| &generic_args.args)
+ .filter_map(|generic_arg| match generic_arg {
+ syn::GenericArgument::Type(ty) => Some(ty),
+ _ => None,
+ }),
+ )
+ .map(|(type_param, generic_arg_type)| {
+ let type_param_ident = &type_param.ident;
+
+ quote! { type #type_param_ident = #generic_arg_type; }
+ });
+
+ let TryGetFieldFns {
+ immutable: try_get_field_immutable_fn,
+ mutable: try_get_field_mutable_fn,
+ } = gen_try_get_field_functions(input);
+
+ quote! {
+ unsafe impl #engine_crate_path::reflection::Reflection for
+ #input_ident #generic_args
+ {
+ const TYPE_REFLECTION: &#engine_crate_path::reflection::Type =
+ &const {
+ use std::any::Any;
+
+ use #engine_crate_path::reflection::GetError;
+
+ #(#generics_type_aliases)*
+
+ #engine_crate_path::reflection::Type::Struct(
+ #engine_crate_path::reflection::Struct {
+ fields: &[
+ #(#fields),*
+ ],
+ get_default_value: || {
+ #get_default_value_fn
+ },
+ cast_dyn_any: |ptr| ptr.cast::<Self>(),
+ try_get_field: #try_get_field_immutable_fn,
+ try_get_field_mut: #try_get_field_mutable_fn,
+ }
+ )
+ };
+ }
+ }
+}
+
+struct TryGetFieldFns
+{
+ immutable: TokenStream,
+ mutable: TokenStream,
+}
+
+fn gen_try_get_field_functions(input: &syn::ItemStruct) -> TryGetFieldFns
+{
+ let struct_pattern = match &input.fields {
+ syn::Fields::Unit => quote! { Self },
+ syn::Fields::Named(syn::FieldsNamed { named: named_fields, .. }) => {
+ let field_idents = named_fields.iter().map(|field| {
+ let Some(field_ident) = &field.ident else {
+ unreachable!();
+ };
+
+ field_ident
+ });
+
+ quote! { Self { #(#field_idents),* } }
+ }
+ syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: unnamed_fields, .. }) => {
+ let field_idents = unnamed_fields
+ .iter()
+ .enumerate()
+ .map(|(field_index, _)| format_ident!("field_{field_index}"));
+
+ quote! { Self(#(#field_idents),*) }
+ }
+ };
+
+ let field_index_match_arms = match &input.fields {
+ syn::Fields::Unit => quote! {},
+ syn::Fields::Named(syn::FieldsNamed { named: named_fields, .. }) => {
+ let match_arms =
+ named_fields.iter().enumerate().map(|(field_index, field)| {
+ let Some(field_ident) = &field.ident else {
+ unreachable!();
+ };
+
+ quote! { #field_index => { #field_ident } }
+ });
+
+ quote! { #(#match_arms)* }
+ }
+ syn::Fields::Unnamed(syn::FieldsUnnamed { unnamed: unnamed_fields, .. }) => {
+ let match_arms = unnamed_fields.iter().enumerate().map(|(field_index, _)| {
+ let field_ident = format_ident!("field_{field_index}");
+
+ quote! { #field_index => { #field_ident } }
+ });
+
+ quote! { #(#match_arms)* }
+ }
+ };
+
+ TryGetFieldFns {
+ immutable: quote! {
+ |target, field_index| { {
+ #![allow(unreachable_code)]
+
+ let target = target
+ .downcast_ref::<Self>()
+ .ok_or(GetError::WrongTargetType)?;
+
+ let #struct_pattern = target;
+
+ let field: &dyn Any = match field_index {
+ #field_index_match_arms
+ _ => {
+ return Err(GetError::IndexOutOfBounds);
+ }
+ };
+
+ Ok(field)
+ } }
+ },
+ mutable: quote! {
+ |target, field_index| { {
+ #![allow(unreachable_code)]
+
+ let target = target
+ .downcast_mut::<Self>()
+ .ok_or(GetError::WrongTargetType)?;
+
+ let #struct_pattern = target;
+
+ let field: &mut dyn Any = match field_index {
+ #field_index_match_arms
+ _ => {
+ return Err(GetError::IndexOutOfBounds);
+ }
+ };
+
+ Ok(field)
+ } }
+ },
+ }
+}
diff --git a/engine-macros/src/reflection/visibility.rs b/engine-macros/src/reflection/visibility.rs
new file mode 100644
index 0000000..c9e8524
--- /dev/null
+++ b/engine-macros/src/reflection/visibility.rs
@@ -0,0 +1,45 @@
+use quote::quote;
+
+use crate::util::syn_path_to_string;
+
+pub fn generate(
+ visibility: &syn::Visibility,
+ engine_crate_path: &syn::Path,
+) -> proc_macro2::TokenStream
+{
+ match visibility {
+ syn::Visibility::Public(_) => {
+ quote! { #engine_crate_path::reflection::Visibility::Pub }
+ }
+ syn::Visibility::Restricted(vis_restricted) => {
+ let vis_scope = if vis_restricted.in_token.is_some() {
+ let in_path = syn_path_to_string(&vis_restricted.path);
+
+ quote! {
+ #engine_crate_path::reflection::VisibilityScope::In(
+ std::borrow::Cow::Borrowed(#in_path)
+ )
+ }
+ } else {
+ let Some(scope) = vis_restricted.path.get_ident() else {
+ unreachable!();
+ };
+
+ if scope == "crate" {
+ quote! { #engine_crate_path::reflection::VisibilityScope::Crate }
+ } else if scope == "super" {
+ quote! { #engine_crate_path::reflection::VisibilityScope::Super }
+ } else if scope == "self" {
+ quote! { #engine_crate_path::reflection::VisibilityScope::SelfModule }
+ } else {
+ unreachable!();
+ }
+ };
+
+ quote! { #engine_crate_path::reflection::Visibility::PubScoped(#vis_scope) }
+ }
+ syn::Visibility::Inherited => {
+ quote! { #engine_crate_path::reflection::Visibility::Private }
+ }
+ }
+}
diff --git a/engine-macros/src/util.rs b/engine-macros/src/util.rs
new file mode 100644
index 0000000..dbeb042
--- /dev/null
+++ b/engine-macros/src/util.rs
@@ -0,0 +1,65 @@
+use std::fmt::Write;
+
+use quote::ToTokens;
+
+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,
+ }
+ };
+}
+
+pub fn find_engine_crate_path() -> Option<syn::Path>
+{
+ 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))
+}
+
+pub fn syn_path_to_string(path: &syn::Path) -> String
+{
+ let mut output = String::with_capacity(2 + path.segments.len() * 8);
+
+ if let Some(leading_colon) = path.leading_colon {
+ write!(output, "{}", leading_colon.to_token_stream()).unwrap();
+ }
+
+ for (segment, punct) in path.segments.pairs().map(syn::punctuated::Pair::into_tuple) {
+ let segment_ident = &segment.ident;
+
+ write!(output, "{segment_ident}").unwrap();
+
+ if let Some(punct) = punct {
+ write!(output, "{}", punct.to_token_stream()).unwrap();
+ }
+ }
+
+ output
+}