-
-
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: Opt-out attribute for TypePath
#9140
Conversation
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 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 |
@@ -128,6 +128,13 @@ pub(crate) static TYPE_NAME_ATTRIBUTE_NAME: &str = "type_name"; | |||
/// | |||
/// Note that in the latter case, `ReflectFromReflect` will no longer be automatically registered. | |||
/// | |||
/// ## `#[reflect(type_path = false)]` |
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 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]
?
@@ -1863,6 +1864,54 @@ bevy_reflect::tests::should_reflect_debug::Test { | |||
let _ = <Recurse<Recurse<()>> as TypePath>::type_path(); | |||
} | |||
|
|||
#[test] | |||
fn can_opt_out_type_path() { |
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.
Maybe add a test for reflect_value
since this is affected by the changes too.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
TypePath
implementation that is automatically generated by theReflect
derive macro, allowing customTypePath
implementations.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
TypePath
and rely on something likestd::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 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 theTypePath
impl when derivingReflect