Introduce ReflectConvert, a generic reflection mechanism for type conversions, intended for asset BSN.#23742
Conversation
conversions, intended for asset BSN.
One of the important features of BSN is that supplying a value of type T
where a value of type U is expected is allowed if a `From` conversion
exists from T to U. This is what allows `HandleTemplate`s to be
automatically created from string literals. We need this feature for BSN
like this to be valid:
```rust
SceneRoot("models/FlightHelmet/FlightHelmet.gltf#Scene0")
```
And for this to be valid:
```rust
EnvironmentMapLight {
diffuse_map: "environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2",
specular_map: "environment_maps/pisa_specular_rgb9e5_zstd.ktx2",
intensity: 250.0,
}
```
Unfortunately, the `From` trait isn't reflectable, and this is a problem
as reflection drives asset BSN. It's not immediately clear how to make
`From` reflectable either, as a syntax like `#[reflect(From)]` wouldn't
work as it's a generic trait.
Instead of making `From` directly reflectable, this patch introduces
`ReflectConvert`, a new `TypeData` that encodes a generic way to convert
a value of type T to type U. You register a type conversion like this:
```rust
App::new()
.register_type::<i32>()
.register_type::<String>()
.register_type_conversion::<i32, String>(|n| Ok(n.into()));
```
And you invoke the conversion like this:
```rust
let reflect_convert = registry
.get_type_data::<ReflectConvert>(TypeId::of::<String>())
.unwrap();
let converted = reflect_convert
.try_convert_from(Box::new(12345i32))
.unwrap()
.downcast::<String>()
.unwrap();
```
I tested `ReflectConvert` with asset BSN (PR bevyengine#23576). The
`ReflectConvert` trait as implemented in this patch successfully enables
`HandleTemplate`s to be created from strings, without hard-coding
`HandleTemplate` anywhere in the patch generation logic.
| /// | ||
| /// See [`bevy_reflect::TypeRegistry::register_type_conversion`]. | ||
| #[cfg(feature = "bevy_reflect")] | ||
| pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>) -> &mut Self |
There was a problem hiding this comment.
I wonder if we should have a register_into_type_conversion that just does the into for you? I suspect most calls to this will just be into.
There was a problem hiding this comment.
Yeah, I was considering that too. In an earlier version you were just able to write .register_type_conversion::<T,U>(Into::into) but I realized that was incompatible with fallible conversions, and I'm pretty sure we do want fallible conversions.
There was a problem hiding this comment.
To clarify: I meant provide both: the into is just a convenience for calling the more general TryInto
There was a problem hiding this comment.
I added register_into_type_conversion as requested.
| /// If the conversion succeeds, the function should return the converted | ||
| /// value. If the conversion fails, the function should return the original | ||
| /// input value. | ||
| pub fn register_type_conversion<T, U>(&mut self, function: fn(T) -> Result<U, T>) |
There was a problem hiding this comment.
Is there a reason this take a fn pointer? It is both more limiting (as it doesn't allow captures) and more expensive (as it always forces the Box below to allocate, while ZST closures can avoid that; it also forces two dynamic dispatch calls instead of just one)
There was a problem hiding this comment.
Because it needs to be Cloneable. I can't write:
struct TypedConverter<T, U>(Box<dyn (FnMut(T) -> Result<U, T>) + Clone>)
where
T: Reflect + TypePath,
U: Reflect + TypePath;Since Clone isn't an auto trait. I could write trait Convert<T, U>: FnMut(T) -> Result<U,T> + Clone but then the closure you pass in wouldn't implement Convert, defeating the ergonomic advantages of taking a function in register_type_conversion.
What's the pattern you're thinking of specifically?
There was a problem hiding this comment.
TypedConverter could store a generic F though, no? You end up type erasing it anyway to a Box<dyn Converter> so there's no need to do another type erasure here.
There was a problem hiding this comment.
Ah yes, makes sense. I made that change.
| match (self.function)(input.take()?) { | ||
| Ok(value) => Ok(Box::new(value)), | ||
| Err(value) => Err(Box::new(value)), | ||
| } |
There was a problem hiding this comment.
Nit: I wonder if we could use Reflect::downcast instead of Reflect::take to get a Box<T> which we can reuse in the Err path to avoid allocating again. This would make a series of conversions that all fail much cheaper. Something like this:
| match (self.function)(input.take()?) { | |
| Ok(value) => Ok(Box::new(value)), | |
| Err(value) => Err(Box::new(value)), | |
| } | |
| let mut input = input.downcast::<T>()?; | |
| match (self.function)(*input) { | |
| Ok(value) => Ok(Box::new(value)), | |
| Err(value) => { | |
| *input = value; | |
| Err(input) | |
| } | |
| } |
cart
left a comment
There was a problem hiding this comment.
We need this, and the feature is about as straightforward as it gets.
appropriate places
…onversions, intended for asset BSN. (bevyengine#23742) One of the important features of BSN is that supplying a value of type T where a value of type U is expected is allowed if a `From` conversion exists from T to U. This is what allows `HandleTemplate`s to be automatically created from string literals. We need this feature for BSN like this to be valid: ```rust SceneRoot("models/FlightHelmet/FlightHelmet.gltf#Scene0") ``` And for this to be valid: ```rust EnvironmentMapLight { diffuse_map: "environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2", specular_map: "environment_maps/pisa_specular_rgb9e5_zstd.ktx2", intensity: 250.0, } ``` Unfortunately, the `From` trait isn't reflectable, and this is a problem as reflection drives asset BSN. It's not immediately clear how to make `From` reflectable either, as a syntax like `#[reflect(From)]` wouldn't work as it's a generic trait. Instead of making `From` directly reflectable, this patch introduces `ReflectConvert`, a new `TypeData` that encodes a generic way to convert a value of type T to type U. You register a type conversion like this: ```rust App::new() .register_type::<i32>() .register_type::<String>() .register_type_conversion::<i32, String>(|n| Ok(n.into())); ``` And you invoke the conversion like this: ```rust let reflect_convert = registry .get_type_data::<ReflectConvert>(TypeId::of::<String>()) .unwrap(); let converted = reflect_convert .try_convert_from(Box::new(12345i32)) .unwrap() .downcast::<String>() .unwrap(); ``` I tested `ReflectConvert` with asset BSN (PR bevyengine#23576). The `ReflectConvert` trait as implemented in this patch successfully enables `HandleTemplate`s to be created from strings, without hard-coding `HandleTemplate` anywhere in the patch generation logic. --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
One of the important features of BSN is that supplying a value of type T where a value of type U is expected is allowed if a
Fromconversion exists from T to U. This is what allowsHandleTemplates to be automatically created from string literals. We need this feature for BSN like this to be valid:And for this to be valid:
Unfortunately, the
Fromtrait isn't reflectable, and this is a problem as reflection drives asset BSN. It's not immediately clear how to makeFromreflectable either, as a syntax like#[reflect(From)]wouldn't work as it's a generic trait.Instead of making
Fromdirectly reflectable, this patch introducesReflectConvert, a newTypeDatathat encodes a generic way to convert a value of type T to type U. You register a type conversion like this:And you invoke the conversion like this:
I tested
ReflectConvertwith asset BSN (PR #23576). TheReflectConverttrait as implemented in this patch successfully enablesHandleTemplates to be created from strings, without hard-codingHandleTemplateanywhere in the patch generation logic.