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

Allow certain components to be marked immutable #16208

Closed
nakedible opened this issue Nov 1, 2024 · 8 comments · Fixed by #16372
Closed

Allow certain components to be marked immutable #16208

nakedible opened this issue Nov 1, 2024 · 8 comments · Fixed by #16372
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Design This issue requires design work to think about how it would best be accomplished

Comments

@nakedible
Copy link
Contributor

What problem does this solve or what need does it fill?

There are needs to enforce invariants, to build indexes or otherwise keep things consistent in the ECS between multiple entities or between multiple components in a single entity. The new "component hooks" system goes a long way towards making it easy to keep things in sync, as they can be triggered on component addition, removal and replace.

However, the hooks system does not catch mutation currently, and catching mutation has a lot of difficult problems to solve if it needs to be synchronously and with every way of mutating a component. Even if all the fields inside a component are marked private, that doesn't prevent the user from assigning a new instance of that component to it, overwriting the data for the old component - and no hooks will be fired, only asynchronous change events. This means that it is currently impossible to use hooks to maintain an invariant if the user mutates components directly.

What solution would you like?

There could be a flag on components that marks the component immutable. When set, it would mean that no API will return mutable references to the component, making it impossible to mutate the values in the component. This doesn't mean that there would be any change in how insert, remove, take etc. would work, so it would still be fully permitted to remove the component, or add a new one, or replace an existing with a new one. The current hooks system will fire on_replace and on_insert hooks when a component is replaced this way instead of mutated in-place.

How the mutation APIs should behave when called on a component that is marked immutable is still unspecified. Perhaps they should panic when trying to mutate a component that can't be mutated. Or maybe they act as if the component doesn't exist on the object if attempting to get a mutable reference. Or perhaps this can somehow be extended all the way to the type system so the calls which attempt to take a mutable reference to a component simply won't compile because those are not implemented for components which are immutable.

What alternative(s) have you considered?

The big alternative is that absolutely no change is needed. Preventing mutation is just a safeguard for certain kinds of components, and if the fields of the component are private, doing a mutable assignment on a component is not easy to accidentally do – so for most usages it's probably enough to just say in documentation "please don't mutate this component".

The second alternative is also that absolutely no change is needed. Such components may be made private, and exposed through QueryData and Bundle in interfaces such that the real component is never accessible, hence never mutated. It might still be accessible via reflection, save/load, etc. but all bets are off there anyway. This is not as nice as it requires a bunch of boilerplate, and the whole point of component hooks was to allow such invariants to be implemented without creating a shadow API to do all the ECS operations while keeping invariants.

The third alternative is to somehow make synchronous change detection hooks work. There is already change detection, so it's not so impossible to think that it could run a hook real time, possibly both before change and after a change. Then we could just have on_mutate and that would probably cover the vast majority of cases that would benefit from immutable components.

Additional context

I'm writing this issue mainly as a placeholder, and to see comments. It's one problem related to hooks that may or may not be common enough to warrant doing something about it. If there comes a strong use case for this that isn't easily solved by other means, and this problem is the primary motivation to make components private, then I will likely want to revisit this.

@nakedible nakedible added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels Nov 1, 2024
@iiYese iiYese added A-ECS Entities, components, systems, and events D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes and removed S-Needs-Triage This issue needs to be labelled labels Nov 1, 2024
@bushrat011899
Copy link
Contributor

The status-quo for this is to create a private Component and a public QueryData, but that doesn't allow replacing the value (instead you'd need to also add some commands). Supporting an immutable Component would eliminate this boilerplate.

Ideally, it would be a compiler error to ask for a mutable reference to an immutable component. Perhaps Component as a trait could be modified to include a parameter indicating mutability, and the mutable QueryData implementation would not be available for said parameter? Or Component could be split into a Component and ComponentMut pair of traits?

@iiYese
Copy link
Contributor

iiYese commented Nov 1, 2024

It could be as simple as the Component derive also adding an impl for a Mutable trait. You can have a #[immutable] attribute macro that opts out of the Mutable impl. Then just:

impl<T: Component + Mutable> QueryData for &'_ mut T {
    // ..
}

@iiYese iiYese added the S-Needs-Design This issue requires design work to think about how it would best be accomplished label Nov 1, 2024
@SpecificProtagonist
Copy link
Contributor

Then just impl<T: Component + Mutable> QueryData for &'static mut T

I like this. Additionally we still need to store a mutability flag in the ComponentInfo and then check it when accessing components by id.

@alice-i-cecile
Copy link
Member

Following some more discussion on Discord, there seems to be broad consensus that this feature is desirable and that we should use the design laid out in #16208 (comment), which allows this to be easily extended to Resource as well.

@onkoe
Copy link
Contributor

onkoe commented Nov 30, 2024

Hey there! Just wanted to quickly drop in and say that this feels very useful. Please keep up the great work! :)


Otherwise, the design discussion on discord was pretty cool! I immediately thought of a scorched-earth solution, which is pretty much a secondary Component trait (like ComponentMut) that's opt-out through some kind of #[component::immutable] attr. Buuuuut... that seems somewhat hard to reason about, especially if anyone manually implements Component, then finds mysterious mutability errors everywhere. That's probably fixable through our god-given #[diagnostic::on_unimplemented(...)]. The discussion on reflection and a better implementation feels pretty solid, though, so I can't wait to see it materialize further!

Overall, I think most folks could get some decent use out of this. That's doubly so for large codebases

@Maximetinu
Copy link
Contributor

Immutable components sound great !

I'm interested to know, although I can imagine: why an on_mutate hook is so tricky to implement ? 🤔

I guess that, when component hooks were first implemented, there were conversations about this, because on_mutate feels like a natural companion to on_insert, on remove, etc; but I missed them

@BenjaminBrienen
Copy link
Contributor

Immutable components sound great !

I'm interested to know, although I can imagine: why an on_mutate hook is so tricky to implement ? 🤔

I guess that, when component hooks were first implemented, there were conversations about this, because on_mutate feels like a natural companion to on_insert, on remove, etc; but I missed them

I think due to mem::swap but I could be wrong

@alice-i-cecile
Copy link
Member

I'm interested to know, although I can imagine: why an on_mutate hook is so tricky to implement ? 🤔

Hooks, and to a lesser extent observers, want to run immediately after the change is made. Because of Bevy's access model, users can mutate data without having access to the full world, delaying the effect. Even if this wasn't true, there would be a serious performance risk due to the loss of cache locality during table iteration.

We can get around this by only implementing on_mutate observers, but there are still concerns around efficiently checking what has been changed. The current change detection impl relies on an O(n) scan every time, which could get expensive very fast, in hard to debug ways.

github-merge-queue bot pushed a commit that referenced this issue Dec 5, 2024
# 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>
ecoskey pushed a commit to ecoskey/bevy that referenced this issue 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 C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Design This issue requires design work to think about how it would best be accomplished
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants