-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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: Add ReflectBox
to remotely reflect Box
#14776
Conversation
Nitpick: it can, since I would also add to the caveats that this way of reflecting a |
IMO no: there's a lot of complexity here, both for users and maintainers. The limitations are also unpleasant, especially the one raised by SkiFire. Avoiding a modest footgun is not worth the complexity cost. I would much rather move forward this than nothing though.
I can't think of anything unfortunately :(
IMO no: doc(hidden) items are mysterious and frustrating to users. I'd like to avoid them in general without an extremely compelling reason. |
i apologise if i'm really not getting it: let mut boxed: Box<dyn Reflect> = Box::new(123u8);
boxed = Box::new(456u16); |
looking at the PR description, i'm very confident all of the mentioned caveats can be solved:
we can definitely implement
essentially:
// loosely defined as:
trait Reflectable {
type Via: Reflect + RemoteReflect<Self>;
}
impl<T: Reflect> Reflectable for T {
type Via = T;
}
impl<T: ToPartialReflect> Reflectable for Box<T> {
type Via = ReflectBox<T>;
} and then, in all relevant |
What I mean is that you won't be able to do this if #[derive(Reflect)]
#[reflect(from_reflect = false)]
struct Foo {
#[reflect(remote = ReflectBox<dyn Reflect>)]
boxed: Box<dyn Reflect>,
}
let mut foo = Foo {
boxed: Box::new(123u8),
};
let replacement: Box<dyn Reflect> = Box::new(456u16);
// Imagine that somewhere else gets `foo` as a `&mut dyn Reflect` and
// pattern matches it against a `ReflectMut::Struct`. This may an UI for example.
let reflected: &mut dyn Reflect = foo;
let ReflectMut::Struct(s) = reflected.reflect_mut() else { panic!() };
// Then it wants to change its `boxed` field to a `Box<dyn Reflect>` containing a different type.
// This is one thing it might try, but I don't think there's a way to make this work because
// any mutation will be forward to the underlying type, which of course can't change its own type.
s.field_mut("boxed").unwrap().apply(&replacement); |
I'd love to have tests for all of these nasty edge cases ;) |
What about |
I think we can do a similar thing here as well. It might be slightly trickier due to getting mutable access to At worst, we just keep the existing impl for |
Yeah this PR is definitely more involved. I don't think it's too bad to maintain since most of the logic is just delegating to the inner It should be noted that most limitations, including the one posted by SkiFire, exists on #3400 as well. The only limitation (I think) that they don't face is being able to use I do still think that this is the safer option (and I really like that I can reflect reflection-compatible trait objects), but the user ergonomics are probably the most compelling reason to go with #3400.
Yeah I was doing some experiments on being able to store and compare the
Fair enough. Any suggestions for making them public? Is the naming clear? Should we update the docs in any meaningful way? |
Oof didn't think about that. Great point! Unfortunately, this is likely an issue with both this PR and #3400. We would have to either not delegate certain methods or we would have to add a new way of changing a value (which would return an I think we can modify the methods for |
Yeah I like this. It allows It's still not perfect since dynamic types (such as those you get when deserializing), will always fail. But we can look into solutions for this as that's just an inherent limitation of working with proxies.
I almost forgot about this! I'll try looking into it, because it would vastly improve the ergonomics. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, thanks to both @MrGVSV and @soqb for addressing my concerns :) If the limitations aren't any worse, then I'm fine to defer to you about maintenance burden. ToReflect
/ ToPartialReflect
are both good names, and the docs are adequate. A doc test or two would be quite helpful in showing off how they're used.
As for end user ergonomics / complexity, can we get a relatively simple top-level example for how to reflect a component that has a Box<dyn Reflect>
? Your PR example was nice, and it would be good to have a starting point for the workflow.
Starting experimenting with this. I'm not sure it's possible when using So unless we remove the generic impl (preventing custom trait object support), then it appears we aren't able to achieve this. Feel free to correct me if I'm wrong, though! |
Since the example I gave in #14776 (comment) may seem a bit weird/artificial, here's another one that might seem more reasonable: #[derive(Reflect)]
#[reflect(from_reflect = false)]
struct Foo {
#[reflect(remote = crate::boxed::ReflectBox<dyn Reflect>)]
boxed: Box<dyn Reflect>,
}
let mut foo1 = Foo {
boxed: Box::new(123u8),
};
let foo2 = Foo {
boxed: Box::new("foo"),
};
foo1.apply(&foo2); Given
To be fair though I think #3400 has the same issue, so there isn't a better approach (yet). |
I actually think #3400 might be okay because it currently only supports
|
So I also looked into adding this. The only one we can potentially support right now is Attempting to do so for And we can't call Like I mentioned on Discord, we might be able to get around this by making |
41d253d
to
5affe80
Compare
Moved these to a dedicated module and updated the docs to include an example of how to implement it on custom trait objects.
You mean an example in the repo? Yeah I can try to put one together 😄 |
could easily become:
NB: i'm not sure if |
No longer needed due to fields being handled directly, and value types always serializing as a full type map
This reverts commit 3b324ad.
I think the problem is that
The real issue is 2, since there is no way to go from |
I may have a solution for this actually 🤔 Update: I mentioned my idea on Discord, but I'm having a lot of concern over the complexity of the solution—both in terms of maintainability and user understandability. We may have to look into solving the |
Also moved these traits to a new `cast` module
5affe80
to
dcef362
Compare
The generated |
FYI I just put up a new PR that uses a different approach that I think is a bit cleaner (at least from an end-user perspective) and will be easier to extend: #15532 If we decide we prefer this PR better, I'll rebase and we can continue forward with this one. |
I prefer that PR too, so I'm closing this for now. |
Objective
Closes #3392
Closes #3400
Closes #6098
Closes #9929
There are plenty of times where we want to reflect a type that itself contains reflected data:
Unfortunately,
Box<dyn Reflect>
, does not implementReflect
, making this impossible.#3400 does a great job at implementing
Reflect
forBox<dyn Reflect>
, but it has one potential issue: double-boxing.Double-boxing occurs when we erase the type of
Box<T>
and put it inside anotherBox
. WhenBox<T>
implements the same bounds required byT
, this becomes very easy to do accidentally.For instance, suppose
Box<dyn Reflect>
did implementReflect
. That would make this possible:And while we can get back to a single box through each
Box
recursively calling a method likeReflect::into_reflect
(i.e.Box<Box<Box<i32>>>
delegates toBox<Box<i32>>
which delegates toBox<i32>
which delegates toi32
which results inBox<i32>
), this is not very efficient—both in terms of performance and memory.This PR aims to bring an alternative approach to the table, based on the newly merged remote reflection feature (#6042). The original idea for this PR comes from @soqb.
Solution
Introduce a new
ReflectBox<T>
wrapper type that can be used to remotely reflectBox<T>
.ReflectBox<T>
cannot be publicly constructed or downcast to. It only exists hidden as the underlying machinery for reflectingBox<T>
. This makes it very difficult to accidentally double-box it. In fact, I'm 99% sure you'll never accidentally have that happen (there are ways to force this if you're feeling rebellious though).The way it works is by defining a couple new traits (both of which are hidden using
#[doc(hidden)]
):ToPartialReflect
andToReflect
. These traits have a blanket impl defined for all typesT: PartialReflect
andT: Reflect
, respectively.However, they also have custom impls for the corresponding trait objects:
dyn PartialReflect
anddyn Reflect
.And because
ReflectBox<T>
requiresT: ToPartialReflect
, it works for all three:Box<T>
,Box<dyn PartialReflect>
, andBox<dyn Reflect>
.I chose to make these traits public to allow for other trait objects to join in the fun! For example, we could refactor our
Player
struct to look like this:Caveats
FromReflect
Note
This limitation is also an issue for #3400.
You might have noticed the
#[reflect(from_reflect = false)]
attributes.We have to opt out of
FromReflect
becausedyn PartialReflect
anddyn Reflect
are not aware ofFromReflect
(i.e. they are not subtraits ofFromReflect
). This means we can't delegate to theFromReflect
impl, even if it does exist for the underlying type.Enums
Note
This limitation is also an issue for #3400.
Unfortunately, due to the lack of
FromReflect
, this approach can't be used in enums.This is due to enums internally relying on
FromReflect
within theirPartialReflect::try_apply
method body, which callsFromReflect
when it needs to replace the current variant with a new one.Usage as a Type Parameter
Note
This limitation only applies to #3400 where the type parameter has a
FromReflect
bound (or any other bound thatBox<dyn Reflect>
can't satisfy). For example, you also couldn't define aVec<Box<dyn Reflect>>
using #3400.Since this approach requires the use of remote reflection, it may not be possible to handle types where
Box
is being used as a type parameter. For example,Vec<Box<dyn Reflect>>
andOption<Box<dyn Reflect>>
are not possible, even with this PR in place.In some cases, it may be possible to remedy this with additional remote reflection. However, it's tricky to appease the compiler without monomorphizing the type down to a dedicated type (i.e. instead of supporting
MyType<Box<dyn Reflect>>
, having a separateMyTypeReflectBox
which removes the generic and assumesBox<dyn Reflect>
in its place).Future PRs may explore adding additional types similar to
ReflectBox
for common types likeVec
andOption
, but these will also be limited in the same wayReflectBox
is now.Serialization
While the current serialization logic isn't the most efficient (we have to do some string comparisons to determine if the type is a
ReflectBox
or not), we are able to serializeReflectBox
just like any other type.Currently, it will serialize concrete
Box<T>
instances as normal andBox<dyn Trait>
as a new type map.For example, if we serialized the following type:
We'd get the following RON:
Open Questions
Are the limitations of this PR worth the avoidance of double-boxing? Or should we continue with Implement Reflect for Box<dyn Reflect> #3400 instead?
Please note that we may still choose to go with this more conservative approach while we debate the true risks of Implement Reflect for Box<dyn Reflect> #3400. We can do this because most usages of
Box
under this PR will be fully compatible with Implement Reflect for Box<dyn Reflect> #3400—the only exception would be customBox<dyn SomeOtherTrait>
usages, which will need their own solution (maybe a macro?). Of the compatible cases, the only migration needed will be to remove the#[reflect(remote = ReflectBox<...>)]
field attributes wherever they are used.Are there better ways to handle serialization other than fetching the type path and comparing the prefix? We already do this for
Option
types, but that's at least only performed in specific scenarios.One thing we could do is add a dedicated method to
PartialReflect
calledis_pointer
oris_indirect
, which we can then call directly to determine whether we're aReflectBox
or not.Alternatively, we could change the
PartialReflect::is_dynamic
method onReflectBox
to returntrue
instead of delegating to the inner type. I don't really like this option because it would be the only method where we do this and it would almost certainly break places that rely onis_dynamic
for their control flow.One last option could be to change the signature of the largely unused
PartialReflect::serializable
method to take in a reference to theTypeRegistry
. Then we could haveReflectBox
return aReflectSerializer
containing the inner data. I think this would be my preferred approach considering it doesn't really change the behavior in an unexpected way (unlike modifyingis_dynamic
). It would just technically be a breaking change.Should the
ToPartialReflect
andToReflect
traits be#[doc(hidden)]
? I did this to prevent them from coming up during autocomplete and so that users don't accidentally misuse them (although, I don't think you can really "misuse" them). But since it's really nice for users to be able to get this feature to work with their own trait objects, should we consider making it more public?Testing
You can test the changes 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 using the new
bevy_reflect::boxed::ReflectBox
type and remote reflection: