Skip to content

Commit 454fa56

Browse files
eugineerdMrGVSVcart
authored
Reflect auto registration (#15030)
# Objective This PR adds automatic registration of non-generic types annotated with `#[derive(Reflect)]`. Manually registering all reflect types can be error-prone, failing to reflect during runtime if some type is not registered. #5781 fixed this for nested types, improving ergonomics of using reflection dramatically, but top-level types still need to be registered manually. This is especially painful when prototyping and using inspector to change values of components. ```rs // from #5781 #[derive(Reflect)] struct Foo(Bar); #[derive(Reflect)] struct Bar(Baz); #[derive(Reflect)] struct Baz(usize); fn main() { // ... app.register_type::<Foo>() // <- can still forget to add this // ... } ``` Fixes #3936. ## Solution Automatically register all types that derive Reflect. This works for all non-generic types, allowing users to just add `#[derive(Reflect)]` to any compatible type and not think about where to register it. ```rs fn main() { // No need to manually call .register_type::<Foo>() App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .run(); } #[derive(Reflect)] pub struct Foo { a: usize, } fn setup(type_registry: Res<AppTypeRegistry>) { let type_registry = type_registry.read(); assert!(type_registry.contains(TypeId::of::<Foo>())); } ``` Automatic registration can be opted-out of by adding `#[reflect(no_auto_register)]` reflect attribute to a type: ```rs #[derive(Reflect)] #[reflect(no_auto_register)] pub struct Foo { a: usize, } ``` Registration normally happens at app creation time, but `TypeRegistry` itself has a new `register_derived_types` method to register all collected types at any appropriate time: ```rs #[derive(Reflect)] struct Foo { name: Option<String>, value: i32 } let mut type_registry = TypeRegistry::empty(); type_registry.register_derived_types(); assert!(type_registry.contains(TypeId::of::<Foo>())); ``` ## Testing - Tested by running `reflect` example and removing explicit type registration, both on native and on `wasm32-unknown-unknown`. - Added a doctest to `register_derived_types` Impact on startup time is negligible: <40ms on debug wasm build and <25ms on debug native build for 1614 registered types . Wasm binary size comparison. All sizes are in KB as reported by `du` on the modified `reflection` example. | | **auto register, KiB** | **no auto register, KiB** | **abs diff, KiB** | **% diff** | | ---------------------------- | ---------------------- | ------------------------- | ----------------- | ---------- | | wasm-release && wasm-bindgen | 27000 | 24224 | 2776 | 11.46% | | wasm-opt -Oz | 16376 | 15340 | 1036 | 6.75% | | wasm-opt -Os | 17728 | 16424 | 1304 | 7.94% | | wasm-opt -Oz && gzip -c | 5620 | 5364 | 256 | 4.77% | ## Considerations While automatic type registration improves ergonomics quite a bit for projects that make heavy use of it, there are also some problems: - Generic types can't be automatically registered. However, as long as top-level reflect types don't need generics, recursive registration can take care of the rest. - Wasm bundle size increase is not insignificant. This is mostly due to registering various engine types that weren't registered before. Size overhead of automatic registration of types instead of manual is relatively small, as can be see from this (somewhat outdated but still relevant) table (see last 2 rows): <details> <summary>1000 simple structs with reflect wasm size comparison, no engine types</summary> | | auto register, KiB | no auto register, KiB | abs diff, KiB | % diff | | --------------------------------------------- | ------------- | ---------------- | -------- | ------ | | wasm-release && wasm-bindgen | 27944 | 21644 | 6300 | 22.55% | | wasm-release && wasm-bindgen +register_type | 28084 | 27764 | 320 | 1.14% | | wasm-opt -Oz | 15612 | 13928 | 1684 | 10.79% | | wasm-opt -Oz +register_type | 15676 | 15580 | 96 | 0.61% | | wasm-release && wasm-bindgen (manual vs auto) | 27944 | 27764 | 180 | 0.64% | | wasm-opt -Oz (manual vs auto) | 15612 | 15580 | 32 | 0.2% | </details> --------- Co-authored-by: Gino Valente <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
1 parent 5af68e9 commit 454fa56

File tree

27 files changed

+652
-8
lines changed

27 files changed

+652
-8
lines changed

Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ members = [
2828
"examples/mobile",
2929
# Examples of using Bevy on no_std platforms.
3030
"examples/no_std/*",
31+
# Examples of compiling Bevy with automatic reflect type registration for platforms without `inventory` support.
32+
"examples/reflection/auto_register_static",
3133
# Benchmarks
3234
"benches",
3335
# Internal tools that are not published.
@@ -159,6 +161,7 @@ default = [
159161
"hdr",
160162
"multi_threaded",
161163
"png",
164+
"reflect_auto_register",
162165
"smaa_luts",
163166
"sysinfo_plugin",
164167
"tonemapping_luts",
@@ -548,6 +551,12 @@ reflect_functions = ["bevy_internal/reflect_functions"]
548551
# Enable documentation reflection
549552
reflect_documentation = ["bevy_internal/reflect_documentation"]
550553

554+
# Enable automatic reflect registration
555+
reflect_auto_register = ["bevy_internal/reflect_auto_register"]
556+
557+
# Enable automatic reflect registration without inventory. See `reflect::load_type_registrations` for more info.
558+
reflect_auto_register_static = ["bevy_internal/reflect_auto_register_static"]
559+
551560
# Enable winit custom cursor support
552561
custom_cursor = ["bevy_internal/custom_cursor"]
553562

@@ -2781,6 +2790,17 @@ description = "Demonstrates how to create and use type data"
27812790
category = "Reflection"
27822791
wasm = false
27832792

2793+
[[example]]
2794+
name = "auto_register_static"
2795+
path = "examples/reflection/auto_register_static/src/lib.rs"
2796+
doc-scrape-examples = true
2797+
2798+
[package.metadata.example.auto_register_static]
2799+
name = "Automatic types registration"
2800+
description = "Demonstrates how to set up automatic reflect types registration for platforms without `inventory` support"
2801+
category = "Reflection"
2802+
wasm = false
2803+
27842804
# Scene
27852805
[[example]]
27862806
name = "scene"

crates/bevy_app/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ reflect_functions = [
2222
"bevy_reflect/functions",
2323
"bevy_ecs/reflect_functions",
2424
]
25+
reflect_auto_register = [
26+
"bevy_reflect",
27+
"bevy_reflect/auto_register",
28+
"bevy_ecs/reflect_auto_register",
29+
]
2530

2631
# Debugging Features
2732

crates/bevy_app/src/app.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ impl Default for App {
108108
{
109109
use bevy_ecs::observer::ObservedBy;
110110

111+
#[cfg(not(feature = "reflect_auto_register"))]
111112
app.init_resource::<AppTypeRegistry>();
113+
114+
#[cfg(feature = "reflect_auto_register")]
115+
app.insert_resource(AppTypeRegistry::new_with_derived_types());
116+
112117
app.register_type::<Name>();
113118
app.register_type::<ChildOf>();
114119
app.register_type::<Children>();

crates/bevy_app/src/hotpatch.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ extern crate alloc;
33

44
use alloc::sync::Arc;
55

6+
#[cfg(feature = "reflect_auto_register")]
7+
use bevy_ecs::schedule::IntoScheduleConfigs;
68
use bevy_ecs::{
79
change_detection::DetectChangesMut, event::EventWriter, system::ResMut, HotPatchChanges,
810
HotPatched,
@@ -44,5 +46,14 @@ impl Plugin for HotPatchPlugin {
4446
}
4547
},
4648
);
49+
50+
#[cfg(feature = "reflect_auto_register")]
51+
app.add_systems(
52+
crate::First,
53+
(move |registry: bevy_ecs::system::Res<bevy_ecs::reflect::AppTypeRegistry>| {
54+
registry.write().register_derived_types();
55+
})
56+
.run_if(bevy_ecs::schedule::common_conditions::on_event::<HotPatched>),
57+
);
4758
}
4859
}

crates/bevy_ecs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ bevy_reflect = ["dep:bevy_reflect"]
2727

2828
## Extends reflection support to functions.
2929
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
30+
reflect_auto_register = ["bevy_reflect", "bevy_reflect/auto_register"]
3031

3132
## Enables automatic backtrace capturing in BevyError
3233
backtrace = ["std"]

crates/bevy_ecs/src/reflect/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ impl DerefMut for AppTypeRegistry {
4747
}
4848
}
4949

50+
impl AppTypeRegistry {
51+
/// Creates [`AppTypeRegistry`] and automatically registers all types deriving [`Reflect`].
52+
///
53+
/// See [`TypeRegistry::register_derived_types`] for more details.
54+
#[cfg(feature = "reflect_auto_register")]
55+
pub fn new_with_derived_types() -> Self {
56+
let app_registry = AppTypeRegistry::default();
57+
app_registry.write().register_derived_types();
58+
app_registry
59+
}
60+
}
61+
5062
/// A [`Resource`] storing [`FunctionRegistry`] for
5163
/// function registrations relevant to a whole app.
5264
///

crates/bevy_internal/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,20 @@ reflect_functions = [
300300
"bevy_ecs/reflect_functions",
301301
]
302302

303+
# Enable automatic reflect registration using inventory.
304+
reflect_auto_register = [
305+
"bevy_reflect/auto_register_inventory",
306+
"bevy_app/reflect_auto_register",
307+
"bevy_ecs/reflect_auto_register",
308+
]
309+
310+
# Enable automatic reflect registration without inventory. See `reflect::load_type_registrations` for more info.
311+
reflect_auto_register_static = [
312+
"bevy_reflect/auto_register_static",
313+
"bevy_app/reflect_auto_register",
314+
"bevy_ecs/reflect_auto_register",
315+
]
316+
303317
# Enable documentation reflection
304318
reflect_documentation = ["bevy_reflect/documentation"]
305319

crates/bevy_reflect/Cargo.toml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ keywords = ["bevy"]
1010
rust-version = "1.85.0"
1111

1212
[features]
13-
default = ["std", "smallvec", "debug"]
13+
default = ["std", "smallvec", "debug", "auto_register_inventory"]
1414

1515
# Features
1616

@@ -68,6 +68,22 @@ std = [
6868
## on all platforms, including `no_std`.
6969
critical-section = ["bevy_platform/critical-section"]
7070

71+
# Enables automatic reflect registration. Does nothing by itself,
72+
# must select `auto_register_inventory` or `auto_register_static` to make it work.
73+
auto_register = []
74+
## Enables automatic reflect registration using inventory. Not supported on all platforms.
75+
auto_register_inventory = [
76+
"auto_register",
77+
"bevy_reflect_derive/auto_register_inventory",
78+
"dep:inventory",
79+
]
80+
## Enable automatic reflect registration without inventory. This feature has precedence over `auto_register_inventory`.
81+
## See `load_type_registrations` for more info.
82+
auto_register_static = [
83+
"auto_register",
84+
"bevy_reflect_derive/auto_register_static",
85+
]
86+
7187
## Enables use of browser APIs.
7288
## Note this is currently only applicable on `wasm32` architectures.
7389
web = ["bevy_platform/web", "uuid?/js"]
@@ -113,6 +129,9 @@ wgpu-types = { version = "26", features = [
113129
"serde",
114130
], optional = true, default-features = false }
115131

132+
# deps for automatic type registration
133+
inventory = { version = "0.3", optional = true }
134+
116135
[dev-dependencies]
117136
ron = "0.10"
118137
rmp-serde = "1.1"

crates/bevy_reflect/derive/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ default = []
1717
documentation = []
1818
# Enables macro logic related to function reflection
1919
functions = []
20+
# Enables automatic reflect registration. Does nothing by itself,
21+
# must select `auto_register_inventory` or `auto_register_static` to make it work.
22+
auto_register = []
23+
# Enables automatic reflection using inventory. Not supported on all platforms.
24+
auto_register_inventory = ["auto_register"]
25+
# Enables automatic reflection on platforms not supported by inventory. See `load_type_registrations` for more info.
26+
auto_register_static = ["auto_register", "dep:uuid"]
2027

2128
[dependencies]
2229
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" }
2330
indexmap = "2.0"
2431
proc-macro2 = "1.0"
2532
quote = "1.0"
2633
syn = { version = "2.0", features = ["full", "extra-traits"] }
34+
uuid = { version = "1.13.1", default-features = false, features = [
35+
"v4",
36+
], optional = true }
2737

2838
[target.'cfg(target_arch = "wasm32")'.dependencies]
2939
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.

crates/bevy_reflect/derive/src/container_attributes.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod kw {
2525
syn::custom_keyword!(Hash);
2626
syn::custom_keyword!(Clone);
2727
syn::custom_keyword!(no_field_bounds);
28+
syn::custom_keyword!(no_auto_register);
2829
syn::custom_keyword!(opaque);
2930
}
3031

@@ -184,6 +185,7 @@ pub(crate) struct ContainerAttributes {
184185
type_path_attrs: TypePathAttrs,
185186
custom_where: Option<WhereClause>,
186187
no_field_bounds: bool,
188+
no_auto_register: bool,
187189
custom_attributes: CustomAttributes,
188190
is_opaque: bool,
189191
idents: Vec<Ident>,
@@ -240,6 +242,8 @@ impl ContainerAttributes {
240242
self.parse_no_field_bounds(input)
241243
} else if lookahead.peek(kw::Clone) {
242244
self.parse_clone(input)
245+
} else if lookahead.peek(kw::no_auto_register) {
246+
self.parse_no_auto_register(input)
243247
} else if lookahead.peek(kw::Debug) {
244248
self.parse_debug(input)
245249
} else if lookahead.peek(kw::Hash) {
@@ -378,6 +382,16 @@ impl ContainerAttributes {
378382
Ok(())
379383
}
380384

385+
/// Parse `no_auto_register` attribute.
386+
///
387+
/// Examples:
388+
/// - `#[reflect(no_auto_register)]`
389+
fn parse_no_auto_register(&mut self, input: ParseStream) -> syn::Result<()> {
390+
input.parse::<kw::no_auto_register>()?;
391+
self.no_auto_register = true;
392+
Ok(())
393+
}
394+
381395
/// Parse `where` attribute.
382396
///
383397
/// Examples:
@@ -583,6 +597,12 @@ impl ContainerAttributes {
583597
self.no_field_bounds
584598
}
585599

600+
/// Returns true if the `no_auto_register` attribute was found on this type.
601+
#[cfg(feature = "auto_register")]
602+
pub fn no_auto_register(&self) -> bool {
603+
self.no_auto_register
604+
}
605+
586606
/// Returns true if the `opaque` attribute was found on this type.
587607
pub fn is_opaque(&self) -> bool {
588608
self.is_opaque

0 commit comments

Comments
 (0)