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: Fix ignored/skipped field order #7575

Merged
merged 5 commits into from Oct 22, 2023

Conversation

MrGVSV
Copy link
Member

@MrGVSV MrGVSV commented Feb 9, 2023

Objective

Fixes #5101
Alternative to #6511

Solution

Corrected the behavior for ignored fields in FromReflect, which was previously using the incorrect field indexes.

Similarly, fields marked with #[reflect(skip_serializing)] no longer break when using FromReflect after deserialization. This was done by modifying SerializationData to store a function pointer that can later be used to generate a default instance of the skipped field during deserialization.

The function pointer points to a function generated by the derive macro using the behavior designated by #[reflect(default)] (or just Default if none provided). The entire output of the macro is now wrapped in an unnamed constant which keeps this behavior hygienic.

Rationale

The biggest downside to this approach is that it requires fields marked #[reflect(skip_serializing)] to provide the ability to create a default instance— either via a Default impl or by specifying a custom one. While this isn't great, I think it might be justified by the fact that we really need to create this value when using FromReflect on a deserialized object. And we need to do this during deserialization because after that (at least for tuples and tuple structs) we lose information about which field is which: "is the value at index 1 in this DynamicTupleStruct the actual value for index 1 or is it really the value for index 2 since index 1 is skippable...?"

Alternatives

An alternative would be to store Option<Box<dyn Reflect>> within DynamicTuple and DynamicTupleStruct instead of just Box<dyn Reflect>. This would allow us to insert "empty"/"missing" fields during deserialization, thus saving the positional information of the skipped fields. However, this may require changing the API of Tuple and TupleStruct such that they can account for their dynamic counterparts returning None for a skipped field. In practice this would probably mean exposing the Option-ness of the dynamics onto implementors via methods like Tuple::drain or TupleStruct::field.

Personally, I think requiring Default would be better than muddying up the API to account for these special cases. But I'm open to trying out this other approach if the community feels that it's better.


Changelog

Public Changes

Fixed

  • The behaviors of #[reflect(ignore)] and #[reflect(skip_serializing)] are no longer dependent on field order

Changed

  • Fields marked with #[reflect(skip_serializing)] now need to either implement Default or specify a custom default function using #[reflect(default = "path::to::some_func")]
  • Deserializing a type with fields marked #[reflect(skip_serializing)] will now include that field initialized to its specified default value
  • SerializationData::new now takes the new SkippedField struct along with the skipped field index
  • Renamed SerializationData::is_ignored_field to SerializationData::is_field_skipped

Added

  • Added SkippedField struct
  • Added methods SerializationData::generate_default and SerializationData::iter_skipped

Internal Changes

Changed

  • Replaced members_to_serialization_denylist and BitSet<u32> with SerializationDataDef
  • The Reflect derive is more hygienic as it now outputs within an unnamed constant
  • StructField::index has been split up into StructField::declaration_index and StructField::reflection_index

Removed

  • Removed bitset dependency

Migration Guide

  • Fields marked #[reflect(skip_serializing)] now must implement Default or specify a custom default function with #[reflect(default = "path::to::some_func")]
    #[derive(Reflect)]
    struct MyStruct {
      #[reflect(skip_serializing)]
      #[reflect(default = "get_foo_default")]
      foo: Foo, // <- `Foo` does not impl `Default` so requires a custom function
      #[reflect(skip_serializing)]
      bar: Bar, // <- `Bar` impls `Default`
    }
    
    #[derive(Reflect)]
    struct Foo(i32);
    
    #[derive(Reflect, Default)]
    struct Bar(i32);
    
    fn get_foo_default() -> Foo {
      Foo(123)
    }
  • SerializationData::new has been changed to expect an iterator of (usize, SkippedField) rather than one of just usize
    // BEFORE
    SerializationData::new([0, 3].into_iter());
    
    // AFTER
    SerializationData::new([
      (0, SkippedField::new(field_0_default_fn)),
      (3, SkippedField::new(field_3_default_fn)),
    ].into_iter());
  • Serialization::is_ignored_field has been renamed to Serialization::is_field_skipped
  • Fields marked #[reflect(skip_serializing)] are now included in deserialization output. This may affect logic that expected those fields to be absent.

@MrGVSV MrGVSV added C-Bug An unexpected or incorrect behavior A-Reflection Runtime information about types C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide labels Feb 9, 2023
@MrGVSV MrGVSV added this to the 0.11 milestone Apr 21, 2023
@MrGVSV MrGVSV force-pushed the reflect-fix-ignore-order branch 3 times, most recently from 01b4ec7 to 113851b Compare April 23, 2023 06:06
@MrGVSV
Copy link
Member Author

MrGVSV commented May 23, 2023

Rebased. Not sure about the check-bans failure, but this should be ready for review again!

@mockersf
Copy link
Member

mockersf commented May 23, 2023

Not sure about the check-bans failure

You can ignore it, Bevy is a little late in a few dependencies and the config file should be updated after more crates are up-to-date.

Similarly, fields marked with #[reflect(skip_serializing)] no longer break when using FromReflect after deserialization. This was done by modifying SerializationData to store a function pointer that can later be used to generate a default instance of the skipped field during deserialization.

I disagree that skip_serializing should impact deserialization. serde has skip, skip_serializing and skip_deserializing. Deserializing should require the field even if it's marked as skip_serializing, to make it use a default value it should be marked as skip_deserializing too. If we want to indicate both, I think we should use a different name for that attribute.

@MrGVSV
Copy link
Member Author

MrGVSV commented May 23, 2023

Similarly, fields marked with #[reflect(skip_serializing)] no longer break when using FromReflect after deserialization. This was done by modifying SerializationData to store a function pointer that can later be used to generate a default instance of the skipped field during deserialization.

I disagree that skip_serializing should impact deserialization. serde has skip, skip_serializing and skip_deserializing. Deserializing should require the field even if it's marked as skip_serializing, to make it use a default value it should be marked as skip_deserializing too. If we want to indicate both, I think we should use a different name for that attribute.

Yeah that makes sense and would again be further aligned with serde so users have an easier time making the switch or at least conceptualizing the similarities.

That being said, I think it should be considered out of scope for this PR. This fixes a pretty longstanding and annoying bug, and shouldn't be blocked by the naming of the attribute used. We can probably create a separate ticket for that.

In fact, maybe I can work on splitting up the attributes while this PR gets reviewed so that we can hopefully be able to merge that change pretty soon before/after this one.

@cart cart modified the milestones: 0.11, 0.12 Jul 8, 2023
@cart
Copy link
Member

cart commented Jul 8, 2023

Moving this to 0.12 as we didn't get it reviewed in time.

@alice-i-cecile
Copy link
Member

@MrGVSV I'd like to not let this slip again; can you rebase?

@MrGVSV MrGVSV force-pushed the reflect-fix-ignore-order branch 2 times, most recently from 9716ed9 to e8203aa Compare October 2, 2023 19:40
@MrGVSV
Copy link
Member Author

MrGVSV commented Oct 4, 2023

I've removed the AssociatedData stuff. While that may be useful in the future, it wasn't really needed for this PR. I think when I originally wrote this, I was unaware that a non-capturing closure can also satisfy the function pointer type 😅.

I did, however, keep the change that puts the macro output in an unnamed constant since I think that's just generally useful regardless.

Copy link
Contributor

@soqb soqb left a comment

Choose a reason for hiding this comment

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

overall looks like excellent improvement; a couple nits and one question for correctness.

}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

yet another place where we might be better off merging Tuple and TupleStruct 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

Personally, I still think we should keep the concepts separate, both for user clarity and the potential to allow their definitions to diverge as needed. If anything, we might try to reduce some code duplication by making better use of macro_rules.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Thorough and well-tested, fixes an important bug. Code is clear and well-documented, as always <3

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Oct 20, 2023
@alice-i-cecile
Copy link
Member

@MrGVSV, are you ready for this to be merged now?

@MrGVSV
Copy link
Member Author

MrGVSV commented Oct 22, 2023

@MrGVSV, are you ready for this to be merged now?

@alice-i-cecile should be good to go!

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Oct 22, 2023
Merged via the queue into bevyengine:main with commit 60773e6 Oct 22, 2023
25 checks passed
robtfm pushed a commit to robtfm/bevy that referenced this pull request Oct 23, 2023
# Objective

Fixes bevyengine#5101
Alternative to bevyengine#6511

## Solution

Corrected the behavior for ignored fields in `FromReflect`, which was
previously using the incorrect field indexes.

Similarly, fields marked with `#[reflect(skip_serializing)]` no longer
break when using `FromReflect` after deserialization. This was done by
modifying `SerializationData` to store a function pointer that can later
be used to generate a default instance of the skipped field during
deserialization.

The function pointer points to a function generated by the derive macro
using the behavior designated by `#[reflect(default)]` (or just
`Default` if none provided). The entire output of the macro is now
wrapped in an [unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
which keeps this behavior hygienic.

#### Rationale

The biggest downside to this approach is that it requires fields marked
`#[reflect(skip_serializing)]` to provide the ability to create a
default instance— either via a `Default` impl or by specifying a custom
one. While this isn't great, I think it might be justified by the fact
that we really need to create this value when using `FromReflect` on a
deserialized object. And we need to do this _during_ deserialization
because after that (at least for tuples and tuple structs) we lose
information about which field is which: _"is the value at index 1 in
this `DynamicTupleStruct` the actual value for index 1 or is it really
the value for index 2 since index 1 is skippable...?"_

#### Alternatives

An alternative would be to store `Option<Box<dyn Reflect>>` within
`DynamicTuple` and `DynamicTupleStruct` instead of just `Box<dyn
Reflect>`. This would allow us to insert "empty"/"missing" fields during
deserialization, thus saving the positional information of the skipped
fields. However, this may require changing the API of `Tuple` and
`TupleStruct` such that they can account for their dynamic counterparts
returning `None` for a skipped field. In practice this would probably
mean exposing the `Option`-ness of the dynamics onto implementors via
methods like `Tuple::drain` or `TupleStruct::field`.

Personally, I think requiring `Default` would be better than muddying up
the API to account for these special cases. But I'm open to trying out
this other approach if the community feels that it's better.

---

## Changelog

### Public Changes

#### Fixed

- The behaviors of `#[reflect(ignore)]` and
`#[reflect(skip_serializing)]` are no longer dependent on field order

#### Changed

- Fields marked with `#[reflect(skip_serializing)]` now need to either
implement `Default` or specify a custom default function using
`#[reflect(default = "path::to::some_func")]`
- Deserializing a type with fields marked `#[reflect(skip_serializing)]`
will now include that field initialized to its specified default value
- `SerializationData::new` now takes the new `SkippedField` struct along
with the skipped field index
- Renamed `SerializationData::is_ignored_field` to
`SerializationData::is_field_skipped`

#### Added

- Added `SkippedField` struct
- Added methods `SerializationData::generate_default` and
`SerializationData::iter_skipped`

### Internal Changes

#### Changed

- Replaced `members_to_serialization_denylist` and `BitSet<u32>` with
`SerializationDataDef`
- The `Reflect` derive is more hygienic as it now outputs within an
[unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
- `StructField::index` has been split up into
`StructField::declaration_index` and `StructField::reflection_index`

#### Removed

- Removed `bitset` dependency

## Migration Guide

* Fields marked `#[reflect(skip_serializing)]` now must implement
`Default` or specify a custom default function with `#[reflect(default =
"path::to::some_func")]`
    ```rust
    #[derive(Reflect)]
    struct MyStruct {
      #[reflect(skip_serializing)]
      #[reflect(default = "get_foo_default")]
foo: Foo, // <- `Foo` does not impl `Default` so requires a custom
function
      #[reflect(skip_serializing)]
      bar: Bar, // <- `Bar` impls `Default`
    }
    
    #[derive(Reflect)]
    struct Foo(i32);
    
    #[derive(Reflect, Default)]
    struct Bar(i32);
    
    fn get_foo_default() -> Foo {
      Foo(123)
    }
    ```
* `SerializationData::new` has been changed to expect an iterator of
`(usize, SkippedField)` rather than one of just `usize`
    ```rust
    // BEFORE
    SerializationData::new([0, 3].into_iter());
    
    // AFTER
    SerializationData::new([
      (0, SkippedField::new(field_0_default_fn)),
      (3, SkippedField::new(field_3_default_fn)),
    ].into_iter());
    ```
* `Serialization::is_ignored_field` has been renamed to
`Serialization::is_field_skipped`
* Fields marked `#[reflect(skip_serializing)]` are now included in
deserialization output. This may affect logic that expected those fields
to be absent.
@MrGVSV MrGVSV deleted the reflect-fix-ignore-order branch October 26, 2023 21:32
ameknite pushed a commit to ameknite/bevy that referenced this pull request Nov 6, 2023
Fixes bevyengine#5101
Alternative to bevyengine#6511

Corrected the behavior for ignored fields in `FromReflect`, which was
previously using the incorrect field indexes.

Similarly, fields marked with `#[reflect(skip_serializing)]` no longer
break when using `FromReflect` after deserialization. This was done by
modifying `SerializationData` to store a function pointer that can later
be used to generate a default instance of the skipped field during
deserialization.

The function pointer points to a function generated by the derive macro
using the behavior designated by `#[reflect(default)]` (or just
`Default` if none provided). The entire output of the macro is now
wrapped in an [unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
which keeps this behavior hygienic.

The biggest downside to this approach is that it requires fields marked
`#[reflect(skip_serializing)]` to provide the ability to create a
default instance— either via a `Default` impl or by specifying a custom
one. While this isn't great, I think it might be justified by the fact
that we really need to create this value when using `FromReflect` on a
deserialized object. And we need to do this _during_ deserialization
because after that (at least for tuples and tuple structs) we lose
information about which field is which: _"is the value at index 1 in
this `DynamicTupleStruct` the actual value for index 1 or is it really
the value for index 2 since index 1 is skippable...?"_

An alternative would be to store `Option<Box<dyn Reflect>>` within
`DynamicTuple` and `DynamicTupleStruct` instead of just `Box<dyn
Reflect>`. This would allow us to insert "empty"/"missing" fields during
deserialization, thus saving the positional information of the skipped
fields. However, this may require changing the API of `Tuple` and
`TupleStruct` such that they can account for their dynamic counterparts
returning `None` for a skipped field. In practice this would probably
mean exposing the `Option`-ness of the dynamics onto implementors via
methods like `Tuple::drain` or `TupleStruct::field`.

Personally, I think requiring `Default` would be better than muddying up
the API to account for these special cases. But I'm open to trying out
this other approach if the community feels that it's better.

---

- The behaviors of `#[reflect(ignore)]` and
`#[reflect(skip_serializing)]` are no longer dependent on field order

- Fields marked with `#[reflect(skip_serializing)]` now need to either
implement `Default` or specify a custom default function using
`#[reflect(default = "path::to::some_func")]`
- Deserializing a type with fields marked `#[reflect(skip_serializing)]`
will now include that field initialized to its specified default value
- `SerializationData::new` now takes the new `SkippedField` struct along
with the skipped field index
- Renamed `SerializationData::is_ignored_field` to
`SerializationData::is_field_skipped`

- Added `SkippedField` struct
- Added methods `SerializationData::generate_default` and
`SerializationData::iter_skipped`

- Replaced `members_to_serialization_denylist` and `BitSet<u32>` with
`SerializationDataDef`
- The `Reflect` derive is more hygienic as it now outputs within an
[unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
- `StructField::index` has been split up into
`StructField::declaration_index` and `StructField::reflection_index`

- Removed `bitset` dependency

* Fields marked `#[reflect(skip_serializing)]` now must implement
`Default` or specify a custom default function with `#[reflect(default =
"path::to::some_func")]`
    ```rust
    #[derive(Reflect)]
    struct MyStruct {
      #[reflect(skip_serializing)]
      #[reflect(default = "get_foo_default")]
foo: Foo, // <- `Foo` does not impl `Default` so requires a custom
function
      #[reflect(skip_serializing)]
      bar: Bar, // <- `Bar` impls `Default`
    }

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

    #[derive(Reflect, Default)]
    struct Bar(i32);

    fn get_foo_default() -> Foo {
      Foo(123)
    }
    ```
* `SerializationData::new` has been changed to expect an iterator of
`(usize, SkippedField)` rather than one of just `usize`
    ```rust
    // BEFORE
    SerializationData::new([0, 3].into_iter());

    // AFTER
    SerializationData::new([
      (0, SkippedField::new(field_0_default_fn)),
      (3, SkippedField::new(field_3_default_fn)),
    ].into_iter());
    ```
* `Serialization::is_ignored_field` has been renamed to
`Serialization::is_field_skipped`
* Fields marked `#[reflect(skip_serializing)]` are now included in
deserialization output. This may affect logic that expected those fields
to be absent.
rdrpenguin04 pushed a commit to rdrpenguin04/bevy that referenced this pull request Jan 9, 2024
# Objective

Fixes bevyengine#5101
Alternative to bevyengine#6511

## Solution

Corrected the behavior for ignored fields in `FromReflect`, which was
previously using the incorrect field indexes.

Similarly, fields marked with `#[reflect(skip_serializing)]` no longer
break when using `FromReflect` after deserialization. This was done by
modifying `SerializationData` to store a function pointer that can later
be used to generate a default instance of the skipped field during
deserialization.

The function pointer points to a function generated by the derive macro
using the behavior designated by `#[reflect(default)]` (or just
`Default` if none provided). The entire output of the macro is now
wrapped in an [unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
which keeps this behavior hygienic.

#### Rationale

The biggest downside to this approach is that it requires fields marked
`#[reflect(skip_serializing)]` to provide the ability to create a
default instance— either via a `Default` impl or by specifying a custom
one. While this isn't great, I think it might be justified by the fact
that we really need to create this value when using `FromReflect` on a
deserialized object. And we need to do this _during_ deserialization
because after that (at least for tuples and tuple structs) we lose
information about which field is which: _"is the value at index 1 in
this `DynamicTupleStruct` the actual value for index 1 or is it really
the value for index 2 since index 1 is skippable...?"_

#### Alternatives

An alternative would be to store `Option<Box<dyn Reflect>>` within
`DynamicTuple` and `DynamicTupleStruct` instead of just `Box<dyn
Reflect>`. This would allow us to insert "empty"/"missing" fields during
deserialization, thus saving the positional information of the skipped
fields. However, this may require changing the API of `Tuple` and
`TupleStruct` such that they can account for their dynamic counterparts
returning `None` for a skipped field. In practice this would probably
mean exposing the `Option`-ness of the dynamics onto implementors via
methods like `Tuple::drain` or `TupleStruct::field`.

Personally, I think requiring `Default` would be better than muddying up
the API to account for these special cases. But I'm open to trying out
this other approach if the community feels that it's better.

---

## Changelog

### Public Changes

#### Fixed

- The behaviors of `#[reflect(ignore)]` and
`#[reflect(skip_serializing)]` are no longer dependent on field order

#### Changed

- Fields marked with `#[reflect(skip_serializing)]` now need to either
implement `Default` or specify a custom default function using
`#[reflect(default = "path::to::some_func")]`
- Deserializing a type with fields marked `#[reflect(skip_serializing)]`
will now include that field initialized to its specified default value
- `SerializationData::new` now takes the new `SkippedField` struct along
with the skipped field index
- Renamed `SerializationData::is_ignored_field` to
`SerializationData::is_field_skipped`

#### Added

- Added `SkippedField` struct
- Added methods `SerializationData::generate_default` and
`SerializationData::iter_skipped`

### Internal Changes

#### Changed

- Replaced `members_to_serialization_denylist` and `BitSet<u32>` with
`SerializationDataDef`
- The `Reflect` derive is more hygienic as it now outputs within an
[unnamed
constant](https://doc.rust-lang.org/stable/reference/items/constant-items.html#unnamed-constant)
- `StructField::index` has been split up into
`StructField::declaration_index` and `StructField::reflection_index`

#### Removed

- Removed `bitset` dependency

## Migration Guide

* Fields marked `#[reflect(skip_serializing)]` now must implement
`Default` or specify a custom default function with `#[reflect(default =
"path::to::some_func")]`
    ```rust
    #[derive(Reflect)]
    struct MyStruct {
      #[reflect(skip_serializing)]
      #[reflect(default = "get_foo_default")]
foo: Foo, // <- `Foo` does not impl `Default` so requires a custom
function
      #[reflect(skip_serializing)]
      bar: Bar, // <- `Bar` impls `Default`
    }
    
    #[derive(Reflect)]
    struct Foo(i32);
    
    #[derive(Reflect, Default)]
    struct Bar(i32);
    
    fn get_foo_default() -> Foo {
      Foo(123)
    }
    ```
* `SerializationData::new` has been changed to expect an iterator of
`(usize, SkippedField)` rather than one of just `usize`
    ```rust
    // BEFORE
    SerializationData::new([0, 3].into_iter());
    
    // AFTER
    SerializationData::new([
      (0, SkippedField::new(field_0_default_fn)),
      (3, SkippedField::new(field_3_default_fn)),
    ].into_iter());
    ```
* `Serialization::is_ignored_field` has been renamed to
`Serialization::is_field_skipped`
* Fields marked `#[reflect(skip_serializing)]` are now included in
deserialization output. This may affect logic that expected those fields
to be absent.
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-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Bug An unexpected or incorrect behavior S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

bevy_reflect: ignored fields are not ignored by FromReflect
5 participants