bevy_reflect: Opt-out attribute for TypePath#9140
Conversation
paul-hansen
left a comment
There was a problem hiding this comment.
I like this solution a lot! Fixes the problem, doesn't require wrappers, still just as easy to use in normal use cases 👍
I made a PR to leafwing input testing this out here: Leafwing-Studios/leafwing-input-manager#366 Didn't have any issues using it.
I couldn't find anything in the code to complain about, all looks great!
|
I gave this a spin, and I guess |
| /// | ||
| /// Note that in the latter case, `ReflectFromReflect` will no longer be automatically registered. | ||
| /// | ||
| /// ## `#[reflect(type_path = false)]` |
There was a problem hiding this comment.
I tested this with #[reflect_value(type_path = false)] and that worked as well. Maybe could add a comment to say this works with reflect_value too, not just #[reflect]?
| } | ||
|
|
||
| #[test] | ||
| fn can_opt_out_type_path() { |
There was a problem hiding this comment.
Maybe add a test for reflect_value since this is affected by the changes too.
soqb
left a comment
There was a problem hiding this comment.
overall this LGTM, this works to resolve one issue but if this is merged with type path part 2 as it is it will be a stability hole (using std::any::type_name).
i wonder if (soon in the future) we could add an associated constant like the following:
trait TypePath {
const TYPE_PATH_IS_STABLE: bool = false;
}which would let us to panic upon (de)serialization.
Yeah. Maybe if we want to be super clear, I could add a warning in the docs to let users know that this is generally recommended against. But yeah, for any type that can't effectively be (de)serialized, I don't think type name stability will be a major issue. |
The issue with that is a library like |
|
i agree that this fix is absolutely necessary for i feel very strongly that we should not allow serialisation on types that do not have stable type path, so we cannot allow serialisation on the type parameter with the offending error in technically, in the specific case of the non-stable type path'd type appearing as a field of a type with a stable type path, it would actually be serialisable! |
Yes, because being able to serialise/deserialise the PRNG is a huge part of being able to store/send/reload a game's state (including its PRNG states) for determinism. Implementing a replay feature that is able to replay a game's 'random' parts, like crits, rolls, etc, requires that you are able to serialise/deserialise the PRNG, and So in summary, for |
|
Ultimately though, if it must be no other way, then |
|
It should be noted that the example in the original post can be achieved in Bevy 0.11.0 already, you just have to manually implement Reflect instead of deriving it. This PR just allows you to disable the automatic implementation of TypePath when you derive Reflect. It doesn't introduce any stability issues that aren't already there. I think this PR could also be helpful for cases where there might be another crate that implements a stable type path and we want to implement TypePath using the methods that crate provides. So I don't really see this PR as solely helping negative use cases. That said, I'm thinking I'll probably not use this for LWIM after all as serializing input maps is a core feature so it would benefit from the stability that TypePath brings for the same reasons that Bevy does and I can't think of a case where a user wouldn't own the type we are asking them to provide. (I'm open to discussion on this though. Feel free to use the LWIM PR) |
There was a problem hiding this comment.
I think enabling opting out here is reasonable, but I also think it seems weird to not capitalize on our ability to make the top level type's path "as stable as possible". I think ideally in these cases the developer only needs to fill in the type path for the generic type. (which could be done by passing in a function to the derive):
#[derive(Reflect)]
#[type_path(T = get_t_type_path())]
struct MyType<T> {}I also suspect that 99% of these cases will want to fall back to type_name. Feels like we could make this easier than "implement TypePath manually".
#[derive(Reflect)]
#[unstable_type_path(T)]
struct MyType<T> {}But this seems ok for 0.11.1
Fixes #9094 Takes a bit from [this](#9094 (comment)) comment as well as a [comment](https://discord.com/channels/691052431525675048/1002362493634629796/1128024873260810271) from @soqb. This allows users to opt-out of the `TypePath` implementation that is automatically generated by the `Reflect` derive macro, allowing custom `TypePath` implementations. ```rust struct Foo<T> { #[reflect(ignore)] _marker: PhantomData<T>, } struct NotTypePath; impl<T: 'static> TypePath for Foo<T> { fn type_path() -> &'static str { std::any::type_name::<Self>() } fn short_type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); CELL.get_or_insert::<Self, _>(|| { bevy_utils::get_short_name(std::any::type_name::<Self>()) }) } fn crate_name() -> Option<&'static str> { Some("my_crate") } fn module_path() -> Option<&'static str> { Some("my_crate::foo") } fn type_ident() -> Option<&'static str> { Some("Foo") } } // Can use `TypePath` let _ = <Foo<NotTypePath> as TypePath>::type_path(); // Can register the type let mut registry = TypeRegistry::default(); registry.register::<Foo<NotTypePath>>(); ``` The stability of type paths mainly come into play during serialization. If a type is moved between builds, an unstable type path may become invalid. Users that opt-out of `TypePath` and rely on something like `std::any::type_name` as in the example above, should be aware that this solution removes the stability guarantees. Deserialization thus expects that type to never move. If it does, then the serialized type paths will need to be updated accordingly. If a user depends on stability, they will need to implement that stability logic manually (probably by looking at the expanded output of a typical `Reflect`/`TypePath` derive). This could be difficult for type parameters that don't/can't implement `TypePath`, and will need to make heavy use of string parsing and manipulation to achieve the same effect (alternatively, they can choose to simply exclude any type parameter that doesn't implement `TypePath`). --- - Added the `#[reflect(type_path = false)]` attribute to opt out of the `TypePath` impl when deriving `Reflect` --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Objective
Fixes #9094
Solution
Takes a bit from this comment as well as a comment from @soqb.
This allows users to opt-out of the
TypePathimplementation that is automatically generated by theReflectderive macro, allowing customTypePathimplementations.Type Path Stability
The stability of type paths mainly come into play during serialization. If a type is moved between builds, an unstable type path may become invalid.
Users that opt-out of
TypePathand rely on something likestd::any::type_nameas in the example above, should be aware that this solution removes the stability guarantees. Deserialization thus expects that type to never move. If it does, then the serialized type paths will need to be updated accordingly.If a user depends on stability, they will need to implement that stability logic manually (probably by looking at the expanded output of a typical
Reflect/TypePathderive). This could be difficult for type parameters that don't/can't implementTypePath, and will need to make heavy use of string parsing and manipulation to achieve the same effect (alternatively, they can choose to simply exclude any type parameter that doesn't implementTypePath).Changelog
#[reflect(type_path = false)]attribute to opt out of theTypePathimpl when derivingReflect