I am trying to write a simple ECS:
struct Ecs {
component_sets: HashMap<TypeId, Box<dyn Any>>,
}
impl Ecs {
pub fn read_all<Component>(&self) -> &SparseSet<Component> {
self.component_sets
.get(&TypeId::of::<Component>())
.unwrap()
.downcast_ref::<SparseSet<Component>>()
.unwrap()
}
pub fn write_all<Component>(&mut self) -> &mut SparseSet<Component> {
self.component_sets
.get_mut(&TypeId::of::<Component>())
.unwrap()
.downcast_mut::<SparseSet<Component>>()
.unwrap()
}
}
I am trying to get mutable access to a certain component while another is immutable. This testing code triggers the error:
fn testing() {
let all_pos = { ecs.write_all::<Pos>() };
let all_vel = { ecs.read_all::<Vel>() };
for (p, v) in all_pos.iter_mut().zip(all_vel.iter()) {
p.x += v.x;
p.y += v.y;
}
}
And the error
error[E0502]: cannot borrow `ecs` as immutable because it is also borrowed as mutable
--> src\ecs.rs:191:25
|
190 | let all_pos = { ecs.write_all::<Pos>() };
| --- mutable borrow occurs here
191 | let all_vel = { ecs.read_all::<Vel>() };
| ^^^ immutable borrow occurs here
My understanding of the borrow checker rules tells me that it's totally fine to get references to different component sets mutably or immutably (that is, &mut SparseSet<Pos>
and &SparseSet<Vel>
) since they are two different types. In order to get these references though, I need to go through the main ECS struct which owns the sets, which is where the compiler complains (i.e. first I use &mut Ecs
when I call ecs.write_all
and then &Ecs
on ecs.read_all
).
My first instinct was to enclose the statements in a scope, thinking it could just drop the &mut Ecs
after I get the reference to the inner component set so as not to have both mutable and immutable Ecs
references alive at the same time. This is probably very stupid, yet I don't fully understand how, so I wouldn't mind some more explaining there.
I suspect one additional level of indirection is needed (similar to RefCell
's borrow
and borrow_mut
) but I am not sure what exactly I should wrap and how I should go about it.
Update
Solution 1: make the method signature of write_all
take a &self
despite returning a RefMut<'_, SparseSet<Component>>
by wrapping the SparseSet in a RefCell (as illustrated in the answer below by Kevin Reid).
Solution 2: similar as above (method signature takes &self
) but uses this piece of unsafe code:
fn write_all<Component>(&self) -> &mut SparseSet<Component> {
let set = self.component_sets
.get(&TypeId::of::<Component>())
.unwrap()
.downcast_ref::<SparseSet<Component>>()
.unwrap();
unsafe {
let set_ptr = set as *const SparseSet<Component>;
let set_ptr = set_ptr as *mut SparseSet<Component>;
&mut *set_ptr
}
}
What are benefits of using solution 1, is the implied runtime borrow-checking provided by RefCell an hindrance in this case or would it actually prove useful? Would the use of unsafe be tolerable in this case? Are there benefits? (e.g. performance)
RefCell
, then its as simple as makingwrite_all
also take only&self
and then referencing multiple parts of data at once will be a non-issue as you can borrow mutably from inside the structure while public interface only requires shared references. In general case for borrowchecker problems though, I'd recommend looking into projects like indexlist and generational-arena to see how we solve problems with circular references. – Kaihaku&T
to&mut T
(or convert via pointer casts) regardless of whether it is aliased or not. You can useunsafe
to accomplish something similar toRefCell
with no runtime overhead; the way to do that is using anUnsafeCell
, which disables the optimizations that make&T
->&mut T
problematic. – trentcl