-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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: Trait casting #5001
Conversation
Improved safety removing ability to specify arbitrary types in ErasedNonNull constructor. Added docs and tests.
…f a type implements it, without requiring it at compile time
@@ -249,6 +366,28 @@ impl TypeRegistration { | |||
self.type_info.type_id() | |||
} | |||
|
|||
#[doc(hidden)] |
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.
I don't think this should be hidden.
self.trait_casts.insert(TypeId::of::<T>(), f); | ||
} | ||
|
||
pub fn has_trait_cast<T: ?Sized + 'static>(&self) -> bool { |
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.
IMO can_trait_cast
is clearer and more natural.
self.trait_casts.insert(TypeId::of::<T>(), f); | ||
} | ||
|
||
pub fn has_trait_cast<T: ?Sized + 'static>(&self) -> bool { |
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.
Needs doc strings.
self.trait_casts.contains_key(&TypeId::of::<T>()) | ||
} | ||
|
||
pub fn trait_cast<'a, T: ?Sized + 'static>(&self, val: &'a dyn Reflect) -> Option<&'a T> { |
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.
Doc strings, including a doc test to demonstrate usage.
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.
Finally giving this the attention it deserves. My current thoughts:
- The trait casting improvements are welcome and great ergonomics.
- Something like the register_all! macro is essential to reduce boilerplate, but it's syntax feels alien to me and it's hard to tell what's going on. Generating a named function that you must call definitely feels wrong. Can we do something method-like on type registeries? Can we pass in a type registry to the macro?
- Why have all the
Serialize
reflect_value calls been removed but not the others?
I'm also not a fan of |
Many traits are not implemented for a struct in the same crate as the struct is declared. |
Regarding 2, I suppose we could require the user to make their own function, and then they invoke the macro inside of it to expand the code. |
Yeah that's totally fair. What if it looked more like: let mut app = App::new();
register_all! {
app,
traits: [SomeTrait],
types: [Foo, Bar]
};
// ... Where we instead pass the
The reason is that, with the current serialization system, only
I agree. I think |
I like this much better! I think it should be the registry though.
This syntax makes sense to me. We'll need good docs to support it though! |
if IsFromTypeImplemented::<$type_data_ty>::FROM_TYPE_FN.is_some() { | ||
let v = IsFromTypeImplemented::<$type_data_ty>::FROM_TYPE_FN.unwrap()(); | ||
Some(v) | ||
} else { | ||
None | ||
} |
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.
if IsFromTypeImplemented::<$type_data_ty>::FROM_TYPE_FN.is_some() { | |
let v = IsFromTypeImplemented::<$type_data_ty>::FROM_TYPE_FN.unwrap()(); | |
Some(v) | |
} else { | |
None | |
} | |
IsFromTypeImplemented::<$type_data_ty>::FROM_TYPE_FN.map(|from_type| from_type()) |
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.
Copying a comment from discord:
How many traits is trait casting enough for? List of requirements is 1. we are only in interested in &self and &mut self methods (so no static methods like default, no associated consts) 2. the trait is object safe and 3. the trait has no generics where you could only register specific monomorphizations.
Looking at some of the traits we reflect in bevy:
Can work with trait casting:
Debug
,SystemLabel
,Iterator
(kind of)
Doesn't really work because the trait has generics so you can only hardcoded specific onesPartialEq
,PartialOrd
Can't work with trait casting because it is not object safe:Hash
,Clone
Can't work with trait casting but type data can add useful fn pointers:Resource
,Component
,Bundle
,Default
Can't work with trait casting because the relevant info is on associated constTypeUuid
Can't work with trait casting but could be useful with type data as a simple markerEq
,Ord
,Copy
,Pod
, (Send
,Sync
?)
Maybe list would be more in favour of trait casting in application code, but here it looks like for most traits we wouldn't even want to use it.
So the question is if we want
#[derive(Reflect, PartialEq, Hash, Debug, Clone)]
#[reflect(PartialEq, Hash, dyn Debug, Clone)]
(or the equivalent with the register_all
macro) if that means having to explain why Debug
can use trait casting but the rest needs type data.
We can of course add ReflectDebug
type data so that the list is consistent for std traits and leave trait casting just for people who know that their own traits are useful with just casting.
@@ -249,6 +366,28 @@ impl TypeRegistration { | |||
self.type_info.type_id() | |||
} | |||
|
|||
#[doc(hidden)] | |||
pub fn register_trait_cast<T: ?Sized + 'static>( |
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.
This needs to be unsafe
and should document what f
should be passed, even if it is doc(hidden)
Closing this out as stale. I also don't know if it's the direction we want to go. |
Objective
Trait Casting
Currently the best way of converting a reflected type to a trait object is to go through a multi-step process wherein a user must:
#[reflect_trait]
ReflectSomeTrait
#[reflect(SomeTrait)]
ReflectSomeTrait
from the registryReflectSomeTrait
to cast your reflected value to&dyn SomeTrait
Example Code
This is both complicated and annoying to code out, with lots of places to go wrong. Ideally there would be a way for casting to Just Work™.
Solution
Trait Casting
Added the following methods to
TypeRegistration
:has_trait_cast<T: ?Sized + 'static>(&self) -> bool
Returns whether or not a given value can be cast to a given trait object.
trait_cast<'a, T: ?Sized + 'static>(&self, val: &'a dyn Reflect) -> Option<&'a T>
Attempts to cast a given value to a given trait object.
Together, these methods can turn the previous code:
into the following:
This is a small improvement, but it's not where the magic of this PR happens. That takes place during type registration.
Type Registration
The biggest difficulty with handling traits using Bevy's reflection system is that each trait has to be registered per implementor— at the implementor's definition. This can be difficult to maintain as implementors need to import the generated
ReflectMyTrait
structs.To get around this issue, this PR adds the
register_all
macro, which allows multiple traits and types to be registered at once, and let the compiler figure out which types can register which trait.The
register_all
registers a trait cast for each trait that a given type implements. It then generates a public function calledregister_types
which can be used to mass register all types.Type Data
While the focus of this PR is trait casting, we can't forget about traits which cannot be cast into
dyn MyTrait
objects. Those traits are often represented as type data, which can be inserted into a type's registration. Since this PR addedregister_all
for traits, it made sense to also give the ability to register type data as well.Benefits
Aside from just being more compact, the macro automatically allows usage of the new
trait_cast
methods, making it much easier to cast types to their respective traits. It also makes using managing reflected traits a lot simpler (users don't need to worry about importing/exporting the properReflectMyTrait
structs).Additionally, it makes it much easier to reflect third-party traits. Instead of manually creating the struct (or using a macro to do it for you), you can simply add it to the list of traits in the
register_all!
macro (same goes for third-party structs as well).Considerations
One downside of this is that it ends up generating a fair amount of code (~40 lines per type per trait) as each type needs to check if it implements a given trait. This means that the total number of generated "blocks" is equal to the number of types times the number of traits.
However, it might be worth it for widely used traits or when used more granularly.
Sample Output
Example
As an example of how this works,
bevy_reflect::impls
now exports aregistrations
module:Because we register all these types with a
Serialize
trait cast, we can use it in ourReflectSerializer
as easily as: