Skip to content
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

bevy_reflect: Reflection-based cloning #13432

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented May 20, 2024

Objective

Using Reflect::clone_value can be somewhat confusing to those unfamiliar with how Bevy's reflection crate works. For example take the following code:

let value: usize = 123;
let clone: Box<dyn Reflect> = value.clone_value();

What can we expect to be the underlying type of clone? If you guessed usize, then you're correct! Let's try another:

#[derive(Reflect, Clone)]
struct Foo(usize);

let value: Foo = Foo(123);
let clone: Box<dyn Reflect> = value.clone_value();

What about this code? What is the underlying type of clone? If you guessed Foo, unfortunately you'd be wrong. It's actually DynamicStruct.

It's not obvious that the generated Reflect impl actually calls Struct::clone_dynamic under the hood, which always returns DynamicStruct.

There are already some efforts to make this a bit more apparent to the end-user: #7207 changes the signature of Reflect::clone_value to instead return Box<dyn PartialReflect>, signaling that we're potentially returning a dynamic type.

But why can't we return Foo?

Foo can obviously be cloned— in fact, we already derived Clone on it. But even without the derive, this seems like something Reflect should be able to handle. Almost all types that implement Reflect either contain no data (trivially clonable), they contain a #[reflect_value] type (which, by definition, must implement Clone), or they contain another Reflect type (which recursively fall into one of these three categories).

This PR aims to enable true reflection-based cloning where you get back exactly the type that you think you do.

Solution

Add a Reflect::reflect_clone method which returns Result<Box<dyn Reflect>, ReflectCloneError>, where the Box<dyn Reflect> is guaranteed to be the same type as Self.

#[derive(Reflect)]
struct Foo(usize);

let value: Foo = Foo(123);
let clone: Box<dyn Reflect> = value.reflect_clone().unwrap();
assert!(clone.is::<Foo>());

Notice that we didn't even need to derive Clone for this to work: it's entirely powered via reflection!

Under the hood, the macro generates something like this:

fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(Self {
        // The `reflect_clone` impl for `usize` just makes use of its `Clone` impl
        0: Reflect::reflect_clone(&self.0)?.take().map_err(/* ... */)?,
    }))
}

If we did derive Clone, we can tell Reflect to rely on that instead:

#[derive(Reflect, Clone)]
#[reflect(Clone)]
struct Foo(usize);
Generated Code
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(Clone::clone(self)))
}

Or, we can specify our own cloning function:

#[derive(Reflect)]
#[reflect(Clone(incremental_clone))]
struct Foo(usize);

fn incremental_clone(value: &usize) -> usize {
  *value + 1
}
Generated Code
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(incremental_clone(self)))
}

Similarly, we can specify how fields should be cloned. This is important for fields that are #[reflect(ignore)]'d as we otherwise have no way to know how they should be cloned.

#[derive(Reflect)]
struct Foo {
 #[reflect(ignore, clone)]
  bar: usize,
  #[reflect(ignore, clone = "incremental_clone")]
  baz: usize,
}

fn incremental_clone(value: &usize) -> usize {
  *value + 1
}
Generated Code
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(Self {
        bar: Clone::clone(&self.bar),
        baz: incremental_clone(&self.baz),
    }))
}

If we don't supply a clone attribute for an ignored field, then the method will automatically return Err(ReflectCloneError::FieldNotClonable {/* ... */}).

Err values "bubble up" to the caller. So if Foo contains Bar and the reflect_clone method for Bar returns Err, then the reflect_clone method for Foo also returns Err.

Attribute Syntax

You might have noticed the differing syntax between the container attribute and the field attribute.

This was purely done for consistency with the current attributes. There are PRs aimed at improving this. #7317 aims at making the "special-cased" attributes more in line with the field attributes syntactically. And #9323 aims at moving away from the stringified paths in favor of just raw function paths.

Compatibility with Unique Reflect

This PR was designed with Unique Reflect (#7207) in mind. This method actually wouldn't change that much (if at all) under Unique Reflect. It would still exist on Reflect and it would still Option<Box<dyn Reflect>>. In fact, Unique Reflect would only improve the user's understanding of what this method returns.

We may consider moving what's currently Reflect::clone_value to PartialReflect and possibly renaming it to partial_reflect_clone or clone_dynamic to better indicate how it differs from reflect_clone.

Testing

You can test locally by running the following command:

cargo test --package bevy_reflect

Changelog

  • Added Reflect::reflect_clone method
  • Added ReflectCloneError error enum
  • Added #[reflect(Clone)] container attribute
  • Added #[reflect(clone)] field attribute

@MrGVSV MrGVSV added C-Enhancement A new feature S-Blocked This cannot move forward until something else changes A-Reflection Runtime information about types D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels May 20, 2024
@MrGVSV MrGVSV added this to the 0.15 milestone May 20, 2024
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.

@@ -509,6 +533,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.

Copy link
Contributor

@cBournhonesque cBournhonesque left a comment

Choose a reason for hiding this comment

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

Looks good to me, I like the change!
I think the naming will be clearer when the PartialReflect/UniqueReflect PR gets merged as well.
Had some small comments, and i'll probably wait for the blocking PR to be merged before approving

@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/reflect-clone branch from 0a05f93 to 51f9b2d Compare May 22, 2024 23:06
@MrGVSV MrGVSV added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Blocked This cannot move forward until something else changes labels May 22, 2024
@MrGVSV MrGVSV force-pushed the mrgvsv/reflect/reflect-clone branch from 51f9b2d to ad1146e Compare May 22, 2024 23:09
@MrGVSV MrGVSV marked this pull request as ready for review May 22, 2024 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Enhancement A new feature D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

None yet

2 participants