stable reflect type name
Objective
Drop the usage of std::any::type_name to introduce a stable type name for reflected types.
Based on https://github.com/bevyengine/bevy/issues/5745#issuecomment-1221389131
closes #3327
Solution
- Add a
TypePathtrait that can be derived with associated methods to get the type's path. -
Reflect::type_path(formelytype_name) relied onTypePath::type_path
When TypePath is automatically derived, the base name is by default path::to::mod::MyType where path::to::mod is the result of module_path! and MyType the ident of the type.
If the type is generic then the base name is followed by the type name of its generic types.
e.g. path::to::mod::MyType<'a, u32, glam::Vec3, 4>
mod a {
mod b {
mod c {
#[derive(TypePath)]
struct ABC;
}
#[derive(TypePath)]
#[type_path(ident = "Foo")]
struct AB<T>;
}
#[derive(TypePath)]
struct A<T>;
}
| Type | TypeName::name() |
|---|---|
ABC |
a::b::c::ABC |
AB<ABC> |
a::b::Foo<a::b::c::ABC> |
A<ABC> |
a::A<a::b::c::ABC> |
A<AB<ABC>> |
a::A<a::b::Foo<a::b::c::ABC>> |
Open questions
- [x] How to handle the last
std::any::type_names ? (see https://github.com/bevyengine/bevy/pull/5805#issuecomment-1229188350) - [x] Whether or not keep the
constbefore const generic parameters in type name ? e.g.MyType<42>vsMyType<const 42>. (see https://github.com/bevyengine/bevy/pull/5805#discussion_r956649449) - [x] What convention for std types' type name ?
- Only the ident (
String) - Full module path (
alloc::string::String) - Library name prefix (
std::String)
- Only the ident (
- [ ] How to handle type path for tuples ? (see https://github.com/bevyengine/bevy/pull/5805#issuecomment-1237044093)
- [ ] Should custom type path and type name should be checked ? (see https://github.com/bevyengine/bevy/pull/5805#issuecomment-1237044093)
Changelog
- Added
TypePathtrait automatically implemented by#[derive(Reflect)] - Added
TypePathas super trait ofAsset(because ofHandle<T>)
Migration Guide
- Custom assets require the
TypePathtrait
- #[derive(TypeUuid)]
+ #[derive(TypeUuid, TypePath)]
#[uuid = "00000000-0000-0000-0000-000000000000"]
struct MyAsset {
// ...
}
- Custom material require the
TypePathtrait
- #[derive(AsBindGroup, TypeUuid, Debug, Clone)]
+ #[derive(AsBindGroup, TypeUuid, TypePath, Debug, Clone)]
#[uuid = "00000000-0000-0000-0000-000000000000"]
struct CustomMaterial {
// ...
}
impl Material for CustomMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/custom_material.wgsl".into()
}
}
- derive
Reflecton generic type require the generic parameters to implementTypePath
struct MyType<T> {
value: T
}
- impl<T: Reflect> Reflect for MyType<T> {
+ impl<T: Reflect + TypePath> Reflect for MyType<T> {
// ...
}
-
Inputrequire theTypePathtrait.
- #[derive(Copy, Clone, Eq, PartialEq, Hash)]
+ #[derive(Copy, Clone, Eq, PartialEq, Hash, TypePath)]
enum DummyInput {
Input1,
Input2,
}
fn system(input: Res<Input<DummyInput>>) {
/* ... */
}
Additional context
Why not an associated const ?
It's not possible to concat string literal and const value in const context.
There is the const_format crate but it's not working with generic (see limitations).
impl<T: TypeName> TypeName for Vec<T> {
const NAME: &'static str = concatcp!("Vec", "<", T::NAME, ">"); // <-- won't compile
}
Current state of replacing std::any::type_name
In the following cases std::any::type_name has not been replaced for different reason.
Feedback on certain points will be appreciated ^^'
ValueInfo [SOLVED]
Cause
On this impl on &dyn Reflect there no access to TypeName because it's not object safe.
https://github.com/tguichaoua/bevy/blob/6fba583d809da89da21c0e2d2f4834a448dd8204/crates/bevy_reflect/src/reflect.rs#L207-L213
Solutions
- Keep
std::any::type_namefor this case : the type_name may differ fromTypeName. - Remove this impl, but I don't know what are the side effects.
TypeRegistry::register_type_data [SOLVED]
Cause
The type names are used only for a panic message.
I don't think it worst it to add a TypeName restriction here.
https://github.com/tguichaoua/bevy/blob/6fba583d809da89da21c0e2d2f4834a448dd8204/crates/bevy_reflect/src/type_registry.rs#L123-L132
Solutions
- Leave as it.
- Add a
TypeNamerestriction.
TypeUuidDynamic::type_name [SOLVED]
Cause
I don't kwow if TypeUuidDynamic::type_name must match TypeName.
Solutions
- Leave as it, if there is no link between
TypeUuidDynamic::type_nameandTypeName. - Add
TypeNamerestriction on typeT.
I think one solution for the issues you're coming across is to do what Typed and GetTypeRegistration do: create an implied bound.
For example, Typed is an implied requirement of Reflect due to Reflect::get_type_info. And it is added as a generic bound on places like TypeRegistration::of. This implied requirement is taken care of by the derive for Reflect so it works out totally fine.
Others might disagree, but I think we can do the same to TypeName. We can get rid of ReflectTypeName (we don't need object safety and removing it should reduce confusion). Instead, since we automatically derive it in #[derive(Reflect)], we can just use that impl in Reflect::type_name. And make a note in the documentation for manual implementors that this trait needs to be implemented as well.
I added a "Open questions" section in the PR description for points that need feedback.
How to handle the last
std::any::type_names ? (see stable reflect type name #5805 (comment))
I think for TypeRegistry::register_type_data, it makes sense to just include it as a bound. If we can get #5772 to land, then this should all get cleaned up pretty easily. For TypeUuidDynamic::type_name, I would use TypeName. This might be fixed by simply adding it as a bound to T in the blanket impl.
Whether or not keep the
constbefore const generic parameters in type name ? e.g.MyType<42>vsMyType<const 42>. (see stable reflect type name #5805 (comment))
I know in one of my comments I said we should keep it, but I changed my mind haha. I don't think it adds anything really. So I vote to exclude it.
What convention for std types' type name ?
- Only the ident (
String)- Full module path (
alloc::string::String)- Library name prefix (
std::String)
I think it depends on the type. For things like String and Option, that are core to Rust and are unlikely to be user-generated, then we should go with the first option. However, it gets complicated for things like Duration which could be ambiguous with a user-defined type.
I'd say go with the first option for things that are part of the Rust prelude. Go with the third option for things that need to be imported.
All done ! Thank you for all your help @MrGVSV.
I was thinking about adding some shortcut when derive TypeName to use the lib name as prefix instead of the mod path or only the ident, see below.
But to get the lib name we require a new dependency to const_str to compute the name at compile time.
const LIB_NAME: &'static str = const_str::split!(module_path!(), "::")[0];
Is that ok to add a new dependency for this ?
// my_lib::mod_a::mod_b::MyType
#[derive(TypeName)]
struct MyType;
// MyCustomName
#[derive(TypeName)]
#[type_name("MyCustomName")]
struct MyType;
// my_lib::MyType
#[derive(TypeName)]
#[type_name(lib)]
struct MyType;
// MyType
#[derive(TypeName)]
#[type_name(ident)]
struct MyType;
Pending questions
How to implement TypePath for tuples ?
For type_path it's quite straight forward, but how to implement the other methods ?
Currently implemented like this :
fn short_type_name_base() -> &'static str {
Self::type_path()
}
fn short_type_name() -> &'static str {
Self::type_path()
}
fn module_path() -> &'static str {
""
}
fn crate_name() -> &'static str {
""
}
Should custom path and ident be checked ?
If the user use custom path or ident with the derive macro, do we have to check those values are correct ?
#[derive(TypePath)]
#[type_path(path = "not well formated path *-*-*", ident = "Blah@ blah")
struct Foo;
I tried to mark this PR as closing #3327, but it didn't seem to take. Could you added a Closes #3327 comment to the PR description?
we could allow associated constant type names with a fixed length array as storage which would panic on overflow instead of using const_format e.g.
trait TypeName {
const NAME: &'static str;
}
impl TypeName for u8 {
const NAME: &'static str = "u8";
}
impl<T: TypeName> TypeName for Vec<T> {
const NAME: &'static str = ConstString::<1024>::from_str("Vec<")
.with_str(T::NAME)
.with_str(">")
.as_str();
}
assert_eq!(Vec::<u8>::NAME, "Vec<u8>");
but const-panic isn't working very well.
are const typenames helpful at all? and would limiting their maximum length be particularly hindering?
Since there is a lot of conflict with the main branch I think the best to do is not solve them, keep this PR open to solve the pending questions and then open a fresh PR to implement the feature.
to solve the tuple issue, what about seperate TypeName and TypePath: TypeName traits where tuples implement TypeName but not TypePath e.g.
impl<T: TypeName, U: TypeName> TypeName for (T, U) {
fn type_name() -> &'static str {
// e.g. "(my_crate::Foo, my_crate::Bar)"
let name = format!("({}, {})", T::type_name(), U::type_name());
Box::leak(Box::new(name)) // but better
}
fn short_type_name() -> &'static str {
// e.g. "(Foo, Bar)"
let name = format!("({}, {})", T::short_type_name(), U::short_type_name());
Box::leak(Box::new(name))
}
}
struct StableName<T, U> {
a: T,
b: (T, U),
}
// generated by derive macro
const PATH: &'static str = module_path!();
const IDENT: &'static str = "StableName";
impl<T: TypeName, U: TypeName> TypeName for StableName<T, U> {
fn type_name() -> &'static str {
<Self as TypePath>::type_path()
}
fn short_type_name() -> &'static str {
<Self as TypePath>::short_type_path()
}
}
// NB: not `T: TypePath`.
impl<T: TypeName, U: TypeName> TypePath for StableName<T, U> {
fn type_path() -> &'static str {
// e.g. "my_crate::StableName::<my_crate::Foo, my_crate::Bar>"
let path = format!("{}::{}::<{}, {}>", PATH, IDENT, T::type_name(), U::type_name());
Box::leak(Box::new(path))
}
fn short_type_path() -> &'static str {
// e.g. "StableName::<Foo, Bar>"
let path = format!("{}::<{}, {}>", IDENT, T::short_type_name(), U::short_type_name());
Box::leak(Box::new(path))
}
// etc.
}
or something
Sorry for taking so long to respond to this. I think this effort is important and worth driving forward.
How to implement TypePath for tuples ?
This will also be a problem for reflected array types, which also don't have a crate or module in their type name.
In addition to @soqb's idea to separate the traits (ill call that option 1) I think we have a few more options:
Option 2: Allow empty crates, but only for built in types like arrays and tuples. Specify this rule in the trait docs and enforce it in the derive macro (as any derived impl is not-built-in by definition).
Option 3: Arbitrarily define a crate for these types (ex: builtin::(usize, usize))
I personally like Option 2 the most. Keeps our traits simple, seems workable generally, and aligns pretty well with std type names. Slightly more complicated rules / constraints, but we do get to make the rules and I can't think of any negative implications.
Should custom path and ident be checked ?
I think yes. Better to ensure data integrity at the definition level.
should the methods on the TypePath (possibly TypeName) trait(s) take in &self so that the Dynamic* reflect types work as intended from a reflection standpoint, or is this out-of-scope for this pr/not desirable behavior?
should the methods on the
TypePath(possiblyTypeName) trait(s) take in&selfso that theDynamic*reflect types work as intended from a reflection standpoint, or is this out-of-scope for this pr/not desirable behavior?
Imo probably not. We want to be able to access these methods without needing an actual instance of that field (i.e. when registering the type itself).
If we want access to this, we could probably do one of the following:
- Add corresponding methods to the
Reflecttrait. Since this is theoretically a "core" reflection trait, we should be okay to add such methods. This would be similar to what we do for theTypedtrait usingReflect::get_type_info. - Similar to 1, but with a custom struct. If we don't want to add a new method to
Reflectfor each one inTypePath, then we could combine everything into one method and have it return a struct with all theTypePathdata (probably usingCow<'static, str>for the fields). - Lastly, we could just continue to use
Reflect::type_nameas we do now for the Dynamic types. It would just contain the fullTypePath::type_pathstring value and nothing more (noshort_type_name,module_path, etc.).
Any thoughts on these options? Maybe there are other solutions I'm missing here, but one of these would make sense to me.
Imo probably not. We want to be able to access these methods without needing an actual instance of that field (i.e. when registering the type itself).
Agreed. I think option (1) is pretty good.
I personally like Option 2 the most.
i think there is a fourth option we haven't considered as much as we should.
moving the functionality into a TypePath struct (as @MrGVSV suggested for Reflect) and adding methods returning Options where applicable similar to TypeInfo.
returning Options is a nice compromise between Option 1 & 2/3 since it cuts down on traits but represents the optional state of the path segments clearly.
type A = T;
type B = (T, U);
impl TypePath {
// A: "my_crate::foo::T"
// B: "(my_crate::foo::T, my_crate::foo::bar::U)"
fn type_path(&self) -> &str;
// A: "T"
// B: "(T, U)"
fn short_type_path(&self) -> &str;
// A: Some("T")
// B: None
fn type_name(&self) -> Option<&str>;
// A: Some("my_crate")
// B: None
fn crate_name(&self) -> Option<&str>;
// A: Some("my_crate::foo")
// B: None
fn module_path(&self) -> Option<&str>;
}
// i suppose `TypePath` would have a `Cow<'static, str>` (potentially `Option`) field for each method.
trait Typed {
fn type_info() -> &'static TypeInfo;
fn type_path() -> &'static TypePath;
}
trait Reflect {
fn get_type_info(&self) -> &'static TypeInfo;
fn get_type_path(&self) -> &'static TypePath;
// ...
}
side note @tguichaoua: i would love to adopt/adapt this PR if you're not willing since its relevant to some of my reflection mad science experiments.
side note @tguichaoua: i would love to adopt/adapt this PR if you're not willing since its relevant to some of my reflection mad science experiments.
@soqb np, feel free to adopt this PR 👍
I noticed the current implementation can cause potential name conflicts:
use bevy::reflect::TypePath;
#[derive(TypePath)]
pub struct Foo;
pub fn bar() {
#[derive(TypePath)]
pub struct Foo;
println!("{}", Foo::type_path());
}
fn main() {
println!("{}", Foo::type_path());
bar();
}
Not sure how to resolve this, module_path doesn't include functions in the path (even though std::any::type_name does 🤨).
i wonder if we can do something like this if it becomes an issue. it's very hacks and not guaranteed to work across compiler versions but it might be enough.
Reading the first sentence of the top comment on this PR, I feel like that might be a non-starter?
Drop the usage of std::any::type_name
Making this stable across compiler versions is one of the core goals of this PR, right?
Reading the first sentence of the top comment on this PR, I feel like that might be a non-starter?
effectively, yes - this is the absolute last thing i'd want to do. but if and only if we really, really need this conflict-resolution, due to the crawling pace of the function_name!() and item_path!() macros put forward in a litany of different dead rust issues and prs, this would likely be our only option for some time to come.
Closing this out as superseded by #7184