Rust 通用值内存存储实现

Rust generic value memory store implementation

我正在为对象存储开发 API,我正在努力寻找一种在 Rust 中实现它的方法。有点令人沮丧,因为我很清楚如何在 C++ 中完成此操作,所以也许该设计从根本上不适合 Rust,但我希望这里的人们可以提供一些有用的见解。

我希望能够做到以下几点:

// 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!() }
}

这将按如下方式使用:

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

由于可以放入商店的具体类型是静态已知的(如果它们是通过 create_something() 方法构造的,它们只能在商店中),我觉得应该有足够的信息类型系统能够做到这一点。我非常想避免的一件事是像 Vec<Box<dyn Any>> 这样的东西,因为它引入了额外的间接。

我意识到这在安全的 Rust 中可能是不可能的,尽管我觉得使用不安全的 Rust 应该是可能的。我的想法是,它有点类似于 Bevy ECS 实现存储组件的方式(相同类型的组件连续存储在内存中,属性 我也想在这里看到),虽然我很难理解究竟是如何工作的。

希望有人对如何实现这个有想法,或者有一个更适合 Rust 的替代设计。谢谢!

您可以在 Obj 上创建一个通用特征,指定如何从存储中检索 Obj 的竞技场 (Vecs),并将其应用于 ObjectAObjectB。然后 Store::get() 使用此实现来检索值。

// 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
    }
}

有了它,Store 将按如下方式实施:

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

我建议研究 an arena crate 而不是实施自己的竞技场(尽管如果您自己实施也不是悲剧 - 重用通常是个好主意)。

关键问题是,您的存储是要存储编译时已知的固定数量类型的对象,还是应该存储任何类型的对象?

类型数量固定

这种设计是静态可行的, 展示了一种很好的实现方式。

动态存储任何类型的对象

这种设计基本上是动态的,不是类型安全的。当访问商店的元素时,您正在访问可以是任何类型的内存;我们知道它属于特定类型的唯一方法是对象 T 和标识符 Id<T> 之间的映射,这不是编译器已知的映射。您实质上是在实现一个动态类型系统,因为您将对象类型维护为商店的一部分。

动态并不意味着不可能;根据经验,在 C++ 中可能的事情在 Rust 中总是可能的——通常是通过使用不安全的代码或可能在运行时 panic 的代码来关闭编译器。

使用 Box<dyn Any>

的示例性实现

正如您所指出的,也许我们不想使用 Box<dyn Any>,因为它的运行时开销很小,但最容易看出如何首先使用 Box<dyn Any> 来抽象一个raw pointer-to-anything 并使用 .downcast_ref() 将这些指针转换为具体类型。这是它的工作原理。我已经用 Default 替换了你的 Object 特征,因为这是下面代码中唯一需要的功能。 运行 代码显示 _obj_a_obj_b 具有正确的类型并且代码不会出现错误(表明我们的动态类型管理正在运行)。

use std::any::Any;
use std::collections::HashMap;
use std::marker::PhantomData;

pub struct Id<T> {
  raw_id: usize,
  _marker: PhantomData<T>,
}

// To implement Store we use Box<dyn Any>. If it is truly crucial
// to avoid the unwrap call at runtime, a pointer could be used instead
// and downcasted dynamically, but this is fundamentally not typesafe.
pub struct Store {
    next_id: usize,
    stash: HashMap<usize, Box<dyn Any>>
}

#[derive(Default)]
pub struct ObjectA {}

#[derive(Default)]
pub struct ObjectB {}

impl Store {
    pub fn new() -> Self {
        Self { next_id: 0, stash: HashMap::new() }
    }
    fn new_id(&mut self) -> usize {
        let id = self.next_id;
        self.next_id += 1;
        id
    }
    pub fn create<Obj: Default + 'static>(&mut self) -> Id<Obj> {
        let id = self.new_id();
        self.stash.insert(id, Box::new(Obj::default()));
        Id {raw_id: id, _marker: PhantomData }
    }
    pub fn get<'a, Obj: 'static>(&'a self, id: Id<Obj>) -> &'a Obj {
        // Note that because we manage Id<Obj>s, the following unwraps
        // should never panic in reality unless the programmer
        // does something bad like modify an Id<Obj>.
        self.stash.get(&id.raw_id).unwrap().downcast_ref().unwrap()
    }
}

fn main() {
    // Example usage

    let mut store = Store::new();
    let handle_a = store.create::<ObjectA>();
    let handle_b = store.create::<ObjectB>();

    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
}

避免运行时检查

在 Rust nightly 中,我们有 downcast_ref_unchecked() on Any which does exactly what we want: it casts the value to a concrete type at runtime without performing the check. We simply replace .downcast_ref() with the unchecked version in the above code -- make sure to do some performance checks if you are using this in a real project to make sure the unsafety is actually worth it and helping performance (which I somewhat doubt, since unchecked operations don't necessarily actually improve performance of Rust code -- see e.g. this paper).

如果你不想使用 Nightly,你需要的是一个(不安全的)void 指针。获取空指针的一种方法是 c_void from the C FFI. Another way is to just cast *const T to *const () (the () is just a placeholder type) and then uncast later using std::mem::transmute.