Skip to content

Commit

Permalink
Add Immutable Component Support (#16372)
Browse files Browse the repository at this point in the history
# Objective

- Fixes #16208

## Solution

- Added an associated type to `Component`, `Mutability`, which flags
whether a component is mutable, or immutable. If `Mutability= Mutable`,
the component is mutable. If `Mutability= Immutable`, the component is
immutable.
- Updated `derive_component` to default to mutable unless an
`#[component(immutable)]` attribute is added.
- Updated `ReflectComponent` to check if a component is mutable and, if
not, panic when attempting to mutate.

## Testing

- CI
- `immutable_components` example.

---

## Showcase

Users can now mark a component as `#[component(immutable)]` to prevent
safe mutation of a component while it is attached to an entity:

```rust
#[derive(Component)]
#[component(immutable)]
struct Foo {
    // ...
}
```

This prevents creating an exclusive reference to the component while it
is attached to an entity. This is particularly powerful when combined
with component hooks, as you can now fully track a component's value,
ensuring whatever invariants you desire are upheld. Before this would be
done my making a component private, and manually creating a `QueryData`
implementation which only permitted read access.

<details>
  <summary>Using immutable components as an index</summary>
  
```rust
/// This is an example of a component like [`Name`](bevy::prelude::Name), but immutable.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Component)]
#[component(
    immutable,
    on_insert = on_insert_name,
    on_replace = on_replace_name,
)]
pub struct Name(pub &'static str);

/// This index allows for O(1) lookups of an [`Entity`] by its [`Name`].
#[derive(Resource, Default)]
struct NameIndex {
    name_to_entity: HashMap<Name, Entity>,
}

impl NameIndex {
    fn get_entity(&self, name: &'static str) -> Option<Entity> {
        self.name_to_entity.get(&Name(name)).copied()
    }
}

fn on_insert_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) {
    let Some(&name) = world.entity(entity).get::<Name>() else {
        unreachable!()
    };
    let Some(mut index) = world.get_resource_mut::<NameIndex>() else {
        return;
    };

    index.name_to_entity.insert(name, entity);
}

fn on_replace_name(mut world: DeferredWorld<'_>, entity: Entity, _component: ComponentId) {
    let Some(&name) = world.entity(entity).get::<Name>() else {
        unreachable!()
    };
    let Some(mut index) = world.get_resource_mut::<NameIndex>() else {
        return;
    };

    index.name_to_entity.remove(&name);
}

// Setup our name index
world.init_resource::<NameIndex>();

// Spawn some entities!
let alyssa = world.spawn(Name("Alyssa")).id();
let javier = world.spawn(Name("Javier")).id();

// Check our index
let index = world.resource::<NameIndex>();

assert_eq!(index.get_entity("Alyssa"), Some(alyssa));
assert_eq!(index.get_entity("Javier"), Some(javier));

// Changing the name of an entity is also fully capture by our index
world.entity_mut(javier).insert(Name("Steven"));

// Javier changed their name to Steven
let steven = javier;

// Check our index
let index = world.resource::<NameIndex>();

assert_eq!(index.get_entity("Javier"), None);
assert_eq!(index.get_entity("Steven"), Some(steven));
```
  
</details>

Additionally, users can use `Component<Mutability = ...>` in trait
bounds to enforce that a component _is_ mutable or _is_ immutable. When
using `Component` as a trait bound without specifying `Mutability`, any
component is applicable. However, methods which only work on mutable or
immutable components are unavailable, since the compiler must be
pessimistic about the type.

## Migration Guide

- When implementing `Component` manually, you must now provide a type
for `Mutability`. The type `Mutable` provides equivalent behaviour to
earlier versions of `Component`:
```rust
impl Component for Foo {
    type Mutability = Mutable;
    // ...
}
```
- When working with generic components, you may need to specify that
your generic parameter implements `Component<Mutability = Mutable>`
rather than `Component` if you require mutable access to said component.
- The entity entry API has had to have some changes made to minimise
friction when working with immutable components. Methods which
previously returned a `Mut<T>` will now typically return an
`OccupiedEntry<T>` instead, requiring you to add an `into_mut()` to get
the `Mut<T>` item again.

## Draft Release Notes

Components can now be made immutable while stored within the ECS.

Components are the fundamental unit of data within an ECS, and Bevy
provides a number of ways to work with them that align with Rust's rules
around ownership and borrowing. One part of this is hooks, which allow
for defining custom behavior at key points in a component's lifecycle,
such as addition and removal. However, there is currently no way to
respond to _mutation_ of a component using hooks. The reasons for this
are quite technical, but to summarize, their addition poses a
significant challenge to Bevy's core promises around performance.
Without mutation hooks, it's relatively trivial to modify a component in
such a way that breaks invariants it intends to uphold. For example, you
can use `core::mem::swap` to swap the components of two entities,
bypassing the insertion and removal hooks.

This means the only way to react to this modification is via change
detection in a system, which then begs the question of what happens
_between_ that alteration and the next run of that system?
Alternatively, you could make your component private to prevent
mutation, but now you need to provide commands and a custom `QueryData`
implementation to allow users to interact with your component at all.

Immutable components solve this problem by preventing the creation of an
exclusive reference to the component entirely. Without an exclusive
reference, the only way to modify an immutable component is via removal
or replacement, which is fully captured by component hooks. To make a
component immutable, simply add `#[component(immutable)]`:

```rust
#[derive(Component)]
#[component(immutable)]
struct Foo {
    // ...
}
```

When implementing `Component` manually, there is an associated type
`Mutability` which controls this behavior:

```rust
impl Component for Foo {
    type Mutability = Mutable;
    // ...
}
```

Note that this means when working with generic components, you may need
to specify that a component is mutable to gain access to certain
methods:

```rust
// Before
fn bar<C: Component>() {
    // ...
}

// After
fn bar<C: Component<Mutability = Mutable>>() {
    // ...
}
```

With this new tool, creating index components, or caching data on an
entity should be more user friendly, allowing libraries to provide APIs
relying on components and hooks to uphold their invariants.

## Notes

- ~~I've done my best to implement this feature, but I'm not happy with
how reflection has turned out. If any reflection SMEs know a way to
improve this situation I'd greatly appreciate it.~~ There is an
outstanding issue around the fallibility of mutable methods on
`ReflectComponent`, but the DX is largely unchanged from `main` now.
- I've attempted to prevent all safe mutable access to a component that
does not implement `Component<Mutability = Mutable>`, but there may
still be some methods I have missed. Please indicate so and I will
address them, as they are bugs.
- Unsafe is an escape hatch I am _not_ attempting to prevent. Whatever
you do with unsafe is between you and your compiler.
- I am marking this PR as ready, but I suspect it will undergo fairly
major revisions based on SME feedback.
- I've marked this PR as _Uncontroversial_ based on the feature, not the
implementation.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Benjamin Brienen <benjamin.brienen@outlook.com>
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-authored-by: Nuutti Kotivuori <naked@iki.fi>
  • Loading branch information
5 people authored Dec 5, 2024
1 parent b7bcd31 commit a35811d
Show file tree
Hide file tree
Showing 27 changed files with 585 additions and 132 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,17 @@ description = "Creates a hierarchy of parents and children entities"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "immutable_components"
path = "examples/ecs/immutable_components.rs"
doc-scrape-examples = true

[package.metadata.example.immutable_components]
name = "Immutable Components"
description = "Demonstrates the creation and utility of immutable components"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "iter_combinations"
path = "examples/ecs/iter_combinations.rs"
Expand Down
12 changes: 6 additions & 6 deletions benches/benches/bevy_ecs/change_detection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bevy_ecs::{
component::Component,
component::{Component, Mutable},
entity::Entity,
prelude::{Added, Changed, EntityWorldMut, QueryState},
query::QueryFilter,
Expand Down Expand Up @@ -124,7 +124,7 @@ fn all_added_detection(criterion: &mut Criterion) {
}
}

fn all_changed_detection_generic<T: Component + Default + BenchModify>(
fn all_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -172,7 +172,7 @@ fn all_changed_detection(criterion: &mut Criterion) {
}
}

fn few_changed_detection_generic<T: Component + Default + BenchModify>(
fn few_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -222,7 +222,7 @@ fn few_changed_detection(criterion: &mut Criterion) {
}
}

fn none_changed_detection_generic<T: Component + Default>(
fn none_changed_detection_generic<T: Component<Mutability = Mutable> + Default>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -271,7 +271,7 @@ fn insert_if_bit_enabled<const B: u16>(entity: &mut EntityWorldMut, i: u16) {
}
}

fn add_archetypes_entities<T: Component + Default>(
fn add_archetypes_entities<T: Component<Mutability = Mutable> + Default>(
world: &mut World,
archetype_count: u16,
entity_count: u32,
Expand All @@ -298,7 +298,7 @@ fn add_archetypes_entities<T: Component + Default>(
}
}
}
fn multiple_archetype_none_changed_detection_generic<T: Component + Default + BenchModify>(
fn multiple_archetype_none_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
archetype_count: u16,
entity_count: u32,
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_animation/src/animation_curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ use core::{
marker::PhantomData,
};

use bevy_ecs::component::Component;
use bevy_ecs::component::{Component, Mutable};
use bevy_math::curve::{
cores::{UnevenCore, UnevenCoreError},
iterable::IterableCurve,
Expand Down Expand Up @@ -221,7 +221,7 @@ pub struct AnimatedField<C, A, F: Fn(&mut C) -> &mut A> {

impl<C, A, F> AnimatableProperty for AnimatedField<C, A, F>
where
C: Component,
C: Component<Mutability = Mutable>,
A: Animatable + Clone + Sync + Debug,
F: Fn(&mut C) -> &mut A + Send + Sync + 'static,
{
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/oit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl Default for OrderIndependentTransparencySettings {
// we can hook on_add to issue a warning in case `layer_count` is seemingly too high.
impl Component for OrderIndependentTransparencySettings {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|world, entity, _| {
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub fn derive_event(input: TokenStream) -> TokenStream {

impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
type Mutability = #bevy_ecs_path::component::Mutable;
}
})
}
Expand Down Expand Up @@ -127,11 +128,17 @@ 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 mutable_type = attrs
.immutable
.then_some(quote! { #bevy_ecs_path::component::Immutable })
.unwrap_or(quote! { #bevy_ecs_path::component::Mutable });

// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
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;
type Mutability = #mutable_type;
fn register_required_components(
requiree: #bevy_ecs_path::component::ComponentId,
components: &mut #bevy_ecs_path::component::Components,
Expand Down Expand Up @@ -191,13 +198,16 @@ pub const ON_INSERT: &str = "on_insert";
pub const ON_REPLACE: &str = "on_replace";
pub const ON_REMOVE: &str = "on_remove";

pub const IMMUTABLE: &str = "immutable";

struct Attrs {
storage: StorageTy,
requires: Option<Punctuated<Require, Comma>>,
on_add: Option<ExprPath>,
on_insert: Option<ExprPath>,
on_replace: Option<ExprPath>,
on_remove: Option<ExprPath>,
immutable: bool,
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -228,6 +238,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
on_replace: None,
on_remove: None,
requires: None,
immutable: false,
};

let mut require_paths = HashSet::new();
Expand Down Expand Up @@ -257,6 +268,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
} else if nested.path.is_ident(ON_REMOVE) {
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(IMMUTABLE) {
attrs.immutable = true;
Ok(())
} else {
Err(nested.error("Unsupported attribute"))
}
Expand Down
88 changes: 88 additions & 0 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,18 @@ pub use bevy_ecs_macros::require;
///
/// # Component and data access
///
/// Components can be marked as immutable by adding the `#[component(immutable)]`
/// attribute when using the derive macro.
/// See the documentation for [`ComponentMutability`] for more details around this
/// feature.
///
/// See the [`entity`] module level documentation to learn how to add or remove components from an entity.
///
/// See the documentation for [`Query`] to learn how to access component data from a system.
///
/// [`entity`]: crate::entity#usage
/// [`Query`]: crate::system::Query
/// [`ComponentMutability`]: crate::component::ComponentMutability
///
/// # Choosing a storage type
///
Expand Down Expand Up @@ -380,6 +386,14 @@ pub trait Component: Send + Sync + 'static {
/// A constant indicating the storage type used for this component.
const STORAGE_TYPE: StorageType;

/// A marker type to assist Bevy with determining if this component is
/// mutable, or immutable. Mutable components will have [`Component<Mutability = Mutable>`],
/// while immutable components will instead have [`Component<Mutability = Immutable>`].
///
/// * For a component to be mutable, this type must be [`Mutable`].
/// * For a component to be immutable, this type must be [`Immutable`].
type Mutability: ComponentMutability;

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

Expand All @@ -401,6 +415,61 @@ pub trait Component: Send + Sync + 'static {
}
}

mod private {
pub trait Seal {}
}

/// The mutability option for a [`Component`]. This can either be:
/// * [`Mutable`]
/// * [`Immutable`]
///
/// This is controlled through either [`Component::Mutability`] or `#[component(immutable)]`
/// when using the derive macro.
///
/// Immutable components are guaranteed to never have an exclusive reference,
/// `&mut ...`, created while inserted onto an entity.
/// In all other ways, they are identical to mutable components.
/// This restriction allows hooks to observe all changes made to an immutable
/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a
/// `OnMutate` hook.
/// This is not practical for mutable components, as the runtime cost of invoking
/// a hook for every exclusive reference created would be far too high.
///
/// # Examples
///
/// ```rust
/// # use bevy_ecs::component::Component;
/// #
/// #[derive(Component)]
/// #[component(immutable)]
/// struct ImmutableFoo;
/// ```
pub trait ComponentMutability: private::Seal + 'static {
/// Boolean to indicate if this mutability setting implies a mutable or immutable
/// component.
const MUTABLE: bool;
}

/// Parameter indicating a [`Component`] is immutable.
///
/// See [`ComponentMutability`] for details.
pub struct Immutable;

impl private::Seal for Immutable {}
impl ComponentMutability for Immutable {
const MUTABLE: bool = false;
}

/// Parameter indicating a [`Component`] is mutable.
///
/// See [`ComponentMutability`] for details.
pub struct Mutable;

impl private::Seal for Mutable {}
impl ComponentMutability for Mutable {
const MUTABLE: bool = true;
}

/// The storage used for a specific component type.
///
/// # Examples
Expand Down Expand Up @@ -626,6 +695,12 @@ impl ComponentInfo {
&self.descriptor.name
}

/// Returns `true` if the current component is mutable.
#[inline]
pub fn mutable(&self) -> bool {
self.descriptor.mutable
}

/// Returns the [`TypeId`] of the underlying component type.
/// Returns `None` if the component does not correspond to a Rust type.
#[inline]
Expand Down Expand Up @@ -778,6 +853,7 @@ pub struct ComponentDescriptor {
// this descriptor describes.
// None if the underlying type doesn't need to be dropped
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
}

// We need to ignore the `drop` field in our `Debug` impl
Expand All @@ -789,6 +865,7 @@ impl Debug for ComponentDescriptor {
.field("is_send_and_sync", &self.is_send_and_sync)
.field("type_id", &self.type_id)
.field("layout", &self.layout)
.field("mutable", &self.mutable)
.finish()
}
}
Expand All @@ -813,6 +890,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: T::Mutability::MUTABLE,
}
}

Expand All @@ -826,6 +904,7 @@ impl ComponentDescriptor {
storage_type: StorageType,
layout: Layout,
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
) -> Self {
Self {
name: name.into(),
Expand All @@ -834,6 +913,7 @@ impl ComponentDescriptor {
type_id: None,
layout,
drop,
mutable,
}
}

Expand All @@ -850,6 +930,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
}
}

Expand All @@ -861,6 +942,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
}
}

Expand All @@ -882,6 +964,12 @@ impl ComponentDescriptor {
pub fn name(&self) -> &str {
self.name.as_ref()
}

/// Returns whether this component is mutable.
#[inline]
pub fn mutable(&self) -> bool {
self.mutable
}
}

/// Function type that can be used to clone an entity.
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ impl EntityCloner {
/// Here's an example of how to do it using [`get_component_clone_handler`](Component::get_component_clone_handler):
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::component::{StorageType, component_clone_via_clone, ComponentCloneHandler};
/// # use bevy_ecs::component::{StorageType, component_clone_via_clone, ComponentCloneHandler, Mutable};
/// #[derive(Clone)]
/// struct SomeComponent;
///
/// impl Component for SomeComponent {
/// const STORAGE_TYPE: StorageType = StorageType::Table;
/// type Mutability = Mutable;
/// fn get_component_clone_handler() -> ComponentCloneHandler {
/// ComponentCloneHandler::Custom(component_clone_via_clone::<Self>)
/// }
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/observer/entity_observer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
component::{Component, ComponentCloneHandler, ComponentHooks, StorageType},
component::{Component, ComponentCloneHandler, ComponentHooks, Mutable, StorageType},
entity::{Entity, EntityCloneBuilder, EntityCloner},
observer::ObserverState,
world::{DeferredWorld, World},
Expand All @@ -11,6 +11,7 @@ pub(crate) struct ObservedBy(pub(crate) Vec<Entity>);

impl Component for ObservedBy {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_remove(|mut world, entity, _| {
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/observer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ impl World {
// Populate ObservedBy for each observed entity.
for watched_entity in &(*observer_state).descriptor.entities {
let mut entity_mut = self.entity_mut(*watched_entity);
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default();
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default().into_mut();
observed_by.0.push(observer_entity);
}
(&*observer_state, &mut self.archetypes, &mut self.observers)
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_ecs/src/observer/runner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::any::Any;

use crate::{
component::{ComponentHook, ComponentHooks, ComponentId, StorageType},
component::{ComponentHook, ComponentHooks, ComponentId, Mutable, StorageType},
observer::{ObserverDescriptor, ObserverTrigger},
prelude::*,
query::DebugCheckedUnwrap,
Expand Down Expand Up @@ -62,6 +62,7 @@ impl ObserverState {

impl Component for ObserverState {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|mut world, entity, _| {
Expand Down Expand Up @@ -314,6 +315,7 @@ impl Observer {

impl Component for Observer {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;
fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|world, entity, _id| {
let Some(observe) = world.get::<Self>(entity) else {
Expand Down
Loading

0 comments on commit a35811d

Please sign in to comment.