Skip to content

Introduce ReflectConvert, a generic reflection mechanism for type conversions, intended for asset BSN.#23742

Merged
alice-i-cecile merged 12 commits into
bevyengine:mainfrom
pcwalton:reflect-convert
Apr 13, 2026
Merged

Introduce ReflectConvert, a generic reflection mechanism for type conversions, intended for asset BSN.#23742
alice-i-cecile merged 12 commits into
bevyengine:mainfrom
pcwalton:reflect-convert

Conversation

@pcwalton
Copy link
Copy Markdown
Contributor

@pcwalton pcwalton commented Apr 9, 2026

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 HandleTemplates to be automatically created from string literals. We need this feature for BSN like this to be valid:

SceneRoot("models/FlightHelmet/FlightHelmet.gltf#Scene0")

And for this to be valid:

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:

App::new()
    .register_type::<i32>()
    .register_type::<String>()
    .register_type_conversion::<i32, String>(|n| Ok(n.into()));

And you invoke the conversion like this:

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 #23576). The ReflectConvert trait as implemented in this patch successfully enables HandleTemplates to be created from strings, without hard-coding HandleTemplate anywhere in the patch generation logic.

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.
@pcwalton pcwalton added the A-Reflection Runtime information about types label Apr 9, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Reflection Apr 9, 2026
@pcwalton pcwalton added C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 9, 2026
Comment thread crates/bevy_app/src/app.rs Outdated
///
/// 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: I meant provide both: the into is just a convenience for calling the more general TryInto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added register_into_type_conversion as requested.

Comment thread crates/bevy_reflect/src/convert.rs Outdated
/// 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>)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, makes sense. I made that change.

Comment thread crates/bevy_reflect/src/convert.rs
@pcwalton pcwalton requested a review from SkiFire13 April 12, 2026 19:36
Comment thread crates/bevy_reflect/src/convert.rs Outdated
Comment on lines +142 to +145
match (self.function)(input.take()?) {
Ok(value) => Ok(Box::new(value)),
Err(value) => Err(Box::new(value)),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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)
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@pcwalton pcwalton requested a review from SkiFire13 April 13, 2026 19:37
Copy link
Copy Markdown
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need this, and the feature is about as straightforward as it gets.

Comment thread crates/bevy_reflect/src/type_registry.rs Outdated
@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 13, 2026
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Apr 13, 2026
Merged via the queue into bevyengine:main with commit 02aef14 Apr 13, 2026
40 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in Reflection Apr 13, 2026
mate-h pushed a commit to mate-h/bevy that referenced this pull request Apr 14, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Reflection Runtime information about types C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants