Skip to content

Abstraction for working with a &World and interior mutability #5956

Closed
@jakobhellermann

Description

@jakobhellermann

Motivation

The essential rule for aliasing in rust is that you can have either multiple shared & references to a location, or a unique &mut reference.
This is sometimes too restrictive, e.g. when you have two systems that both share a reference to a World but you know that they don't access the same data. As an escape hatch rust provides the UnsafeCell<T> type (and safe wrappers around it), which is special in that it lets you get a &mut T from a &UnsafeCell<T>, provided that you manually uphold the safety requirements. Notable, this doesn't change anything about the fact that you can't have two aliasing &muts. The only thing that this allows, and that we make use of, is building abstractions like these:

struct World {
  storages: UnsafeCell<ActualStorage>
  ...
}

where you can pass a &World around and expose some unsafe fn World::get_unchecked_mut<T>(&World, Entity) -> Option<&mut T>.

Why is this not enough?
This works if you carefully document the unchecked_mut safety contracts and take care to not violate them. It is still error prone, because when you have a &World that only lets you access resource Foo, safe code can do bad things.
This is because all the methods on World like World::get_resource are safe, which is usually corrent when treating &World as an immutable world reference. This isn't the case anymore if it is used as a access restricted kinda-mutable world.

Solution
Add a new wrapper type around &World (here InteriorMutableWorld, name up for discussion). Using this type documents intent better and makes writing unsafe code easier than using a &World directly.

Proposed API

struct World { .. }
impl World {
  fn as_interior_mutable(&'w self) -> InteriorMutableWorld<'w>;
}

#[derive(Clone, Copy)]
struct InteriorMutableWorld<'w>(&'w World);
impl<'w> InteriorMutableWorld {
  unsafe fn world(&self) -> &World;

  fn get_entity(&self) -> InteriorMutableEntityRef<'w>;

  unsafe fn get_resource<T>(&self) -> Option<&'w T>;
  unsafe fn get_resource_by_id(&self, ComponentId) -> Option<&'w T>;
  unsafe fn get_resource_mut<T>(&self) -> Option<Mut<'w, T>>;
  unsafe fn get_resource_mut_by_id<T>(&self) -> Option<MutUntyped<'w>>;

  // not included: remove, remove_resource, despawn, anything that might change archetypes
}

struct InteriorMutableEntityRef<'w> { .. }
impl InteriorMutableEntityRef<'w> {
  unsafe fn get<T>(&self, Entity) -> Option<&'w T>;
  unsafe fn get_by_id(&self, Entity, ComponentId) -> Option<Ptr<'w>>;
  unsafe fn get_mut<T>(&self, Entity) -> Option<Mut<'w, T>>;
  unsafe fn get_mut_by_id(&self, Entity, ComponentId) -> Option<MutUntyped<'w>>;
  unsafe fn get_change_ticks<T>(&self, Entity) -> Option<Mut<'w, T>>;
  unsafe fn get_mut_by_id(&self, Entity, ComponentId) -> Option<MutUntyped<'w>>;
}

After this abstraction has been built, we should update our internal use of &World to InteriorMutableWorld, for example

trait System {
-    unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out;
+    unsafe fn run_unsafe(&mut self, input: Self::In, world: &InteriorMutableWorld) -> Self::Out;
    ...
}

Prior Art

#5588 builds something similar, but custom built for the QueryState use case.

Implementation Questions

When we have World::get_resource_mut and InteriorMutableWorld::get_resource_mut we don't want code duplication.
Do we

  1. define get_resource_mut_inner(&self) on World and use it in both or
  2. just define the method in InteriorMutableWorld and call self.as_interior_mutable().get_resource_mut(self) in World

Same question for get_resource, but here we could also define it in World and let InteriorMutableWorld just call self.0.get_resource.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-Code-QualityA section of code that is hard to understand or changeC-UsabilityA targeted quality-of-life change that makes Bevy easier to useP-UnsoundA bug that results in undefined compiler behavior

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions