Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic registration of ECS types #12332

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b14896a
Automatically register Component types on initialization
james7132 Mar 6, 2024
3d5f60d
Make AppTypeRegistry use the World internal one
james7132 Mar 6, 2024
5a9c04b
Merge branch 'main' into automatic-ecs-registration
james7132 Mar 6, 2024
fc1b308
Fix dynamic_scene_builder tests
james7132 Mar 6, 2024
9c388a5
Formatting
james7132 Mar 6, 2024
91354fd
Fix typo.
james7132 Mar 6, 2024
c19bda8
Cleanup proc macro code
james7132 Mar 6, 2024
cc7c253
Target reflect(Component) instead
james7132 Mar 6, 2024
51a2f08
Support automatically registering resources
james7132 Mar 6, 2024
b615535
Fix up tests
james7132 Mar 6, 2024
512593e
Cleanup + disable generics
james7132 Mar 7, 2024
20dde15
Minimize what we're reexporting from the private module
james7132 Mar 7, 2024
5fdea45
Merge branch 'main' into automatic-ecs-registration
james7132 Mar 8, 2024
3e1b0b1
Fix tests
james7132 Mar 8, 2024
ccd8405
Fix deadlock by not panicking if already registered.
james7132 Mar 8, 2024
20e18ac
Fix tests
james7132 Mar 8, 2024
3013029
Fix more tests
james7132 Mar 8, 2024
01725d9
Merge branch 'main' into automatic-ecs-registration
james7132 Mar 8, 2024
4558439
Remove unnecessary registrations
james7132 Mar 9, 2024
5aa2fa9
Cleanup generated code with a shim
james7132 Mar 9, 2024
a8950a6
Cleanup has_reflect_attr
james7132 Mar 9, 2024
47ef511
Cleanup macro logic
james7132 Mar 9, 2024
54809cf
More cleanup
james7132 Mar 9, 2024
8e44f11
then_some
james7132 Mar 9, 2024
4f07b1f
Update example for scenes
james7132 Mar 9, 2024
9a42c8b
Provide crate level docs
james7132 Mar 9, 2024
89bddcb
AppTypeRegistry is already in bevy_ecs
james7132 Mar 9, 2024
807f947
Make AppTypeRegistry only constructable via FromWorld
james7132 Mar 9, 2024
000f939
Add registration test for resources
james7132 Mar 9, 2024
91ad713
Fix tests
james7132 Mar 9, 2024
9fda301
Merge branch 'main' into automatic-ecs-registration
james7132 Mar 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ categories = ["game-engines", "data-structures"]
trace = []
multi-threaded = ["bevy_tasks/multi-threaded"]
bevy_debug_stepping = []
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs_macros/bevy_reflect"]
default = ["bevy_reflect", "bevy_debug_stepping"]

[dependencies]
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ license = "MIT OR Apache-2.0"
[lib]
proc-macro = true

[features]
bevy_reflect = []

[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }

Expand Down
38 changes: 37 additions & 1 deletion crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{parse_macro_input, parse_quote, DeriveInput, Ident, LitStr, Path, Result};
use syn::{parse_macro_input, parse_quote, DeriveInput, Ident, LitStr, Meta, Path, Result};

pub fn derive_event(input: TokenStream) -> TokenStream {
let mut ast = parse_macro_input!(input as DeriveInput);
Expand Down Expand Up @@ -32,9 +32,11 @@ pub fn derive_resource(input: TokenStream) -> TokenStream {

let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
let register_type = get_type_registration(&ast, &bevy_ecs_path, "Resource");

TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::system::Resource for #struct_name #type_generics #where_clause {
#register_type
}
})
}
Expand All @@ -57,16 +59,19 @@ pub fn derive_component(input: TokenStream) -> TokenStream {

let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
let register_type = get_type_registration(&ast, &bevy_ecs_path, "Component");

TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
#register_type
}
})
}

pub const COMPONENT: &str = "component";
pub const STORAGE: &str = "storage";
const REFLECT: &str = "reflect";

struct Attrs {
storage: StorageTy,
Expand Down Expand Up @@ -109,6 +114,37 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
Ok(attrs)
}

fn get_type_registration(
ast: &DeriveInput,
bevy_ecs_path: &Path,
reflect_trait: &'static str,
) -> Option<proc_macro2::TokenStream> {
// Generics require the generic parameters to implement Reflect
// This is unsupported for now.
if !cfg!(feature = "bevy_reflect") || !ast.generics.params.is_empty() {
return None;
}

ast.attrs.iter().any(|attr| {
if !attr.path().is_ident(REFLECT) || !matches!(attr.meta, Meta::List(_)) {
return false;
}

attr.parse_nested_meta(|meta| {
meta.path.is_ident(reflect_trait)
.then_some(())
.ok_or_else(|| meta.error("missing required reflect attribute"))
})
.is_ok()
})
.then(|| quote! {
#[doc(hidden)]
fn __register_type(registry: &#bevy_ecs_path::private::bevy_reflect::TypeRegistryArc) {
#bevy_ecs_path::private::bevy_reflect::register_type_shim::<Self>(registry);
}
})
}

fn storage_path(bevy_ecs_path: &Path, ty: StorageTy) -> TokenStream2 {
let storage_type = match ty {
StorageTy::Table => Ident::new("Table", Span::call_site()),
Expand Down
78 changes: 68 additions & 10 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
pub use bevy_ecs_macros::Component;
use bevy_ptr::{OwningPtr, UnsafeCellDeref};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
use bevy_reflect::{Reflect, TypeRegistryArc};
use bevy_utils::TypeIdMap;
use std::cell::UnsafeCell;
use std::{
Expand Down Expand Up @@ -156,6 +156,13 @@ pub trait Component: Send + Sync + 'static {

/// Called when registering this component, allowing mutable access to it's [`ComponentHooks`].
fn register_component_hooks(_hooks: &mut ComponentHooks) {}

/// Shim for automatically registering components. Intentionally hidden as it's not part of the
/// public interface. Only available if the `bevy_reflect` feature is enabled.
#[doc(hidden)]
#[allow(unused_variables)]
#[cfg(feature = "bevy_reflect")]
fn __register_type(registry: &TypeRegistryArc) {}
}

/// The storage used for a specific component type.
Expand Down Expand Up @@ -525,6 +532,8 @@ pub struct Components {
components: Vec<ComponentInfo>,
indices: TypeIdMap<ComponentId>,
resource_indices: TypeIdMap<ComponentId>,
#[cfg(feature = "bevy_reflect")]
pub(crate) type_registry: TypeRegistryArc,
}

impl Components {
Expand Down Expand Up @@ -552,6 +561,8 @@ impl Components {
ComponentDescriptor::new::<T>(),
);
T::register_component_hooks(&mut components[index.index()].hooks);
#[cfg(feature = "bevy_reflect")]
T::__register_type(&self.type_registry);
index
})
}
Expand Down Expand Up @@ -720,9 +731,16 @@ impl Components {
pub fn init_resource<T: Resource>(&mut self) -> ComponentId {
// SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.get_or_insert_resource_with(TypeId::of::<T>(), || {
ComponentDescriptor::new_resource::<T>()
})
Self::get_or_insert_resource_with(
&mut self.components,
&mut self.resource_indices,
TypeId::of::<T>(),
|| {
#[cfg(feature = "bevy_reflect")]
T::__register_type(&self.type_registry);
ComponentDescriptor::new_resource::<T>()
},
)
}
}

Expand All @@ -733,9 +751,12 @@ impl Components {
pub fn init_non_send<T: Any>(&mut self) -> ComponentId {
// SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.get_or_insert_resource_with(TypeId::of::<T>(), || {
ComponentDescriptor::new_non_send::<T>(StorageType::default())
})
Self::get_or_insert_resource_with(
&mut self.components,
&mut self.resource_indices,
TypeId::of::<T>(),
|| ComponentDescriptor::new_non_send::<T>(StorageType::default()),
)
}
}

Expand All @@ -744,12 +765,12 @@ impl Components {
/// The [`ComponentDescriptor`] must match the [`TypeId`]
#[inline]
unsafe fn get_or_insert_resource_with(
&mut self,
components: &mut Vec<ComponentInfo>,
resource_indices: &mut TypeIdMap<ComponentId>,
type_id: TypeId,
func: impl FnOnce() -> ComponentDescriptor,
) -> ComponentId {
let components = &mut self.components;
*self.resource_indices.entry(type_id).or_insert_with(|| {
*resource_indices.entry(type_id).or_insert_with(|| {
let descriptor = func();
let component_id = ComponentId(components.len());
components.push(ComponentInfo::new(component_id, descriptor));
Expand Down Expand Up @@ -971,3 +992,40 @@ impl<T: Component> FromWorld for InitComponentId<T> {
}
}
}

#[cfg(test)]
#[cfg(feature = "bevy_reflect")]
mod test {
use crate as bevy_ecs;
use crate::{
component::Components, prelude::Component, prelude::Resource, reflect::ReflectComponent,
reflect::ReflectResource, storage::Storages,
};
use bevy_reflect::Reflect;
use core::any::TypeId;

#[test]
fn init_component_registers_component() {
#[derive(Component, Reflect)]
#[reflect(Component)]
struct A(usize);

let mut components = Components::default();
let mut storages = Storages::default();
components.init_component::<A>(&mut storages);

assert!(components.type_registry.read().contains(TypeId::of::<A>()));
}

#[test]
fn init_resource_registers_resource() {
#[derive(Resource, Reflect)]
#[reflect(Resource)]
struct A(usize);

let mut components = Components::default();
components.init_resource::<A>();

assert!(components.type_registry.read().contains(TypeId::of::<A>()));
}
}
9 changes: 9 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ pub mod prelude {
};
}

#[cfg(feature = "bevy_reflect")]
#[doc(hidden)]
pub mod private {
pub mod bevy_reflect {
pub use crate::reflect::register_type_shim;
pub use bevy_reflect::TypeRegistryArc;
}
}

#[cfg(test)]
mod tests {
use crate as bevy_ecs;
Expand Down
13 changes: 3 additions & 10 deletions crates/bevy_ecs/src/reflect/entity_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,13 +338,12 @@ mod tests {
fn insert_reflected() {
let mut world = World::new();

let type_registry = AppTypeRegistry::default();
world.init_resource::<AppTypeRegistry>();
{
let mut registry = type_registry.write();
let mut registry = world.resource::<AppTypeRegistry>().write();
registry.register::<ComponentA>();
registry.register_type_data::<ComponentA, ReflectComponent>();
}
world.insert_resource(type_registry);

let mut system_state: SystemState<Commands> = SystemState::new(&mut world);
let mut commands = system_state.get_mut(&mut world);
Expand Down Expand Up @@ -405,13 +404,7 @@ mod tests {
fn remove_reflected() {
let mut world = World::new();

let type_registry = AppTypeRegistry::default();
{
let mut registry = type_registry.write();
registry.register::<ComponentA>();
registry.register_type_data::<ComponentA, ReflectComponent>();
}
world.insert_resource(type_registry);
world.init_resource::<AppTypeRegistry>();

let mut system_state: SystemState<Commands> = SystemState::new(&mut world);
let mut commands = system_state.get_mut(&mut world);
Expand Down
78 changes: 74 additions & 4 deletions crates/bevy_ecs/src/reflect/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
//! Types that enable reflection support.
//!
//! # Automatic Reflect Type Registration
//! The [`Component`] and [`Resource`] resource derive macros will automatically register
//! its types that implement [`Reflect`] into the [`AppTypeRegistry`] resource when first
//! seen by the ECS [`World`].
//!
//! If you find that your component or resource is not registered, they may need to be manually
//! registered. There are a few exceptions:
//!
//! * Automatic registration is only supported via the derive macros. Manual implementations of
//! `Component`, `Resource`, or `Reflect` must be manually registered.
//! * The associated ECS trait must be reflected (via `reflect(Component)` or `reflect(Resource)`).
Copy link
Contributor

Choose a reason for hiding this comment

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

This requirement is so that we don't automatically register to the World non-ECS-related Reflect types?

Copy link
Member Author

Choose a reason for hiding this comment

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

This was requested so that users could opt out of this, such as avoiding serialization in scenes. I personally think we should find other ways of opting out of each use case and just move towards registration by default.

Copy link
Member

Choose a reason for hiding this comment

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

We could also consider an opt-out attribute. There could be other reasons to exclude a type from the registry besides opting out of scenes. For larger projects, you may only want to register types you know you’ll need (to save on memory and reduce startup times). You might also be using a third party crate that uses the registry for things you want to opt out of as well. Or maybe you don't want to "leak" type information to downstream crates.

Whatever the reason, we need a way to opt out of automatic registration since registration is intentionally a one-way process: you can't undo or remove a registration. This is so that guarantees about the registered type are never broken without the user being aware of it (unless, of course, a mischievous plugin goes nuclear and replaces the entire AppTypeRegistry resource but that's a whole separate issue haha).

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for the opt-out attribute. It's more explicit, and allows this to work even when the user forgets to reflect Component/Resource

//! * Generic types are not supported, and must be manually registered.
//! * Types are registered when the World first initializes the type. This may cause registrations
//! to be missing due to mistiming. These initialization points include but are not limited to:
//! - spawning an entity with the component or inserting the resource
//! - inserting the component existing entity
//! - attempting to remove the component or resource, even if it's not present.
//! - a system that references the component or resource is added to a schedule
//!
//! ```rust
//! use bevy_ecs::prelude::*;
//! use bevy_reflect::Reflect;
//!
//! // This will automatically register upon first use!
//! #[derive(Component, Reflect)]
//! #[reflect(Component)]
//! pub struct MyComponent {
//! a: usize,
//! b: (u32, u8)
//! }
//!
//! // This won't!
//! #[derive(Component, Reflect)]
//! #[reflect(Component)]
//! pub struct GenericComponent<T>(T);
//!
//! // This won't!
//! #[derive(Component, Reflect)]
//! pub struct NoReflectComponent;
//! ```
//!
//! [`Component`]: crate::prelude::Component
//! [`Resource`]: crate::prelude::Resource

use std::any::TypeId;
use std::ops::{Deref, DerefMut};

use crate as bevy_ecs;
use crate::{system::Resource, world::World};
use bevy_reflect::{FromReflect, Reflect, TypeRegistry, TypeRegistryArc};
use crate::{
system::Resource,
world::{FromWorld, World},
};
use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypeRegistry, TypeRegistryArc};

mod bundle;
mod component;
Expand All @@ -21,10 +68,27 @@ pub use from_world::{ReflectFromWorld, ReflectFromWorldFns};
pub use map_entities::ReflectMapEntities;
pub use resource::{ReflectResource, ReflectResourceFns};

#[doc(hidden)]
pub fn register_type_shim<T: GetTypeRegistration>(registry: &TypeRegistryArc) {
if let Ok(mut registry) = registry.internal.try_write() {
registry.register::<T>();
return;
}
if let Ok(registry) = registry.internal.try_read() {
if registry.contains(::core::any::TypeId::of::<T>()) {
return;
}
}
panic!(
"Deadlock while registering <{}>.",
::std::any::type_name::<T>()
);
}

/// A [`Resource`] storing [`TypeRegistry`] for
/// type registrations relevant to a whole app.
#[derive(Resource, Clone, Default)]
pub struct AppTypeRegistry(pub TypeRegistryArc);
#[derive(Resource, Clone, Debug)]
pub struct AppTypeRegistry(TypeRegistryArc);

impl Deref for AppTypeRegistry {
type Target = TypeRegistryArc;
Expand All @@ -42,6 +106,12 @@ impl DerefMut for AppTypeRegistry {
}
}

impl FromWorld for AppTypeRegistry {
fn from_world(world: &mut World) -> Self {
Self(world.type_registry().clone())
}
}

/// Creates a `T` from a `&dyn Reflect`.
///
/// The first approach uses `T`'s implementation of `FromReflect`.
Expand Down
Loading