From 3bf75457ce08e69a09edc760dbcfcc72b9fbaa5c Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 8 Apr 2026 20:20:43 -0700 Subject: [PATCH 01/10] Introduce `ReflectConvert`, a generic reflection mechanism for type 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::() .register_type::() .register_type_conversion::(|n| Ok(n.into())); ``` And you invoke the conversion like this: ```rust let reflect_convert = registry .get_type_data::(TypeId::of::()) .unwrap(); let converted = reflect_convert .try_convert_from(Box::new(12345i32)) .unwrap() .downcast::() .unwrap(); ``` I tested `ReflectConvert` with asset BSN (PR #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. --- crates/bevy_app/src/app.rs | 26 +++ crates/bevy_app/src/sub_app.rs | 12 ++ crates/bevy_asset/src/handle.rs | 1 + crates/bevy_asset/src/lib.rs | 1 + crates/bevy_reflect/src/convert.rs | 225 +++++++++++++++++++++++ crates/bevy_reflect/src/lib.rs | 1 + crates/bevy_reflect/src/type_registry.rs | 37 +++- 7 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_reflect/src/convert.rs diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 169c63f057f93..05e939a64655c 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -694,6 +694,32 @@ impl App { self } + /// Registers a fallible conversion from type T to U with the reflection + /// system. + /// + /// The supplied closure is expected to produce a value of type U, given an + /// instance of type T. If the conversion fails, the closure should return + /// the input value, wrapped in an `Err` variant. + /// + /// # Example + /// ``` + /// App::new() + /// .register_type::() + /// .register_type::() + /// .register_type_conversion::(|n| Ok(n.into())); + /// ``` + /// + /// See [`bevy_reflect::TypeRegistry::register_type_conversion`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_type_conversion(&mut self, function: fn(T) -> Result) -> &mut Self + where + T: Reflect + TypePath, + U: Reflect + TypePath, + { + self.main_mut().register_type_conversion(function); + self + } + /// Registers the given function into the [`AppFunctionRegistry`] resource. /// /// The given function will internally be stored as a [`DynamicFunction`] diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 7790fbf9c7683..391372d5fa9e0 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -485,6 +485,18 @@ impl SubApp { self } + /// See [`App::register_type_conversion`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_type_conversion(&mut self, function: fn(T) -> Result) -> &mut Self + where + T: bevy_reflect::Reflect + bevy_reflect::TypePath, + U: bevy_reflect::Reflect + bevy_reflect::TypePath, + { + let registry = self.world.resource_mut::(); + registry.write().register_type_conversion::(function); + self + } + /// See [`App::register_function`]. #[cfg(feature = "reflect_functions")] pub fn register_function(&mut self, function: F) -> &mut Self diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index e908cda11d290..3b28389982aee 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -208,6 +208,7 @@ impl FromTemplate for Handle { type Template = HandleTemplate; } +#[derive(Reflect)] pub enum HandleTemplate { Path(AssetPath<'static>), Handle(Handle), diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index dd636d46a2e84..bd345ce5a2212 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -684,6 +684,7 @@ impl AssetApp for App { type_registry.register::>(); type_registry.register_type_data::(); type_registry.register_type_data::, ReflectHandle>(); + type_registry.register_type_conversion::>(|s| Ok(s.into())); } self diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs new file mode 100644 index 0000000000000..01d78a1080137 --- /dev/null +++ b/crates/bevy_reflect/src/convert.rs @@ -0,0 +1,225 @@ +//! The [`ReflectConvert`] type, which allows types to register conversions to +//! and from one another. + +use alloc::boxed::Box; +use core::any::TypeId; + +use bevy_utils::TypeIdMap; + +use crate::{Reflect, TypePath}; + +/// Provides a mechanism for converting values of one type to another. +/// +/// This [`crate::type_registry::TypeData`] is associated with the type to be +/// converted *into*, not the type to be converted *from*. To convert a value, +/// use code like the following: +/// +/// ```rust +/// # let mut registry = TypeRegistry::default(); +/// # registry.add_registration(i32::get_type_registration()); +/// # registry.add_registration(String::get_type_registration()); +/// # registry.register_type_conversion(|x: i32| Ok(x.to_string())); +/// +/// let reflect_convert = registry +/// .get_type_data::(TypeId::of::()) +/// .unwrap(); +/// let converted: String = reflect_convert +/// .try_convert_from(Box::new(12345i32)) +/// .unwrap() +/// .downcast::() +/// .unwrap(); +/// ``` +#[derive(Default)] +pub struct ReflectConvert { + /// A mapping from the type to be converted *from* to its associated + /// [`Converter`]. + conversions: TypeIdMap>, +} + +/// An internal trait that wraps a conversion function in an untyped interface. +trait Converter: Send + Sync { + /// Converts the value to the appropriate type. + /// + /// This returns the converted value if the conversion succeeds or the + /// original value if the conversion fails. + fn convert(&self, input: Box) -> Result, Box>; + + /// Returns a new boxed instance wrapping the same [`Converter`]. + fn clone_converter(&self) -> Box; +} + +/// A wrapper that contains a conversion function and implements [`Converter`]. +struct TypedConverter(fn(T) -> Result) +where + T: Reflect + TypePath, + U: Reflect + TypePath; + +impl ReflectConvert { + /// Creates a new, empty [`ReflectConvert`] with no conversions registered. + pub fn new() -> ReflectConvert { + Self::default() + } + + /// Attempts to construct an instance of this type from the provided + /// `input`. + /// + /// If the conversion fails, either because no conversion has been + /// registered from the type of `input` or because the conversion function + /// itself returned `Err`, the `input` value is returned as an error. + pub fn try_convert_from( + &self, + input: Box, + ) -> Result, Box> { + let type_id = (*input.as_any()).type_id(); + match self.conversions.get(&type_id) { + Some(converter) => converter.convert(input), + None => Err(input), + } + } + + /// Adds a conversion function from the type `T` to this type. + /// + /// 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(&mut self, function: fn(T) -> Result) + where + T: Reflect + TypePath, + U: Reflect + TypePath, + { + self.conversions + .insert(TypeId::of::(), Box::new(TypedConverter(function))); + } +} + +impl Clone for ReflectConvert { + fn clone(&self) -> Self { + ReflectConvert { + conversions: self + .conversions + .iter() + .map(|(type_id, converter)| (*type_id, converter.clone_converter())) + .collect(), + } + } +} + +impl Clone for TypedConverter +where + T: Reflect + TypePath, + U: Reflect + TypePath, +{ + fn clone(&self) -> Self { + TypedConverter(self.0) + } +} + +impl Converter for TypedConverter +where + T: Reflect + TypePath, + U: Reflect + TypePath, +{ + fn convert(&self, input: Box) -> Result, Box> { + match (self.0)(input.take()?) { + Ok(value) => Ok(Box::new(value)), + Err(value) => Err(Box::new(value)), + } + } + + fn clone_converter(&self) -> Box { + Box::new(self.clone()) + } +} + +#[cfg(test)] +mod tests { + use alloc::{ + borrow::ToOwned as _, + boxed::Box, + string::{String, ToString}, + }; + use core::any::TypeId; + + use crate::{convert::ReflectConvert, type_registry::GetTypeRegistration, TypeRegistry}; + + /// Tests that `i32` can be converted to `String` if the appropriate + /// conversion is registered. + #[test] + fn convert_from_i32_to_string() { + // Register the types and the conversion. + let mut registry = TypeRegistry::default(); + registry.add_registration(i32::get_type_registration()); + registry.add_registration(String::get_type_registration()); + registry.register_type_conversion(|x: i32| Ok(x.to_string())); + + let reflect_convert = registry + .get_type_data::(TypeId::of::()) + .unwrap(); + + // Test that a successful conversion works. + let converted = reflect_convert + .try_convert_from(Box::new(12345i32)) + .unwrap() + .downcast::() + .unwrap(); + assert_eq!(&**converted, "12345"); + } + + /// Tests that `String` can be fallibly converted to `i32` if the + /// appropriate conversion is registered. + /// + /// This also tests that the behavior of returning the original string on + /// error is correct. + #[test] + fn convert_from_string_to_i32() { + // Register the types and the conversion. + let mut registry = TypeRegistry::default(); + registry.add_registration(i32::get_type_registration()); + registry.add_registration(String::get_type_registration()); + registry.register_type_conversion(|x: String| match x.parse::() { + Ok(value) => Ok(value), + Err(_) => Err(x), + }); + + let reflect_convert = registry + .get_type_data::(TypeId::of::()) + .unwrap(); + + // Test a successful conversion from string to integer. + let converted = reflect_convert + .try_convert_from(Box::new("12345".to_owned())) + .unwrap() + .downcast::() + .unwrap(); + assert_eq!(*converted, 12345); + + // Test an unsuccessful conversion from string to integer. + let error = reflect_convert + .try_convert_from(Box::new("qqqqq".to_owned())) + .unwrap_err() + .downcast::() + .unwrap(); + assert_eq!(&**error, "qqqqq"); + } + + /// Tests that the error-handling behavior is correct when attempting a + /// conversion that hasn't been registered. + #[test] + fn no_such_conversion() { + let mut registry = TypeRegistry::default(); + registry.add_registration(i32::get_type_registration()); + registry.add_registration(String::get_type_registration()); + + let reflect_convert = registry + .get_type_data::(TypeId::of::()) + .unwrap(); + + // Test that we get the original value back on error. + let error = reflect_convert + .try_convert_from(Box::new("12345".to_owned())) + .unwrap_err() + .downcast::() + .unwrap(); + assert_eq!(&**error, "12345"); + } +} diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index cf0ac38354351..56799242b5d3f 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -645,6 +645,7 @@ mod impls { } pub mod attributes; +pub mod convert; pub mod enums; mod generics; pub mod serde; diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 8ccd81ce710f7..6e43f55165541 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -1,4 +1,6 @@ -use crate::{serde::Serializable, FromReflect, Reflect, TypeInfo, TypePath, Typed}; +use crate::{ + convert::ReflectConvert, serde::Serializable, FromReflect, Reflect, TypeInfo, TypePath, Typed, +}; use alloc::{boxed::Box, string::String}; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -353,6 +355,39 @@ impl TypeRegistry { data.insert(D::from_type()); } + /// Registers a fallible conversion from type T to U with the reflection + /// system. + /// + /// The supplied closure is expected to produce a value of type U, given an + /// instance of type T. If the conversion fails, the closure should return + /// the input value, wrapped in an `Err` variant. + /// + /// # Example + /// ``` + /// let mut type_registry = TypeRegistry::default(); + /// type_registry.register::(); + /// type_registry.register::(); + /// type_registry.register_type_conversion::(|n| Ok(n.into())); + /// ``` + pub fn register_type_conversion(&mut self, function: fn(T) -> Result) + where + T: Reflect + TypePath, + U: Reflect + TypePath, + { + let data = self.get_mut(TypeId::of::()).unwrap_or_else(|| { + panic!( + "attempted to call `TypeRegistry::register_type_conversion` for type `{U}` without registering `{U}` first", + U = U::type_path(), + ) + }); + if !data.contains::() { + data.insert(ReflectConvert::new()); + } + data.data_mut::() + .unwrap() + .register_type_conversion(function); + } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) From 865fa701d14edfba13c03814c75156b31b75fd6d Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 9 Apr 2026 20:24:08 -0700 Subject: [PATCH 02/10] Doc check police --- crates/bevy_app/src/app.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 05e939a64655c..ce0487b24e369 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -703,10 +703,12 @@ impl App { /// /// # Example /// ``` + /// use bevy_app::App; + /// /// App::new() /// .register_type::() /// .register_type::() - /// .register_type_conversion::(|n| Ok(n.into())); + /// .register_type_conversion::(|n| Ok(n.to_string())); /// ``` /// /// See [`bevy_reflect::TypeRegistry::register_type_conversion`]. From 55349176d4f8e4bc34839fda4d86783122976d84 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 9 Apr 2026 21:08:23 -0700 Subject: [PATCH 03/10] Doc check police --- crates/bevy_reflect/src/convert.rs | 5 ++++- crates/bevy_reflect/src/type_registry.rs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index 01d78a1080137..f622049710510 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -15,6 +15,9 @@ use crate::{Reflect, TypePath}; /// use code like the following: /// /// ```rust +/// # use bevy_reflect::{convert::ReflectConvert, GetTypeRegistration, TypeRegistry}; +/// # use std::any::TypeId; +/// # /// # let mut registry = TypeRegistry::default(); /// # registry.add_registration(i32::get_type_registration()); /// # registry.add_registration(String::get_type_registration()); @@ -23,7 +26,7 @@ use crate::{Reflect, TypePath}; /// let reflect_convert = registry /// .get_type_data::(TypeId::of::()) /// .unwrap(); -/// let converted: String = reflect_convert +/// let converted: String = *reflect_convert /// .try_convert_from(Box::new(12345i32)) /// .unwrap() /// .downcast::() diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 6e43f55165541..81521e3acea16 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -364,10 +364,12 @@ impl TypeRegistry { /// /// # Example /// ``` + /// # use bevy_reflect::TypeRegistry; + /// /// let mut type_registry = TypeRegistry::default(); /// type_registry.register::(); /// type_registry.register::(); - /// type_registry.register_type_conversion::(|n| Ok(n.into())); + /// type_registry.register_type_conversion::(|n| Ok(n.to_string())); /// ``` pub fn register_type_conversion(&mut self, function: fn(T) -> Result) where From d66e05ffc8585d66d3c3b024314f30c09b4454a6 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 9 Apr 2026 23:22:41 -0700 Subject: [PATCH 04/10] Add `register_into_type_conversion` and fix tests --- crates/bevy_app/src/app.rs | 24 ++++++++++++++++++ crates/bevy_app/src/sub_app.rs | 12 +++++++++ crates/bevy_reflect/src/convert.rs | 4 +++ crates/bevy_reflect/src/type_registry.rs | 31 ++++++++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index ce0487b24e369..ad3df52d11b8a 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -722,6 +722,30 @@ impl App { self } + /// Given types T and U, where `U: From`, registers that conversion with + /// the reflection system. + /// + /// # Example + /// ``` + /// use bevy_app::App; + /// + /// App::new() + /// .register_type::() + /// .register_type::() + /// .register_type_conversion::(|n| Ok(n.into())); + /// ``` + /// + /// See [`bevy_reflect::TypeRegistry::register_into_type_conversion`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_into_type_conversion(&mut self) -> &mut Self + where + T: Reflect + TypePath, + U: Reflect + TypePath + From, + { + self.main_mut().register_into_type_conversion::(); + self + } + /// Registers the given function into the [`AppFunctionRegistry`] resource. /// /// The given function will internally be stored as a [`DynamicFunction`] diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 391372d5fa9e0..806cdd0a2c7b3 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -497,6 +497,18 @@ impl SubApp { self } + /// See [`App::register_into_type_conversion`]. + #[cfg(feature = "bevy_reflect")] + pub fn register_into_type_conversion(&mut self) -> &mut Self + where + T: bevy_reflect::Reflect + bevy_reflect::TypePath, + U: bevy_reflect::Reflect + bevy_reflect::TypePath + From, + { + let registry = self.world.resource_mut::(); + registry.write().register_into_type_conversion::(); + self + } + /// See [`App::register_function`]. #[cfg(feature = "reflect_functions")] pub fn register_function(&mut self, function: F) -> &mut Self diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index f622049710510..c9ccb98a1026f 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -212,6 +212,10 @@ mod tests { let mut registry = TypeRegistry::default(); registry.add_registration(i32::get_type_registration()); registry.add_registration(String::get_type_registration()); + registry + .get_mut(TypeId::of::()) + .unwrap() + .insert(ReflectConvert::default()); let reflect_convert = registry .get_type_data::(TypeId::of::()) diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 81521e3acea16..c1aace734107f 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -390,6 +390,37 @@ impl TypeRegistry { .register_type_conversion(function); } + /// Given types T and U, where `U: From`, registers that conversion with + /// the reflection system. + /// + /// # Example + /// ``` + /// # use bevy_reflect::TypeRegistry; + /// + /// let mut type_registry = TypeRegistry::default(); + /// type_registry.register::(); + /// type_registry.register::(); + /// type_registry.register_type_conversion::(|n| Ok(n.into())); + /// ``` + pub fn register_into_type_conversion(&mut self) + where + T: Reflect + TypePath, + U: Reflect + TypePath + From, + { + let data = self.get_mut(TypeId::of::()).unwrap_or_else(|| { + panic!( + "attempted to call `TypeRegistry::register_type_conversion` for type `{U}` without registering `{U}` first", + U = U::type_path(), + ) + }); + if !data.contains::() { + data.insert(ReflectConvert::new()); + } + data.data_mut::() + .unwrap() + .register_type_conversion::(|input| Ok(input.into())); + } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) From 5b89b144bee17d5e1426a59a7ea8a7486954580c Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 9 Apr 2026 23:27:46 -0700 Subject: [PATCH 05/10] Make sure `HandleTemplate` is registered --- crates/bevy_asset/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index bd345ce5a2212..1053b5e389bb8 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -682,6 +682,7 @@ impl AssetApp for App { type_registry.register::(); type_registry.register::>(); + type_registry.register::>(); type_registry.register_type_data::(); type_registry.register_type_data::, ReflectHandle>(); type_registry.register_type_conversion::>(|s| Ok(s.into())); From 3a5871e134f504bccb7c56515d35006b9e74d160 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 11 Apr 2026 23:45:09 -0700 Subject: [PATCH 06/10] Add a test for multiple conversions to the same type --- crates/bevy_reflect/src/convert.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index c9ccb98a1026f..3e3a173713fb4 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -205,6 +205,36 @@ mod tests { assert_eq!(&**error, "qqqqq"); } + /// Tests that we can register multiple conversions into the same type and + /// that they all work. + #[test] + fn convert_from_f32_and_u32_to_i32() { + let mut registry = TypeRegistry::default(); + registry.add_registration(i32::get_type_registration()); + registry.add_registration(f32::get_type_registration()); + registry.add_registration(u32::get_type_registration()); + registry.register_type_conversion::(|n: u32| n.try_into().map_err(|_| n)); + registry.register_type_conversion::(|n: f32| Ok(n as i32)); + + let reflect_convert = registry + .get_type_data::(TypeId::of::()) + .unwrap(); + + // Test that we can convert `u32` and `f32` into `i32`. + let a = reflect_convert + .try_convert_from(Box::new(99u32)) + .unwrap() + .downcast::() + .unwrap(); + assert_eq!(*a, 99i32); + let b = reflect_convert + .try_convert_from(Box::new(99.0f32)) + .unwrap() + .downcast::() + .unwrap(); + assert_eq!(*b, 99i32); + } + /// Tests that the error-handling behavior is correct when attempting a /// conversion that hasn't been registered. #[test] From 59519648b0cd6c7934321b21040d948902ab730a Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 12 Apr 2026 12:31:19 -0700 Subject: [PATCH 07/10] Use a `Fn` instead of a `fn` --- crates/bevy_app/src/app.rs | 5 +-- crates/bevy_app/src/sub_app.rs | 7 +++-- crates/bevy_asset/src/lib.rs | 3 +- crates/bevy_reflect/src/convert.rs | 40 +++++++++++++++++------- crates/bevy_reflect/src/type_registry.rs | 9 +++--- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index ad3df52d11b8a..f291295046067 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -708,15 +708,16 @@ impl App { /// App::new() /// .register_type::() /// .register_type::() - /// .register_type_conversion::(|n| Ok(n.to_string())); + /// .register_type_conversion::(|n| Ok(n.to_string())); /// ``` /// /// See [`bevy_reflect::TypeRegistry::register_type_conversion`]. #[cfg(feature = "bevy_reflect")] - pub fn register_type_conversion(&mut self, function: fn(T) -> Result) -> &mut Self + pub fn register_type_conversion(&mut self, function: F) -> &mut Self where T: Reflect + TypePath, U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { self.main_mut().register_type_conversion(function); self diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 806cdd0a2c7b3..1b20aab2ff5d3 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -487,13 +487,16 @@ impl SubApp { /// See [`App::register_type_conversion`]. #[cfg(feature = "bevy_reflect")] - pub fn register_type_conversion(&mut self, function: fn(T) -> Result) -> &mut Self + pub fn register_type_conversion(&mut self, function: F) -> &mut Self where T: bevy_reflect::Reflect + bevy_reflect::TypePath, U: bevy_reflect::Reflect + bevy_reflect::TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { let registry = self.world.resource_mut::(); - registry.write().register_type_conversion::(function); + registry + .write() + .register_type_conversion::(function); self } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 1053b5e389bb8..d5f531fc5cb22 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -685,7 +685,8 @@ impl AssetApp for App { type_registry.register::>(); type_registry.register_type_data::(); type_registry.register_type_data::, ReflectHandle>(); - type_registry.register_type_conversion::>(|s| Ok(s.into())); + type_registry + .register_type_conversion::, _>(|s| Ok(s.into())); } self diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index 3e3a173713fb4..a3198ad050c03 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -2,7 +2,7 @@ //! and from one another. use alloc::boxed::Box; -use core::any::TypeId; +use core::{any::TypeId, marker::PhantomData}; use bevy_utils::TypeIdMap; @@ -52,10 +52,15 @@ trait Converter: Send + Sync { } /// A wrapper that contains a conversion function and implements [`Converter`]. -struct TypedConverter(fn(T) -> Result) +struct TypedConverter where T: Reflect + TypePath, - U: Reflect + TypePath; + U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, +{ + function: F, + phantom: PhantomData<(T, U)>, +} impl ReflectConvert { /// Creates a new, empty [`ReflectConvert`] with no conversions registered. @@ -85,13 +90,19 @@ impl ReflectConvert { /// 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(&mut self, function: fn(T) -> Result) + pub fn register_type_conversion(&mut self, function: F) where T: Reflect + TypePath, U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { - self.conversions - .insert(TypeId::of::(), Box::new(TypedConverter(function))); + self.conversions.insert( + TypeId::of::(), + Box::new(TypedConverter { + function, + phantom: PhantomData, + }), + ); } } @@ -107,23 +118,28 @@ impl Clone for ReflectConvert { } } -impl Clone for TypedConverter +impl Clone for TypedConverter where T: Reflect + TypePath, U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { fn clone(&self) -> Self { - TypedConverter(self.0) + TypedConverter { + function: self.function.clone(), + phantom: PhantomData, + } } } -impl Converter for TypedConverter +impl Converter for TypedConverter where T: Reflect + TypePath, U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { fn convert(&self, input: Box) -> Result, Box> { - match (self.0)(input.take()?) { + match (self.function)(input.take()?) { Ok(value) => Ok(Box::new(value)), Err(value) => Err(Box::new(value)), } @@ -213,8 +229,8 @@ mod tests { registry.add_registration(i32::get_type_registration()); registry.add_registration(f32::get_type_registration()); registry.add_registration(u32::get_type_registration()); - registry.register_type_conversion::(|n: u32| n.try_into().map_err(|_| n)); - registry.register_type_conversion::(|n: f32| Ok(n as i32)); + registry.register_type_conversion::(|n: u32| n.try_into().map_err(|_| n)); + registry.register_type_conversion::(|n: f32| Ok(n as i32)); let reflect_convert = registry .get_type_data::(TypeId::of::()) diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index c1aace734107f..8db68f7c2fae3 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -369,12 +369,13 @@ impl TypeRegistry { /// let mut type_registry = TypeRegistry::default(); /// type_registry.register::(); /// type_registry.register::(); - /// type_registry.register_type_conversion::(|n| Ok(n.to_string())); + /// type_registry.register_type_conversion::(|n| Ok(n.to_string())); /// ``` - pub fn register_type_conversion(&mut self, function: fn(T) -> Result) + pub fn register_type_conversion(&mut self, function: F) where T: Reflect + TypePath, U: Reflect + TypePath, + F: Fn(T) -> Result + Clone + Send + Sync + 'static, { let data = self.get_mut(TypeId::of::()).unwrap_or_else(|| { panic!( @@ -400,7 +401,7 @@ impl TypeRegistry { /// let mut type_registry = TypeRegistry::default(); /// type_registry.register::(); /// type_registry.register::(); - /// type_registry.register_type_conversion::(|n| Ok(n.into())); + /// type_registry.register_type_conversion::(|n| Ok(n.into())); /// ``` pub fn register_into_type_conversion(&mut self) where @@ -418,7 +419,7 @@ impl TypeRegistry { } data.data_mut::() .unwrap() - .register_type_conversion::(|input| Ok(input.into())); + .register_type_conversion::(|input| Ok(input.into())); } /// Whether the type with given [`TypeId`] has been registered in this registry. From 1e176594ac6208d0e13642fcbecf0919f6cf8173 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 12 Apr 2026 13:51:33 -0700 Subject: [PATCH 08/10] Doc check police --- crates/bevy_app/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index f291295046067..b78fbedd1a241 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -733,7 +733,7 @@ impl App { /// App::new() /// .register_type::() /// .register_type::() - /// .register_type_conversion::(|n| Ok(n.into())); + /// .register_into_type_conversion::(); /// ``` /// /// See [`bevy_reflect::TypeRegistry::register_into_type_conversion`]. From 008aefe7f06de6c65d89874cd15fcbf0b345df92 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 13 Apr 2026 12:11:44 -0700 Subject: [PATCH 09/10] Address review comments --- crates/bevy_reflect/src/convert.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index a3198ad050c03..451fb59667c5f 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -139,9 +139,13 @@ where F: Fn(T) -> Result + Clone + Send + Sync + 'static, { fn convert(&self, input: Box) -> Result, Box> { - match (self.function)(input.take()?) { + let mut input = input.downcast::()?; + match (self.function)(*input) { Ok(value) => Ok(Box::new(value)), - Err(value) => Err(Box::new(value)), + Err(value) => { + *input = value; + Err(input) + } } } From 0cf1e95f3a231ae574cc869497391eee7612fea4 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 13 Apr 2026 13:08:05 -0700 Subject: [PATCH 10/10] Add TypeRegistration::get_or_insert_data_with and use it in the appropriate places --- crates/bevy_reflect/src/convert.rs | 5 ----- crates/bevy_reflect/src/type_registry.rs | 24 ++++++++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/crates/bevy_reflect/src/convert.rs b/crates/bevy_reflect/src/convert.rs index 451fb59667c5f..5fd9bcfdaa1ca 100644 --- a/crates/bevy_reflect/src/convert.rs +++ b/crates/bevy_reflect/src/convert.rs @@ -63,11 +63,6 @@ where } impl ReflectConvert { - /// Creates a new, empty [`ReflectConvert`] with no conversions registered. - pub fn new() -> ReflectConvert { - Self::default() - } - /// Attempts to construct an instance of this type from the provided /// `input`. /// diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 8db68f7c2fae3..55e137ae74768 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -383,11 +383,7 @@ impl TypeRegistry { U = U::type_path(), ) }); - if !data.contains::() { - data.insert(ReflectConvert::new()); - } - data.data_mut::() - .unwrap() + data.get_or_insert_data_with(ReflectConvert::default) .register_type_conversion(function); } @@ -414,11 +410,7 @@ impl TypeRegistry { U = U::type_path(), ) }); - if !data.contains::() { - data.insert(ReflectConvert::new()); - } - data.data_mut::() - .unwrap() + data.get_or_insert_data_with(ReflectConvert::default) .register_type_conversion::(|input| Ok(input.into())); } @@ -657,6 +649,18 @@ impl TypeRegistration { self.data.insert(TypeId::of::(), Box::new(data)); } + /// Gets the instance of `T` into this registration's [type data], if it exists. If it does not + /// exist, it will insert a new instance using `get_data` and then return it. + /// + /// [type data]: TypeData + pub fn get_or_insert_data_with(&mut self, get_data: impl FnOnce() -> T) -> &mut T { + let boxed_data = self + .data + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(get_data())); + boxed_data.downcast_mut::().unwrap() + } + /// Inserts the [`TypeData`] instance of `T` created for `V`, and inserts any /// [`TypeData`] dependencies for that combination of `T` and `V`. #[inline]