I am working on an API for an object store, and I am struggling to figure out a way to implement it in Rust. It is a bit frustrating, since I have a good idea of how this could be done in C++, so maybe the design is fundamentally not suitable for Rust, but I hope people here can provide some useful insights.
I would like to be able to do the following:
// This is clone & copy
struct Id<Identifiable> {
// What I think would be needed to get this entire system
// to work, but it is not fixed like this so alternate
// ideas are welcome.
raw_id: usize,
_marker: PhantomData<Identifiable>,
}
struct Store {
// ... don't know how to implement this
}
trait Object {
// ... some behavior
}
struct ObjectA {}
impl Object for ObjectA {}
struct ObjectB {}
impl Object for ObjectB {}
impl Store {
pub fn create_object_a(&mut self) -> Id<ObjectA> { todo!() }
pub fn create_object_b(&mut self) -> Id<ObjectB> { todo!() }
pub fn get<'a, Obj: Object>(&self, id: Id<Obj>) -> &'a Obj { todo!() }
}
This would be used as follows:
let store = Store::new();
let handle_a = store.create_object_a();
let handle_b = store.create_object_b();
let obj_a = store.get(handle_a); // obj_a is of type &ObjectA
let obj_b = store.get(handle_b); // obj_b is of type &ObjectB
Since the concrete types that can be put into the store are known statically (they can only be in the store if they are constructed through a create_something() method), I feel like there should be enough information in the type system to be able to do this. One thing I desperately want to avoid is something like Vec<Box<dyn Any>>, because that introduces extra indirection.
I realize that this is likely not possible in safe Rust, although I feel like it should be possible using unsafe Rust. My thought is that it is somewhat similar to how the Bevy ECS implementation stores components (components of the same type are stored contiguously in memory, a property I would like to see here as well), although I am struggling to understand exactly how that works.
Hopefully someone has ideas on how to implement this, or has an alternate design which would suite Rust better. Thank you!
You can create a trait generic over Obj that specifies how to retrieve arenas (Vecs) of Obj from the store, and implement it as applied to ObjectA and ObjectB. Then Store::get() uses this implementation to retrieve the values.
// Your store type:
struct Store {
a_store: Vec<ObjectA>,
b_store: Vec<ObjectB>,
}
// Trait family that specifies how to obtain slice of T's from Store
trait StoreGet<T> {
fn raw_storage(&self) -> &[T];
}
// Implementation of the trait applied to ObjectA and ObjectB on Store:
impl StoreGet<ObjectA> for Store {
fn raw_storage(&self) -> &[ObjectA] {
&self.a_store
}
}
impl StoreGet<ObjectB> for Store {
fn raw_storage(&self) -> &[ObjectB] {
&self.b_store
}
}
With that in place, Store would be implemented as follows:
impl Store {
pub fn create_object_a(&mut self) -> Id<ObjectA> {
let raw_id = self.a_store.len();
self.a_store.push(ObjectA {});
Id {
raw_id,
_marker: PhantomData,
}
}
pub fn create_object_b(&mut self) -> Id<ObjectB> {
// ... essentially the same as create_object_a ...
}
pub fn get<Obj>(&self, id: Id<Obj>) -> &Obj
where
Obj: Object,
Self: StoreGet<Obj>,
{
let slice = self.raw_storage();
&slice[id.raw_id]
}
}
Playground
I recommend looking into an arena crate rather than implementing your own arena (though it's not a tragedy if you do implement it yourself - it's just generally a good idea to reuse).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With