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

Add Immutable Component Support #16372

Merged
merged 34 commits into from
Dec 5, 2024

Conversation

bushrat011899
Copy link
Contributor

@bushrat011899 bushrat011899 commented Nov 13, 2024

Objective

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:

#[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.

Using immutable components as an index
/// 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));

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:
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)]:

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

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

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:

// 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.

The `Component` trait now implies an immutable, readonly component. To add mutability, you must implement `ComponentMut`, which is a simple marker. In the derive macro, `ComponentMut` will be implemented with `Component` unless you add an `#[immutable]` attribute.
@bushrat011899 bushrat011899 added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events A-Reflection Runtime information about types X-Uncontroversial This work is generally agreed upon S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Needs-SME Decision or review from an SME is required D-Macros Code that generates Rust code labels Nov 13, 2024
@bushrat011899 bushrat011899 added this to the 0.16 milestone Nov 13, 2024
@bushrat011899 bushrat011899 added the M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide label Nov 13, 2024
@BenjaminBrienen
Copy link
Contributor

The behavior of implementing Component before this change and the behavior of implementing Component + ComponentMut after this change should be identical. Do I understand that correctly?

@bushrat011899
Copy link
Contributor Author

Yes a pre-this-PR Component is identical to a this-PR Component + ComponentMut. Component contains all the implementation details it had previously, but now only implies an immutable type. Mutability is now explicitly stated by implementing ComponentMut. But for the derive macro, Component + ComponentMut are implemented by default (since that is the most typical use-case). To opt-out of mutability in the derive macro, you add #[immutable].

@ItsDoot
Copy link
Contributor

ItsDoot commented Nov 13, 2024

Small nit: I would prefer #[component(immutable)] to keep all component attributes together. It also follows #[world_query(mutable)].

@bushrat011899
Copy link
Contributor Author

I've updated the macro to instead use #[component(immutable)]. It's much clearer what's happening and should be cleaner too. Good suggestion @ItsDoot.

@bushrat011899
Copy link
Contributor Author

Of note, FilteredEntityMut::get_mut_by_id is (so far) the only safe method I have found that can bypass immutable components. I did want to add the immutable flag to ComponentDescriptor, but propagating that information proved very challenging. If anyone has a suggestion for how to integrate ComponentMut and ComponentDescriptor in the least impactful way I would be greatly appreciative.

@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Nov 13, 2024
@alice-i-cecile
Copy link
Member

alice-i-cecile commented Nov 13, 2024

Why do you prefer the ComponentMut: Component design over a Mutable + Component design? I have a mild preference for the latter because I think it'll be easier to extend to resources. Broadly happy with this otherwise though, although I do think the reflection and dynamic component stories should probably be improved 🤔

@alice-i-cecile alice-i-cecile added S-Needs-Help The author needs help finishing this PR. and removed S-Needs-SME Decision or review from an SME is required labels Nov 13, 2024
@NthTensor
Copy link
Contributor

Don't have time to look over this fully, but I like this. I also prefer the version without the trait bound on component.

Just so I am sure this can be used as I want, if we make parent/children immutable, how do we preserve the existing hierarchy commands api? Will we use unsafe within the commands to get mutable access, or go properly immutable with only clones and inserts?

@MrGVSV
Copy link
Member

MrGVSV commented Nov 13, 2024

Just so I am sure this can be used as I want, if we make parent/children immutable, how do we preserve the existing hierarchy commands api? Will we use unsafe within the commands to get mutable access, or go properly immutable with only clones and inserts?

I was wondering if it would make sense to have mutable component access require a key type. Then crates could keep that type private to simulate immutability while still being able to mutate the component themselves.

Not sure if that's possible and I don't know how well it fits with this approach, but possibly an option (though I’m going to guess far more complex and involved).

@iiYese
Copy link
Contributor

iiYese commented Nov 13, 2024

or go properly immutable with only clones and inserts

It would be this. Either through Parent's on insert hook or a command.

Copy link
Contributor

@nakedible nakedible left a comment

Choose a reason for hiding this comment

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

I'm very happy with this pull in general.

crates/bevy_ecs/src/reflect/component.rs Outdated Show resolved Hide resolved
examples/ecs/immutable_components.rs Outdated Show resolved Hide resolved
alice-i-cecile and others added 4 commits November 17, 2024 08:48
Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
`take`/`apply`/`insert` only makes sense for immutable components, but this code-path explicitly forbids them.

Co-Authored-By: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-Authored-By: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-Authored-By: Nuutti Kotivuori <naked@iki.fi>
bushrat011899 and others added 2 commits November 18, 2024 07:42
Co-Authored-By: Gino Valente <49806985+MrGVSV@users.noreply.github.com>
Co-Authored-By: Benjamin Brienen <benjamin.brienen@outlook.com>
@@ -180,6 +188,10 @@ impl ReflectComponent {
}

/// Gets the value of this [`Component`] type from the entity as a mutable reflected reference.
///
/// # Panics
Copy link
Member

Choose a reason for hiding this comment

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

Follow-up: I would prefer to make these all return Results instead of panicking.

@@ -265,6 +281,8 @@ impl ReflectComponent {

impl<C: Component + Reflect + TypePath> FromType<C> for ReflectComponent {
fn from_type() -> Self {
// TODO: Currently we panic if a component is immutable and you use
Copy link
Member

Choose a reason for hiding this comment

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

Strongly agree!

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Strongly in favor of this, and it's really well-made. I have a few small complaints I'd like to see done here, but once those are resolved I'm very happy to merge it.

I'd also like to merge the immutable_components_dynamic example into the immutable_components example, possibly in a module. We have too many examples already lol.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

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

I think this is the best path forward.

Worth noting that the need to manually specify mutability could go away if associated type defaults ever lands.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 5, 2024
Merged via the queue into bevyengine:main with commit a35811d Dec 5, 2024
31 of 32 checks passed
ecoskey pushed a commit to ecoskey/bevy that referenced this pull request Jan 6, 2025
# Objective

- Fixes bevyengine#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Reflection Runtime information about types C-Feature A new feature, making something new possible D-Macros Code that generates Rust code M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow certain components to be marked immutable