Skip to content

bevy_reflect: Default attribute paths#9323

Open
MrGVSV wants to merge 1 commit intobevyengine:mainfrom
MrGVSV:reflect-default-attribute-parsing
Open

bevy_reflect: Default attribute paths#9323
MrGVSV wants to merge 1 commit intobevyengine:mainfrom
MrGVSV:reflect-default-attribute-parsing

Conversation

@MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented Jul 31, 2023

Objective

Followup to #9322 but with a breaking change.

With syn2, it's a whole lot easier to customize attributes. One small improvement we could make would be to change the default attribute to take a type path directly instead of a stringified path.

Solution

Updated the parsing logic to take an ExprPath directly.


Changelog

  • Default functions passed to #[reflect(default)] no longer need to be string literals (i.e. they can specify the path directly like #[reflect(default = path::to::func)])

Migration Guide

The optional function passed to #[reflect(default)] is no longer a stringified path. It can now be a standard type path:

mod defaults {
  fn foo() -> i32 {
    123
  }
}

// BEFORE
#[derive(Reflect)]
struct Foo {
  #[reflect(default = "defaults::foo")]
  value: i32
}

// AFTER
#[derive(Reflect)]
struct Foo {
  #[reflect(default = defaults::foo)]
  value: i32
}

@MrGVSV MrGVSV added S-Blocked This cannot move forward until something else changes C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Jul 31, 2023
@MrGVSV MrGVSV force-pushed the reflect-default-attribute-parsing branch from 6a62619 to 9e04df0 Compare August 30, 2023 00:37
@MrGVSV MrGVSV removed the S-Blocked This cannot move forward until something else changes label Aug 30, 2023
@MrGVSV MrGVSV marked this pull request as ready for review August 30, 2023 00:38
Copy link
Contributor

@killercup killercup left a comment

Choose a reason for hiding this comment

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

This should also allow autocomplete to work in these cases, right?

@MrGVSV
Copy link
Member Author

MrGVSV commented Aug 30, 2023

This should also allow autocomplete to work in these cases, right?

Ideally yes, but I can't speak for all editors.

@BenjaminBrienen BenjaminBrienen added D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jan 23, 2025
github-merge-queue bot pushed a commit that referenced this pull request Mar 11, 2025
# 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:

```rust
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:

```rust
#[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`.

```rust
#[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:

```rust
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:

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

<details>
<summary>Generated Code</summary>

```rust
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(Clone::clone(self)))
}
```

</details>

Or, we can specify our own cloning function:

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

fn incremental_clone(value: &usize) -> usize {
  *value + 1
}
```

<details>
<summary>Generated Code</summary>

```rust
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(incremental_clone(self)))
}
```

</details>

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.

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

fn incremental_clone(value: &usize) -> usize {
  *value + 1
}
```

<details>
<summary>Generated Code</summary>

```rust
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
    Ok(Box::new(Self {
        bar: Clone::clone(&self.bar),
        baz: incremental_clone(&self.baz),
    }))
}
```

</details>

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
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-Usability A targeted quality-of-life change that makes Bevy easier to use D-Straightforward Simple bug fixes and API improvements, docs, test and examples M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

No open projects
Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants