bevy_reflect: Add casting traits to support Box
and other wrapper types
#15532
+675
−658
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
Closes #3392
Closes #3400
Closes #6098
Closes #9929
Closes #14776
Here we go again...
I won't go into detail about the problem space as it has been covered in all those issues/PRs above (see #14776 for a high-level overview).
Essentially, we want to enable this:
without directly implementing
Reflect
forBox<dyn Reflect>
, as this makes it very easy to accidentally "double-box" your trait object. And though those double-boxed values would still behave as though it were just the inner type, it would really be potentially dozens of unnecessary pointer dereferences and heap allocations depending on how deep the double-boxing goes.So what's the solution?
#14776 attempted to solve the problem by using remote reflection and introducing a
RemoteBox<T>
type, as suggested by @soqb.While that approach works and saves us from the double-boxing problem, it still has some limitations.
The biggest limitation is the inability to use it as a type parameter to another type (e.g. the
T
inFoo<T>
), since remote reflection doesn't support remotely reflecting a type parameter.This is a real pain point as it significantly blocks us from supporting things like
Vec<Box<dyn Reflect>>
andHashMap<String, Box<dyn Reflect>>
—arguably some of the most common cases for this feature.So this PR attempts to support
Box<dyn Reflect>
without implementingReflect
on it and without using remote reflection.How? By taking another idea from @soqb!
Solution
This PR introduces two new traits:
CastPartialReflect
andCastReflect
. These are used to cast todyn PartialReflect
anddyn Reflect
, respectively. In fact, they're supertraits of those traits!By introducing these traits, we can alter the behavior of the logic used in the derive macro and custom implementations to rely on them. In other words, rather than requiring a type that is
PartialReflect
, we require one that casts toPartialReflect
.This means we can support any wrapper type around a reflected type and almost act as if it doesn't exist!
Obviously, this is meant to work with
Box<dyn Reflect>
andBox<dyn PartialReflect>
, but it also supportsBox<T: PartialReflect>
(if you need that for some reason) as well asBox<dyn CustomTrait>
, whereCustomTrait: CastPartialReflect
.Future Work
To keep these PRs smaller and tighter in scope, I decided to split this feature across multiple PRs.
This is the primary one, that lays down the groundwork for those to come.
The following PRs may potentially be somewhat controversial, and they may modify the API proposed here in less ergonomic ways in order to achieve their goals.
FromReflect
One thing we may want to do is support
FromReflect
for these types so their container types can also beFromReflect
.Now we could just implement it and return
None
or attempt to clone and cast the data (which is also likely to returnNone
), but this almost makes the problem worse: types containing these ones will almost always fail.Instead, we should look into making
ReflectFromReflect
available to these types. One way will be through a globalTypeRegistry
. However, since that may be a ways out, another way is through storing it onTypeInfo
, which is the solution I'm planning to look into.Serialization
With
FromReflect
solved, we're going to want a way to easily serialize and deserialize these types.Luckily, #15131 sorta already solved this problem. I'll work on rebasing that PR so it can be reviewed and merged, making support for
Box<dyn Reflect>
serialization almost trivial.However, it still needs review. If we don't like the approach or can't agree on the format for the dynamic types, then that affects
Box<dyn Reflect>
as well (seeing as it is partially a dynamic type).Modification
Currently, the wrapper type is completely hidden away (apart from
TypeInfo
andTypeRegistration
) once thrown behind adyn PartialReflect
ordyn Reflect
. This works fine, but it does mean that there is no way to modify the wrapper itself.As suggested by @SkiFire13, it would be nice if there was a way to
Reflect::set
orPartialReflect::apply
theBox<dyn Reflect>
itself so it's stored value could be changed at runtime.This is a trickier problem to solve and may require a larger change. I think it might be possible by moving the
apply
andset
methods to these new traits (or rather, introduce new traits for modifyingPartialReflect
andReflect
types). That should allow wrapper types likeBox
to control how they are modified.On top of another big change, this will potentially also affect the ergonomics of supporting custom trait objects. If so, it's not the end of the world, but all of these nuances are better off being explored in a separate PR.
Testing
You can test locally by running:
Showcase
A common occurrence when reflecting a type is wanting to store reflected data within a reflected type. This wasn't possible before, but now it is!
This works thanks to changes in
bevy_reflect
that makes the reflection logic now use the newCastPartialReflect
andCastReflect
traits. This means our fields no longer need to implementReflect
themselves, but can instead delegate their reflection behavior to another type at runtime!Migration Guide
The
Reflect
bound has been removed fromFromReflect
. If you were relying on that bound existing, you will now need to add it yourself:The
Reflect
bound has been removed fromTyped
. If you were relying on that bound existing, you will now need to add it yourself:The following trait methods have been moved:
Reflect::as_reflect
→CastReflect::as_reflect
Reflect::as_reflect_mut
→CastReflect::as_reflect_mut
Reflect::into_reflect
→CastReflect::into_reflect
Reflect
now also requiresCastReflect
.