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

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

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.

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.

Co-Authored-By: Alice Cecile <[email protected]>
@bushrat011899
Copy link
Contributor Author

bushrat011899 commented Nov 14, 2024

After further iteration, the ComponentMut and ComponentImmutable traits have been removed, opting instead for matching against the associated type Mutability.

/// Work with a component, regardless of its mutability
fn get_component<C: Component>(/* ... */) { /* ... */ }

/// _Only_ allow mutable components
fn get_component_mut<C: Component<Mutability = Mutable>>(/* ... */) { /* ... */ }

/// _Only_ allow immutable components
fn get_component_immutable<C: Component<Mutability = Immutable>>(/* ... */) { /* ... */ }

If you find this cumbersome, you can easily create your own blanket-impl trait(s):

pub trait ComponentMut: Component<Mutability = Mutable> {}
impl<C: Component<Mutability = Mutable>> ComponentMut for C {}

pub trait ComponentNonMut: Component<Mutability = Immutable> {}
impl<C: Component<Mutability = Immutable>> ComponentNonMut for C {}

@alice-i-cecile
Copy link
Member

I think that my only remaining major request is that this has a test suite for dynamic components. Once that's done I'll do a final polish pass on this at the start of 0.16.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Needs-Help The author needs help finishing this PR. labels Nov 14, 2024
@BenjaminBrienen
Copy link
Contributor

I really like how this PR is turning out. I'm a fan of the associated type idea.

Demonstrates using the immutable components feature with dynamic components. We go through the process of creating dynamic immutable components, adding them to an entity, and then testing retrieval methods.
@bushrat011899
Copy link
Contributor Author

I have added a second example, immutable_components_dynamic, which demonstrates creating dynamic immutable components at runtime. In particular, the example shows that while get_by_id(...) will succeed for an immutable component, get_mut_by_id(...) will return an Err, since that component cannot be mutably accessed.

There is definitely room for better documentation, and there's probably some additional methods/changes to existing methods we'd want to do before merging, but I think I have sufficiently covered the bulk of the work for this feature. I look forward to more detailed feedback in the coming weeks!

@bushrat011899 bushrat011899 added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Nov 14, 2024
@bushrat011899
Copy link
Contributor Author

As an aside, I added #[component(immutable)] to Name to see if anything broke, and to my pleasant surprise it just worked without any knock-on effects. Bevy already treats Name as some immutable data, so making it "official" is a 1-line PR once this is merged. I haven't included that in this PR just to keep the scope as small as it can be.

@NthTensor
Copy link
Contributor

The associated trait magic with sealed logic types is very cute, I like it. Is the issue with FilteredEntityMut::get_mut_by_id resolved or still outstanding?

@bushrat011899
Copy link
Contributor Author

The associated trait magic with sealed logic types is very cute, I like it. Is the issue with FilteredEntityMut::get_mut_by_id resolved or still outstanding?

Thanks! And yes that issue is resolved. The move to an associated type made enough information available on Component for me to have immutability be a bool on ComponentDescriptor.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

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

I did some thinking about what this API will let us do over my lunch-break, and I'm very pleased. An immutable component with an insert hook can cache data on a private required mutable component with extremely little overhead (at most one archetype move, and some non-structural mutations). This seems to align perfectly with the direction the engine is going with required components and hooks (and, eventually, archetype invariants).

Looked over the code and saw no issues, as far as I am aware this is good to go.

@NthTensor NthTensor added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Nov 14, 2024
Copy link
Contributor Author

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

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

Below is some notes to assist reviewers understand the decisions I've made for this PR.

crates/bevy_ecs/src/component.rs Outdated Show resolved Hide resolved
/// # Safety
/// - the `drop` fn must be usable on a pointer with a value of the layout `layout`
/// - the component type must be safe to access from any thread (Send + Sync in rust terms)
pub unsafe fn new_immutable_with_layout(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered just adding an immutable: bool parameter to the existing new_with_layout, but this avoids another breaking change in the API.

@@ -841,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 _),
immutable: false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't added immutability to Resources in this PR to keep the scope contained. I suspect it would basically just be a duplication of some of the infrastructure added for immutable Components anyway, so once this PR is merged a followup should be straight-forward. However, without Resource-hooks, I'm not sure how useful it'd be.

@@ -46,7 +46,7 @@ pub mod prelude {
pub use crate::{
bundle::Bundle,
change_detection::{DetectChanges, DetectChangesMut, Mut, Ref},
component::Component,
component::{Component, Immutable, Mutable},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a little concerned about putting these in the prelude, since they're such generic terms. If any other area of Bevy needs to use similar terms, I think it'd make sense to move these mutability markers into their own module, independent of component (perhaps change_detection?).

Happy to remove them from the prelude if anyone has concerns. They're still publicly accessible regardless.

let mut observed_by = entity_mut.entry::<ObservedBy>().or_default();
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default().into_mut();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the consequence of changes made to the entry API for components. Ideally, we would return Mut<T> when calling or_default() on a mutable component, and &T on an immutable component (and likewise for other entry methods). The problem is that would require specialisation, since the Rust compiler has no way of knowing that Component<Mutability = Mutable> and Component<Mutability = Immutable> are mutually exclusive traits.

Since we already have the OccupiedEntry type, I decided to return that for all relevant operations instead, since it already has methods to either get a im/mutable reference to the underlying component. The alternatives would either be to not allow the entry API for immutable components (bad), or duplicate all the methods (or_default_immutable(), etc.)

crates/bevy_ecs/src/world/deferred_world.rs Outdated Show resolved Hide resolved
Comment on lines -2215 to -2239
/// Inserts the component into the `VacantEntry` and returns a mutable reference to it.
///
/// # Examples
///
/// ```
/// # use bevy_ecs::{prelude::*, world::Entry};
/// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)]
/// struct Comp(u32);
///
/// # let mut world = World::new();
/// let mut entity = world.spawn_empty();
///
/// if let Entry::Vacant(v) = entity.entry::<Comp>() {
/// v.insert(Comp(10));
/// }
///
/// assert_eq!(world.query::<&Comp>().single(&world).0, 10);
/// ```
#[inline]
pub fn insert(self, component: T) -> Mut<'a, T> {
self.entity_world.insert(component);
// This shouldn't panic because we just added this component
self.entity_world.get_mut::<T>().unwrap()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed because insert_entry and insert would be identical with insert returning an OccupiedEntry. As previously mentioned, if we had specialisation we wouldn't need to make this change.

///
/// - `T` must be a mutable component
#[inline]
pub unsafe fn into_mut_assume_mutable<T: Component>(self) -> Option<Mut<'w, T>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These x_assume_mutable methods are the escape-hatch to let you work with a possibly mutable component. I don't like expanding the unsafe API (especially the public unsafe API), but there isn't really a clean alternative. As mentioned in earlier reviews, this is at least a very small safety invariant to uphold.

// - No drop command is required
// - The component will store [u8; size], which is Send + Sync
let descriptor = unsafe {
ComponentDescriptor::new_immutable_with_layout(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is all that really changes when working with immutable dynamic components; the ComponentDescriptor just needs to include the immutable flag and then everything else works as it did previously.

Comment on lines +66 to +67
// ...but we cannot gain a mutable reference.
assert!(entity.get_mut_by_id(*component_id).is_err());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A runtime-error is as-good-as it gets for dynamic immutable components. Not ideal but there's no compile-time options for hopefully pretty obvious reasons.

This is an alternative to storing `immutable`, which is strictly speaking a negation on `mutable`.
@@ -265,6 +266,8 @@ impl ReflectComponent {

impl<C: Component + Reflect + TypePath> FromType<C> for ReflectComponent {
fn from_type() -> Self {
// TODO: Currently we panic of 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.

Suggested change
// TODO: Currently we panic of a component is immutable and you use
// TODO: Currently we panic if a component is immutable and you use

component.apply(reflected_component.as_partial_reflect());
entity.insert(component);
Copy link
Member

Choose a reason for hiding this comment

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

Was this intentional?

Suggested change
entity.insert(component);

@@ -273,12 +276,22 @@ impl<C: Component + Reflect + TypePath> FromType<C> for ReflectComponent {
entity.insert(component);
},
apply: |mut entity, reflected_component| {
let mut component = entity.get_mut::<C>().unwrap();
if !C::Mutability::MUTABLE {
panic!("This component is immutable, you cannot modify it through reflection");
Copy link
Member

Choose a reason for hiding this comment

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

Nit: For debuggability, could we maybe have these panic messages indicate the method that caused the error? Even better would be to provide the name of the component.

Maybe something like: {component_name} is immutable, it cannot be modified via `ReflectComponent::apply` ?

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