Skip to content

bevy_reflect: Reflection-based cloning #13432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 11, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mod incorrect_inner_type {
//~| ERROR: `TheirInner<T>` does not implement `PartialReflect` so cannot be introspected
//~| ERROR: `TheirInner<T>` does not implement `PartialReflect` so cannot be introspected
//~| ERROR: `TheirInner<T>` does not implement `TypePath` so cannot provide dynamic type path information
//~| ERROR: `TheirInner<T>` does not implement `TypePath` so cannot provide dynamic type path information
//~| ERROR: `?` operator has incompatible types
struct MyOuter<T: FromReflect + GetTypeRegistration> {
// Reason: Should not use `MyInner<T>` directly
Expand Down
48 changes: 45 additions & 3 deletions crates/bevy_reflect/derive/src/container_attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
attribute_parser::terminated_parser, custom_attributes::CustomAttributes,
derive_data::ReflectTraitToImpl,
};
use bevy_macro_utils::fq_std::{FQAny, FQOption};
use bevy_macro_utils::fq_std::{FQAny, FQClone, FQOption, FQResult};
use proc_macro2::{Ident, Span};
use quote::quote_spanned;
use syn::{
Expand All @@ -23,6 +23,7 @@ mod kw {
syn::custom_keyword!(Debug);
syn::custom_keyword!(PartialEq);
syn::custom_keyword!(Hash);
syn::custom_keyword!(Clone);
syn::custom_keyword!(no_field_bounds);
syn::custom_keyword!(opaque);
}
Expand Down Expand Up @@ -175,6 +176,7 @@ impl TypePathAttrs {
/// > __Note:__ Registering a custom function only works for special traits.
#[derive(Default, Clone)]
pub(crate) struct ContainerAttributes {
clone: TraitImpl,
debug: TraitImpl,
hash: TraitImpl,
partial_eq: TraitImpl,
Expand Down Expand Up @@ -236,12 +238,14 @@ impl ContainerAttributes {
self.parse_opaque(input)
} else if lookahead.peek(kw::no_field_bounds) {
self.parse_no_field_bounds(input)
} else if lookahead.peek(kw::Clone) {
self.parse_clone(input)
} else if lookahead.peek(kw::Debug) {
self.parse_debug(input)
} else if lookahead.peek(kw::PartialEq) {
self.parse_partial_eq(input)
} else if lookahead.peek(kw::Hash) {
self.parse_hash(input)
} else if lookahead.peek(kw::PartialEq) {
self.parse_partial_eq(input)
} else if lookahead.peek(Ident::peek_any) {
self.parse_ident(input)
} else {
Expand Down Expand Up @@ -274,6 +278,26 @@ impl ContainerAttributes {
Ok(())
}

/// Parse `clone` attribute.
///
/// Examples:
/// - `#[reflect(Clone)]`
/// - `#[reflect(Clone(custom_clone_fn))]`
fn parse_clone(&mut self, input: ParseStream) -> syn::Result<()> {
let ident = input.parse::<kw::Clone>()?;

if input.peek(token::Paren) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should use darling as a helper crate to parse attributes?
It seems easier than what you're doing, especially if the attributes become more complex later on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could utilize something like darling in the future (not this PR though). I'm not sure our parsing logic is so complex we really need it, but it's certainly worth considering.

let content;
parenthesized!(content in input);
let path = content.parse::<Path>()?;
self.clone.merge(TraitImpl::Custom(path, ident.span))?;
} else {
self.clone = TraitImpl::Implemented(ident.span);
}

Ok(())
}

/// Parse special `Debug` registration.
///
/// Examples:
Expand Down Expand Up @@ -523,6 +547,24 @@ impl ContainerAttributes {
}
}

pub fn get_clone_impl(&self, bevy_reflect_path: &Path) -> Option<proc_macro2::TokenStream> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used? I couldn't understand this part.
I get the clone_impl on struct,tuple,enum,value,etc. but not this.

Or is this the top-level impl?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used by derive_data.rs. I could probably move this logic into that file directly, since it's the only place where it's used.

match &self.clone {
&TraitImpl::Implemented(span) => Some(quote_spanned! {span=>
#[inline]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#FQClone::clone(self)))
}
}),
&TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=>
#[inline]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#impl_fn(self)))
}
}),
TraitImpl::NotImplemented => None,
}
}

pub fn custom_attributes(&self) -> &CustomAttributes {
&self.custom_attributes
}
Expand Down
206 changes: 203 additions & 3 deletions crates/bevy_reflect/derive/src/derive_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ use crate::{
where_clause_options::WhereClauseOptions,
REFLECT_ATTRIBUTE_NAME, TYPE_NAME_ATTRIBUTE_NAME, TYPE_PATH_ATTRIBUTE_NAME,
};
use quote::{quote, ToTokens};
use quote::{format_ident, quote, ToTokens};
use syn::token::Comma;

use crate::enum_utility::{EnumVariantOutputData, ReflectCloneVariantBuilder, VariantBuilder};
use crate::field_attributes::CloneBehavior;
use crate::generics::generate_generics;
use bevy_macro_utils::fq_std::{FQClone, FQOption, FQResult};
use syn::{
parse_str, punctuated::Punctuated, spanned::Spanned, Data, DeriveInput, Field, Fields,
GenericParam, Generics, Ident, LitStr, Meta, Path, PathSegment, Type, TypeParam, Variant,
GenericParam, Generics, Ident, LitStr, Member, Meta, Path, PathSegment, Type, TypeParam,
Variant,
};

pub(crate) enum ReflectDerive<'a> {
Expand Down Expand Up @@ -266,7 +270,7 @@ impl<'a> ReflectDerive<'a> {
{
return Err(syn::Error::new(
meta.type_path().span(),
format!("a #[{TYPE_PATH_ATTRIBUTE_NAME} = \"...\"] attribute must be specified when using {provenance}")
format!("a #[{TYPE_PATH_ATTRIBUTE_NAME} = \"...\"] attribute must be specified when using {provenance}"),
));
}

Expand Down Expand Up @@ -546,6 +550,31 @@ impl<'a> StructField<'a> {
pub fn attrs(&self) -> &FieldAttributes {
&self.attrs
}

/// Generates a [`Member`] based on this field.
///
/// If the field is unnamed, the declaration index is used.
/// This allows this member to be used for both active and ignored fields.
pub fn to_member(&self) -> Member {
match &self.data.ident {
Some(ident) => Member::Named(ident.clone()),
None => Member::Unnamed(self.declaration_index.into()),
}
}

/// Returns a token stream for generating a `FieldId` for this field.
pub fn field_id(&self, bevy_reflect_path: &Path) -> proc_macro2::TokenStream {
match &self.data.ident {
Some(ident) => {
let name = ident.to_string();
quote!(#bevy_reflect_path::FieldId::Named(#bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(#name)))
}
None => {
let index = self.declaration_index;
quote!(#bevy_reflect_path::FieldId::Unnamed(#index))
}
}
}
}

impl<'a> ReflectStruct<'a> {
Expand Down Expand Up @@ -655,6 +684,135 @@ impl<'a> ReflectStruct<'a> {
#bevy_reflect_path::TypeInfo::#info_variant(#info)
}
}
/// Returns the `Reflect::reflect_clone` impl, if any, as a `TokenStream`.
pub fn get_clone_impl(&self) -> Option<proc_macro2::TokenStream> {
let bevy_reflect_path = self.meta().bevy_reflect_path();

if let container_clone @ Some(_) = self.meta().attrs().get_clone_impl(bevy_reflect_path) {
return container_clone;
}

let mut tokens = proc_macro2::TokenStream::new();

for field in self.fields().iter() {
let field_ty = field.reflected_type();
let member = field.to_member();
let accessor = self.access_for_field(field, false);

match &field.attrs.clone {
CloneBehavior::Default => {
let value = if field.attrs.ignore.is_ignored() {
let field_id = field.field_id(bevy_reflect_path);

quote! {
return #FQResult::Err(#bevy_reflect_path::ReflectCloneError::FieldNotCloneable {
field: #field_id,
variant: #FQOption::None,
container_type_path: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(
<Self as #bevy_reflect_path::TypePath>::type_path()
)
})
}
} else {
quote! {
#bevy_reflect_path::PartialReflect::reflect_clone(#accessor)?
.take()
.map_err(|value| #bevy_reflect_path::ReflectCloneError::FailedDowncast {
expected: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(
<#field_ty as #bevy_reflect_path::TypePath>::type_path()
),
received: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Owned(
#bevy_reflect_path::__macro_exports::alloc_utils::ToString::to_string(
#bevy_reflect_path::DynamicTypePath::reflect_type_path(&*value)
)
),
})?
}
};

tokens.extend(quote! {
#member: #value,
});
}
CloneBehavior::Trait => {
tokens.extend(quote! {
#member: #FQClone::clone(#accessor),
});
}
CloneBehavior::Func(clone_fn) => {
tokens.extend(quote! {
#member: #clone_fn(#accessor),
});
}
}
}

let ctor = match self.meta.remote_ty() {
Some(ty) => {
let ty = ty.as_expr_path().ok()?.to_token_stream();
quote! {
Self(#ty {
#tokens
})
}
}
None => {
quote! {
Self {
#tokens
}
}
}
};

Some(quote! {
#[inline]
#[allow(unreachable_code, reason = "Ignored fields without a `clone` attribute will early-return with an error")]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#ctor))
}
})
}

/// Generates an accessor for the given field.
///
/// The mutability of the access can be controlled by the `is_mut` parameter.
///
/// Generally, this just returns something like `&self.field`.
/// However, if the struct is a remote wrapper, this then becomes `&self.0.field` in order to access the field on the inner type.
///
/// If the field itself is a remote type, the above accessor is further wrapped in a call to `ReflectRemote::as_wrapper[_mut]`.
pub fn access_for_field(
&self,
field: &StructField<'a>,
is_mutable: bool,
) -> proc_macro2::TokenStream {
let bevy_reflect_path = self.meta().bevy_reflect_path();
let member = field.to_member();

let prefix_tokens = if is_mutable { quote!(&mut) } else { quote!(&) };

let accessor = if self.meta.is_remote_wrapper() {
quote!(self.0.#member)
} else {
quote!(self.#member)
};

match &field.attrs.remote {
Some(wrapper_ty) => {
let method = if is_mutable {
format_ident!("as_wrapper_mut")
} else {
format_ident!("as_wrapper")
};

quote! {
<#wrapper_ty as #bevy_reflect_path::ReflectRemote>::#method(#prefix_tokens #accessor)
}
}
None => quote!(#prefix_tokens #accessor),
}
}
}

impl<'a> ReflectEnum<'a> {
Expand Down Expand Up @@ -757,6 +915,48 @@ impl<'a> ReflectEnum<'a> {
#bevy_reflect_path::TypeInfo::Enum(#info)
}
}

/// Returns the `Reflect::reflect_clone` impl, if any, as a `TokenStream`.
pub fn get_clone_impl(&self) -> Option<proc_macro2::TokenStream> {
let bevy_reflect_path = self.meta().bevy_reflect_path();

if let container_clone @ Some(_) = self.meta().attrs().get_clone_impl(bevy_reflect_path) {
return container_clone;
}

let this = Ident::new("this", Span::call_site());
let EnumVariantOutputData {
variant_patterns,
variant_constructors,
..
} = ReflectCloneVariantBuilder::new(self).build(&this);

let inner = quote! {
match #this {
#(#variant_patterns => #variant_constructors),*
}
};

let body = if self.meta.is_remote_wrapper() {
quote! {
let #this = <Self as #bevy_reflect_path::ReflectRemote>::as_remote(self);
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(<Self as #bevy_reflect_path::ReflectRemote>::into_wrapper(#inner)))
}
} else {
quote! {
let #this = self;
#FQResult::Ok(#bevy_reflect_path::__macro_exports::alloc_utils::Box::new(#inner))
}
};

Some(quote! {
#[inline]
#[allow(unreachable_code, reason = "Ignored fields without a `clone` attribute will early-return with an error")]
fn reflect_clone(&self) -> #FQResult<#bevy_reflect_path::__macro_exports::alloc_utils::Box<dyn #bevy_reflect_path::Reflect>, #bevy_reflect_path::ReflectCloneError> {
#body
}
})
}
}

impl<'a> EnumVariant<'a> {
Expand Down
Loading