-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
bevy_reflect: Add ReflectBox to remotely reflect Box
#14776
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
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
ReflectforBox<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
Boxrecursively calling a method likeReflect::into_reflect(i.e.Box<Box<Box<i32>>>delegates toBox<Box<i32>>which delegates toBox<i32>which delegates toi32which 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)]):ToPartialReflectandToReflect. These traits have a blanket impl defined for all typesT: PartialReflectandT: Reflect, respectively.However, they also have custom impls for the corresponding trait objects:
dyn PartialReflectanddyn 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
Playerstruct to look like this:Caveats
FromReflectNote
This limitation is also an issue for #3400.
You might have noticed the
#[reflect(from_reflect = false)]attributes.We have to opt out of
FromReflectbecausedyn PartialReflectanddyn Reflectare not aware ofFromReflect(i.e. they are not subtraits ofFromReflect). This means we can't delegate to theFromReflectimpl, 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
FromReflectwithin theirPartialReflect::try_applymethod body, which callsFromReflectwhen 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
FromReflectbound (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
Boxis 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 separateMyTypeReflectBoxwhich removes the generic and assumesBox<dyn Reflect>in its place).Future PRs may explore adding additional types similar to
ReflectBoxfor common types likeVecandOption, but these will also be limited in the same wayReflectBoxis 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
ReflectBoxor not), we are able to serializeReflectBoxjust 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
Boxunder 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
Optiontypes, but that's at least only performed in specific scenarios.One thing we could do is add a dedicated method to
PartialReflectcalledis_pointeroris_indirect, which we can then call directly to determine whether we're aReflectBoxor not.Alternatively, we could change the
PartialReflect::is_dynamicmethod onReflectBoxto returntrueinstead 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_dynamicfor their control flow.One last option could be to change the signature of the largely unused
PartialReflect::serializablemethod to take in a reference to theTypeRegistry. Then we could haveReflectBoxreturn aReflectSerializercontaining 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
ToPartialReflectandToReflecttraits 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::ReflectBoxtype and remote reflection: