Skip to content

bevy_reflect: Type data registration callbacks#24518

Open
MrGVSV wants to merge 7 commits into
bevyengine:mainfrom
MrGVSV:mrgvsv/type-data-callbacks
Open

bevy_reflect: Type data registration callbacks#24518
MrGVSV wants to merge 7 commits into
bevyengine:mainfrom
MrGVSV:mrgvsv/type-data-callbacks

Conversation

@MrGVSV
Copy link
Copy Markdown
Member

@MrGVSV MrGVSV commented Jun 2, 2026

Objective

This is a rework of #9777.

I'll give the TL;DR of the objective from that PR here.

Many times we'll define type data that relies on other types and type data being registered.

An example in our codebase might be ReflectAsset. This type data wants to work with other types and type data. So to ensure these are available in the registry, we define an extension method on App specifically for their registration:

fn register_asset_reflect<A>(&mut self) -> &mut Self
where
A: Asset + Reflect + FromReflect + GetTypeRegistration,
{
let type_registry = self.world().resource::<AppTypeRegistry>();
{
let mut type_registry = type_registry.write();
type_registry.register::<A>();
type_registry.register::<Handle<A>>();
type_registry.register::<HandleTemplate<A>>();
type_registry.register_type_data::<A, ReflectAsset>();
type_registry.register_type_data::<Handle<A>, ReflectHandle>();
type_registry
.register_type_conversion::<String, HandleTemplate<A>, _>(|s| Ok(s.into()));
}
self
}

This method registers other types like Handle<A> and type data like ReflectHandle. But users need to know to use it. Not only that, but it means that other libraries with similar needs will likely want to follow in the same footsteps, meaning more work for library authors and more mental burden for library users.

Registering such a type should be as simple as this:

#[derive(Reflect, Asset)]
#[reflect(Asset)]
structure MyAsset;

app.register::<MyAsset>();

Done. No special app extension methods. Just a simple registration like every other type.

Solution

Callbacks

To achieve this, this PR adds a more comprehensive set of registration callbacks. The following callbacks have been added:

  • TypeData::on_insert
  • TypeData::on_register
  • CreateTypeData::on_insert (CreateTypeData::insert_dependencies has been deprecated)
  • CreateTypeData::on_register

The on_insert callbacks are invoked when the type data is inserted into a TypeRegistration. The on_register callbacks are invoked when that TypeRegistration is registered into a TypeRegistration (or immediately if already registered—more on how we do that later).

I chose to allow callbacks on both traits for maximum flexibility. Here is a list of their pros/cons:

  • TypeData callbacks
    • Work for all type data, regardless of whether CreateTypeData is also implemented
    • Always invoked no matter which method on TypeRegistration/TypeRegistry is used
    • Access to &self for runtime-defined callbacks
    • No compile-time information for the type the type data is being registered to
  • CreateTypeData callbacks
    • Only invoked when registered through specific methods on TypeRegistration and TypeRegistry
    • Provide full compile-time information for the type the type data is being registered to
    • Ability to pass in input directly from the derive macro
    • No access to &self (though, we certainly could add that if we think it would be beneficial)

Below is how we might redefine ReflectAsset to make use of these callbacks:

impl<A: Asset + FromReflect> CreateTypeData<A> for ReflectAsset {
  // ...

  fn on_register(_: &()) -> Option<OnRegisterTypeData> {
    Some(OnRegisterTypeData::new(|registry| {
      registry.register::<A>();
      registry.register::<Handle<A>>();
      registry.register::<HandleTemplate<A>>();
      registry.register_type_data::<Handle<A>, ReflectHandle>();
      registry.register_type_conversion::<String, HandleTemplate<A>, _>(|s| Ok(s.into()));
    }))
  }
}

Removed Clone

Additionally, I removed the restriction that TypeRegistration: Clone. It's really not needed for any of our internal machinery and it doesn't appear to be useful for users in most cases.

This also means that type data no longer needs to be Clone. So now the only real requirements are Self: Send + Sync + 'static, allowing for much more flexibility in defining type data.

Derive Macro

We also introduce a derive macro to quickly derive TypeData without any callbacks. This macro was kept very barebones for this PR. We can explore improvements and helper attributes for it in a followup PR (such as a way to easily define callbacks or dependencies).

Restricted Type Data Insertions

To ensure that callbacks are almost always properly invoked, we had to change how type data is inserted into a TypeRegistration. The main change is that type data can no longer be inserted directly onto a &mut TypeRegistration. Instead, type data must be inserted onto an owned instance of TypeRegistration.

Additionally, the on_insert callbacks are provided a TypeRegistrationMut<'_> in order to mutably insert type data onto an existing registration. This type is also used by the new TypeRegistry::registration_scope method to provide users with a way of inserting multiple type data without incurring the cost of multiple registration lookups).

By adding these restrictions, we can ensure that on_register callbacks are always invoked.

Documentation Improvements

I also went ahead and added a new set of docs for the type_data module (which is now public). I figured since things were getting somewhat complicated, it made sense to have a central place explaining how everything works together.

I also updated the type_data example to account for the new changes.

Testing

I added extensive unit tests to bevy_reflect and also updated the type_data example.

Followup Work

After working on this PR, I think I've determined the following work as good for followup:

  • Reorganizing all type data and registry code into a new registry directory module
  • Improving the derive macro with additional helper attributes (possibly similar to how hooks are defined for Component derives)
  • Identify existing type data, like ReflectAsset, that could benefit from these changes
  • Doing a cleanup pass to ensure consistency in naming, parameter ordering, etc. (e.g., we use both input and params for talking about CreateTypeData input)

Additionally, we could consider adding the ability to queue additional on_register callbacks onto a TypeRegistration apart from type data (potentially removing the need for GetTypeRegistration::register_type_dependencies). This is actually trivial to implement with this PR, but I was worried there was already too much happening in a single diff. Of course, if we'd definitely like to do that, I can do that! Drafted a PR: MrGVSV#2

AI Usage Disclaimer

As per the AI usage policy, I did not have LLMs generate any code, assets, examples, or documentation—including this PR's description. Nor did I use LLMs to make design decisions for me.

My AI usage was limited to using ChatGPT for rubberducking, evaluating solutions, and suggesting type names.


Showcase

Type data can now be defined with a comprehensive set of registration callbacks, including on_insert and on_register.

Additionally, type data no longer needs to be Clone and instead only needs to implement or derive TypeData.

Below is a snippet from our newly updated type data example:

Click to view showcase
struct MyTypeData;

// We can add callbacks directly to type data, which will always be invoked.
impl TypeData for MyTypeData {
    // This callback runs when the type data is inserted into a `TypeRegistration`.
    // We can use it to insert other type data into the same registration.
    fn on_insert(&self) -> Option<OnInsertTypeData> {
        Some(OnInsertTypeData::new(|_registration| {
            println!("called TypeData::on_insert");
        }))
    }

    // This callback runs when the `TypeRegistration` is registered (or is already registered).
    // We can use it to register other types we know we'll need.
    fn on_register(&self) -> Option<OnRegisterTypeData> {
        Some(OnRegisterTypeData::new(|_registry| {
            println!("called TypeData::on_register");
        }))
    }
}

// We can also register callbacks on `CreateTypeData`.
// These will only be run when using a method like `TypeRegistry::register_type_data`
// or `TypeRegistration::register_type_data`.
// However, it has the benefit of providing access to the type we're registering, `T`, as well as any input.
impl<T: Reflect> CreateTypeData<T, String> for MyTypeData {
    fn create_type_data(_: String) -> Self {
        Self
    }

    // This callback runs when the type data is inserted into a `TypeRegistration`.
    // We can use it to insert other type data into the same registration.
    fn on_insert(input: &String) -> Option<OnInsertTypeData> {
        let input = input.clone();
        Some(OnInsertTypeData::new(move |_registration| {
            println!("called CreateTypeData::on_insert with input: {input}");
        }))
    }

    // This callback runs when the `TypeRegistration` is registered (or is already registered).
    // We can use it to register other types we know we'll need.
    fn on_register(input: &String) -> Option<OnRegisterTypeData> {
        let input = input.clone();
        Some(OnRegisterTypeData::new(move |_registry| {
            println!("called CreateTypeData::on_register with input: {input}");
        }))
    }
}

println!("i32:");
let registration = TypeRegistration::of::<i32>();
// This should invoke `TypeData::on_insert`:
let registration = registration.insert_data(MyTypeData);

let mut registry = TypeRegistry::empty();
// This should invoke `TypeData::on_register`:
registry.add_registration(registration);

println!("u32:");
registry.register::<u32>();
// This should invoke both `TypeData` and `CreateTypeData` callbacks:
registry.register_type_data_with::<u32, MyTypeData, _>(String::from("HELLO!"));

println!("f32:");
// But again, note that `CreateTypeData` callbacks are not automatically invoked when used directly
let data =
    <MyTypeData as CreateTypeData<f32, String>>::create_type_data(String::from("HELLO!"));
let registration = TypeRegistration::of::<f32>().insert_data(data);
registry.add_registration(registration);

Output:

i32:
called TypeData::on_insert
called TypeData::on_register
u32:
called TypeData::on_insert
called CreateTypeData::on_insert with input: HELLO!
called TypeData::on_register
called CreateTypeData::on_register with input: HELLO!
f32:
called TypeData::on_insert
called TypeData::on_register

@MrGVSV MrGVSV added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide 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 Jun 2, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Reflection Jun 2, 2026
@MrGVSV MrGVSV force-pushed the mrgvsv/type-data-callbacks branch 3 times, most recently from 5f584ef to e04239a Compare June 2, 2026 20:41
@MrGVSV MrGVSV force-pushed the mrgvsv/type-data-callbacks branch from e04239a to 7174bbc Compare June 2, 2026 21:06
@MrGVSV MrGVSV moved this from Needs SME Triage to SME Triaged in Reflection Jun 2, 2026
Copy link
Copy Markdown
Contributor

@andriyDev andriyDev left a comment

Choose a reason for hiding this comment

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

Nice! Looks great, just a couple of nits.

Comment thread crates/bevy_reflect/derive/src/lib.rs Outdated
Comment thread crates/bevy_reflect/src/type_data.rs Outdated
Co-authored-by: andriyDev <andriydzikh@gmail.com>
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.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-Usability A targeted quality-of-life change that makes Bevy easier to use D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: SME Triaged

Development

Successfully merging this pull request may close these issues.

2 participants