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),并将其应用于 ObjectA
和 ObjectB
。然后 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]
}
}
我建议研究 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
.
我正在为对象存储开发 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),并将其应用于 ObjectA
和 ObjectB
。然后 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]
}
}
我建议研究 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
.