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

[Merged by Bors] - Make Reflect safe to implement #5010

Closed
wants to merge 26 commits into from

Conversation

PROMETHIA-27
Copy link
Contributor

@PROMETHIA-27 PROMETHIA-27 commented Jun 14, 2022

Objective

Currently, Reflect is unsafe to implement because of a contract in which any and any_mut must return self, or downcast will cause UB. This PR makes Reflect safe, makes downcast not use unsafe, and eliminates this contract.

Solution

This PR adds a method to Reflect, any. It also renames the old any to as_any.
any now takes a Box<Self> and returns a Box<dyn Any>.


Changelog

Added:

  • any() method
  • represents() method

Changed:

  • Reflect is now a safe trait
  • downcast() is now safe
  • The old any is now called as_any, and any_mut is now as_mut_any

Migration Guide

  • Reflect derives should not have to change anything
  • Manual reflect impls will need to remove the unsafe keyword, add any() implementations, and rename the old any and any_mut to as_any and as_mut_any.
  • Calls to any/any_mut must be changed to as_any/as_mut_any

Points of discussion:

  • Should renaming any be avoided and instead name the new method any_box?
  • Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.
  • Could/should is and type_id() be implemented differently? For example, moving is onto Reflect as an fn(&self, TypeId) -> bool

@PROMETHIA-27 PROMETHIA-27 requested a review from MrGVSV June 14, 2022 01:07
@alice-i-cecile alice-i-cecile added C-Usability A simple quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types labels Jun 14, 2022
@alice-i-cecile alice-i-cecile added this to the Bevy 0.8 milestone Jun 14, 2022
@alice-i-cecile
Copy link
Member

Nice work!

Should renaming any be avoided and instead name the new method any_box?

I think that's fine; this naming scheme is clearer.

Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.

I wouldn't accept this justification to make it unsafe, so I won't block on the answer here.

Could/should is and type_id() be implemented differently? For example, moving is onto Reflect as an fn(&self, TypeId) -> bool

I like the current design quite a bit; I think it's quite clear and should be easy to use.

crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/lib.rs Show resolved Hide resolved
examples/reflection/trait_reflection.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/struct_trait.rs Outdated Show resolved Hide resolved
@MrGVSV
Copy link
Member

MrGVSV commented Jun 14, 2022

Should renaming any be avoided and instead name the new method any_box?

I agree with the name change. Although, it should be noted that it goes against the convention we've established thus far (DynamicTuple::insert_boxed, DynamicStruct::push_box, etc.)

Could/should is and type_id() be implemented differently? For example, moving is onto Reflect as an fn(&self, TypeId) -> bool

I think it makes sense to move is to Reflect if it's given a default implementation. Otherwise, it's probably fine to just to reflect_value.type_id() == other_type_id.

@alice-i-cecile
Copy link
Member

I don't remember the conclusion: do we care about implementing Reflect for non-Rust types? If so, we should either split off type_id or have it return an Option.

@PROMETHIA-27
Copy link
Contributor Author

PROMETHIA-27 commented Jun 14, 2022

Thinking about it more, I'm wondering if downcast should be more directly controlled?

  • Normal types want to downcast to themselves and report their own TypeID
  • Dynamic types want to be unable to downcast and report a different TypeID, but optionally
  • Box wants to downcast to its inner type and report its inner type's TypeId. But what if we want to downcast to the box itself? E.g. we have a Box<dyn Reflect>(i32) behind a Box<dyn Reflect>, downcasting it would give the i32, but what if we just want the inner Box?

@killercup
Copy link
Contributor

killercup commented Jun 20, 2022

Just a comment on the wording: To be consistent with the Rust API guidelines, I'd suggest calling the methods into_any(Box<Self>) -> Box<dyn Any>, as_any(&self) -> &dyn Any, and as_mut_any(&mut self) -> &mut dyn Any. That is also already the naming followed by the methods below.

(That also allows you to deprecate the previous any method and add a blanket impl for a short transition period if that is at all needed.)

@PROMETHIA-27
Copy link
Contributor Author

This PR is more or less blocked on the Unique Reflect RFC; that might wildly change what is necessary here/possibly obsolete this PR.

Copy link
Member

@MrGVSV MrGVSV left a comment

Choose a reason for hiding this comment

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

Like you said, I'm sure things will change a lot with the Unique Reflect RfC, but I left some comments just in case.

My main issue rn is with the inclusion of an underlying_type_id method. It could be worth it, but I'm just having a hard time getting past its possible confusions with proxy types.


/// Returns the value as a [`&mut dyn Any`][std::any::Any].
fn any_mut(&mut self) -> &mut dyn Any;
fn as_mut_any(&mut self) -> &mut dyn Any;
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be as_any_mut to match the naming of other methods in the codebase?

Copy link
Contributor Author

@PROMETHIA-27 PROMETHIA-27 Jun 21, 2022

Choose a reason for hiding this comment

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

quoting the rust api guidelines:

If the mut qualifier in the name of a conversion method constitutes part of the return type, it should appear as it would appear in the type. For example Vec::as_mut_slice returns a mut slice; it does what it says. This name is preferred over as_slice_mut.

But I could accept reversing it (to as_any_mut) for consistency, as it's not in the scope of this PR to change global naming conventions.

Copy link
Member

Choose a reason for hiding this comment

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

Hm yeah it might be good to rename this to as_any_mut at least to match as_reflect_mut. But we can look into renaming these according to a more Rust-approved convention later.

crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/type_info.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/utility.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/src/utility.rs Outdated Show resolved Hide resolved
@PROMETHIA-27
Copy link
Contributor Author

PROMETHIA-27 commented Jun 21, 2022

I'd be comfortable waiting for Unique Reflect for this, but if anyone else (@MrGVSV?) would also like to see this move forward first then I'll give this another pass and resolve my issues with it. Mainly I want to redo is, type_id, and downcast to be clearer as to what they mean, probably reusing terminology from the Unique Reflect RFC. In fact I'll probably want to split them roughly like this:
represents: is but for checking if this represents the given type (probably via repr_type_name comparison)
is: Add documentation clarifying that this refers to the underlying type, and what that means.
repr_type_name(): The optional Cow<&str> of the represented type's name.
type_name(): Replaced by inheriting from TypeName, a new trait with a blanket impl so that no one can violate the rule that this is the exact name of the underlying type.
repr_type_id(): The optional TypeId of the represented type. I may omit this entirely since None can't mean "this value doesn't represent a type", only "this value doesn't know the TypeId of the represented type". Maybe return a Result with multiple variants instead, to be more descriptive?
type_id(): Replaced by inheriting Any::type_id()
downcast into_underlying: Downcasts only from the dyn reflect into the true underlying type as according to is.
downcast_ref as_underlying: see above
downcast_mut as_mut_underlying: see above
for "downcasting" (I believe the RFC calls this "sidecasting") to the represented type, use FromReflect.
With these changes (and maybe more- splitting the any/reflect methods into blanket impls as well, to force returning the underlying type?) I would also be happy to merge this before Unique Reflect.

@MrGVSV
Copy link
Member

MrGVSV commented Jun 21, 2022

I'd be comfortable waiting for Unique Reflect for this, but if anyone else (@MrGVSV?) would also like to see this move forward first then I'll give this another pass and resolve my issues with it.

Yeah I think it makes sense to get this merged even before the RFC.

Mainly I want to redo is, type_id, and downcast to be clearer as to what they mean, probably reusing terminology from the Unique Reflect RFC. In fact I'll probably want to split them roughly like this:

I don't think these changes should be part of this PR. They add new functionality that isn't directly in attempts to make Reflect safe.

represents: is but for checking if this represents the given type (probably via repr_type_name comparison)
is: Add documentation clarifying that this refers to the underlying type, and what that means.

I really like the idea to include a represents method. The other suggestions, though, I'm a bit more hesitant on, especially with the Unique Reflect RFC possibly having a large impact on their usage/implementation.

@PROMETHIA-27
Copy link
Contributor Author

That's fair; I suppose fixing the API is what Unique Reflect is for. I'll start working on this soon.


/// Returns the value as a [`&mut dyn Any`][std::any::Any].
fn any_mut(&mut self) -> &mut dyn Any;
fn as_mut_any(&mut self) -> &mut dyn Any;
Copy link
Member

Choose a reason for hiding this comment

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

Hm yeah it might be good to rename this to as_any_mut at least to match as_reflect_mut. But we can look into renaming these according to a more Rust-approved convention later.

crates/bevy_reflect/src/reflect.rs Show resolved Hide resolved
crates/bevy_reflect/src/reflect.rs Outdated Show resolved Hide resolved
@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 Jun 27, 2022
@PROMETHIA-27
Copy link
Contributor Author

I've just removed the doc links; I don't think there's a way to make them work. They won't recognize Self as dyn Reflect, and fully qualified <dyn Reflect>:: syntax isn't supported, so there's no way to reference the functions.

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.

Surprisingly simple in practice; those impl files make this look way more involved than it is.

bors r+

bors bot pushed a commit that referenced this pull request Jun 27, 2022
# Objective

Currently, `Reflect` is unsafe to implement because of a contract in which `any` and `any_mut` must return `self`, or `downcast` will cause UB. This PR makes `Reflect` safe, makes `downcast` not use unsafe, and eliminates this contract. 

## Solution

This PR adds a method to `Reflect`, `any`. It also renames the old `any` to `as_any`.
`any` now takes a `Box<Self>` and returns a `Box<dyn Any>`. 

---

## Changelog

### Added:
- `any()` method
- `represents()` method

### Changed:
- `Reflect` is now a safe trait
- `downcast()` is now safe
- The old `any` is now called `as_any`, and `any_mut` is now `as_mut_any`

## Migration Guide

- Reflect derives should not have to change anything
- Manual reflect impls will need to remove the `unsafe` keyword, add `any()` implementations, and rename the old `any` and `any_mut` to `as_any` and `as_mut_any`.
- Calls to `any`/`any_mut` must be changed to `as_any`/`as_mut_any`

## Points of discussion:

- Should renaming `any` be avoided and instead name the new method `any_box`?
- ~~Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.~~
- ~~Could/should `is` and `type_id()` be implemented differently? For example, moving `is` onto `Reflect` as an `fn(&self, TypeId) -> bool`~~


Co-authored-by: PROMETHIA-27 <[email protected]>
@bors bors bot changed the title Make Reflect safe to implement [Merged by Bors] - Make Reflect safe to implement Jun 27, 2022
@bors bors bot closed this Jun 27, 2022
@PROMETHIA-27 PROMETHIA-27 deleted the make-reflect-safe branch June 27, 2022 17:13
impl dyn Reflect {
/// Downcasts the value to type `T`, consuming the trait object.
///
/// If the underlying value is not of type `T`, returns `Err(self)`.
pub fn downcast<T: Reflect>(self: Box<dyn Reflect>) -> Result<Box<T>, Box<dyn Reflect>> {
// SAFE?: Same approach used by std::any::Box::downcast. ReflectValue is always Any and type
// has been checked.
if self.is::<T>() {
Copy link
Member

Choose a reason for hiding this comment

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

Downcast does this check internally. We should probably remove it from here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't without changing the error type from Box<dyn Reflect> to Box<dyn Any>

Copy link
Member

Choose a reason for hiding this comment

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

Spun out into #5120.

inodentry pushed a commit to IyesGames/bevy that referenced this pull request Aug 8, 2022
# Objective

Currently, `Reflect` is unsafe to implement because of a contract in which `any` and `any_mut` must return `self`, or `downcast` will cause UB. This PR makes `Reflect` safe, makes `downcast` not use unsafe, and eliminates this contract. 

## Solution

This PR adds a method to `Reflect`, `any`. It also renames the old `any` to `as_any`.
`any` now takes a `Box<Self>` and returns a `Box<dyn Any>`. 

---

## Changelog

### Added:
- `any()` method
- `represents()` method

### Changed:
- `Reflect` is now a safe trait
- `downcast()` is now safe
- The old `any` is now called `as_any`, and `any_mut` is now `as_mut_any`

## Migration Guide

- Reflect derives should not have to change anything
- Manual reflect impls will need to remove the `unsafe` keyword, add `any()` implementations, and rename the old `any` and `any_mut` to `as_any` and `as_mut_any`.
- Calls to `any`/`any_mut` must be changed to `as_any`/`as_mut_any`

## Points of discussion:

- Should renaming `any` be avoided and instead name the new method `any_box`?
- ~~Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.~~
- ~~Could/should `is` and `type_id()` be implemented differently? For example, moving `is` onto `Reflect` as an `fn(&self, TypeId) -> bool`~~


Co-authored-by: PROMETHIA-27 <[email protected]>
james7132 pushed a commit to james7132/bevy that referenced this pull request Oct 28, 2022
# Objective

Currently, `Reflect` is unsafe to implement because of a contract in which `any` and `any_mut` must return `self`, or `downcast` will cause UB. This PR makes `Reflect` safe, makes `downcast` not use unsafe, and eliminates this contract. 

## Solution

This PR adds a method to `Reflect`, `any`. It also renames the old `any` to `as_any`.
`any` now takes a `Box<Self>` and returns a `Box<dyn Any>`. 

---

## Changelog

### Added:
- `any()` method
- `represents()` method

### Changed:
- `Reflect` is now a safe trait
- `downcast()` is now safe
- The old `any` is now called `as_any`, and `any_mut` is now `as_mut_any`

## Migration Guide

- Reflect derives should not have to change anything
- Manual reflect impls will need to remove the `unsafe` keyword, add `any()` implementations, and rename the old `any` and `any_mut` to `as_any` and `as_mut_any`.
- Calls to `any`/`any_mut` must be changed to `as_any`/`as_mut_any`

## Points of discussion:

- Should renaming `any` be avoided and instead name the new method `any_box`?
- ~~Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.~~
- ~~Could/should `is` and `type_id()` be implemented differently? For example, moving `is` onto `Reflect` as an `fn(&self, TypeId) -> bool`~~


Co-authored-by: PROMETHIA-27 <[email protected]>
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
# Objective

Currently, `Reflect` is unsafe to implement because of a contract in which `any` and `any_mut` must return `self`, or `downcast` will cause UB. This PR makes `Reflect` safe, makes `downcast` not use unsafe, and eliminates this contract. 

## Solution

This PR adds a method to `Reflect`, `any`. It also renames the old `any` to `as_any`.
`any` now takes a `Box<Self>` and returns a `Box<dyn Any>`. 

---

## Changelog

### Added:
- `any()` method
- `represents()` method

### Changed:
- `Reflect` is now a safe trait
- `downcast()` is now safe
- The old `any` is now called `as_any`, and `any_mut` is now `as_mut_any`

## Migration Guide

- Reflect derives should not have to change anything
- Manual reflect impls will need to remove the `unsafe` keyword, add `any()` implementations, and rename the old `any` and `any_mut` to `as_any` and `as_mut_any`.
- Calls to `any`/`any_mut` must be changed to `as_any`/`as_mut_any`

## Points of discussion:

- Should renaming `any` be avoided and instead name the new method `any_box`?
- ~~Could there be a performance regression from avoiding the unsafe? I doubt it, but this change does seem to introduce redundant checks.~~
- ~~Could/should `is` and `type_id()` be implemented differently? For example, moving `is` onto `Reflect` as an `fn(&self, TypeId) -> bool`~~


Co-authored-by: PROMETHIA-27 <[email protected]>
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-Usability A simple quality-of-life change that makes Bevy easier to use S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

None yet

7 participants