简体   繁体   English

Rust 通用值内存存储实现

[英]Rust generic value memory store implementation

I am working on an API for an object store, and I am struggling to figure out a way to implement it in Rust.我正在为对象存储开发 API,并且我正在努力寻找一种在 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.这有点令人沮丧,因为我很清楚如何在 C++ 中做到这一点,所以也许这个设计根本不适合 Rust,但我希望这里的人们可以提供一些有用的见解。

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.由于可以放入 store 的具体类型是静态已知的(如果它们是通过create_something()方法构造的,它们只能在 store 中),我觉得类型系统中应该有足够的信息能够做这个。 One thing I desperately want to avoid is something like Vec<Box<dyn Any>> , because that introduces extra indirection.我非常想避免的一件事是Vec<Box<dyn Any>> ,因为这会引入额外的间接性。

I realize that this is likely not possible in safe Rust, although I feel like it should be possible using unsafe Rust.我意识到这在安全的 Rust 中可能是不可能的,尽管我觉得使用不安全的 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.我的想法是它有点类似于 Bevy ECS 实现存储组件的方式(相同类型的组件连续存储在内存中,我也想在这里看到一个属性),尽管我很难理解它是如何工作的.

Hopefully someone has ideas on how to implement this, or has an alternate design which would suite Rust better.希望有人对如何实现这一点有想法,或者有一个更适合 Rust 的替代设计。 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 .您可以在Obj上创建一个特征泛型,指定如何从存储中检索Obj的竞技场(Vecs),并将其实现为应用于ObjectAObjectB Then Store::get() uses this implementation to retrieve the values.然后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
    }
}

With that in place, Store would be implemented as follows:有了这些, 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 操场

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).我建议研究一个 arena crate而不是实现你自己的 arena(尽管如果你自己实现它并不是一个悲剧——它通常是一个重用的好主意)。

The key question is, is your store intended to store objects of a fixed number of types known at compile time, or is the same store object supposed to store objects of any type?关键问题是,您的商店是打算存储在编译时已知的固定数量类型的对象,还是应该存储任何类型的对象?

Fixed number of types固定数量的类型

This design is possible statically, and user4815162342's answer shows a good way of doing it.这种设计是静态的,用户 4815162342 的回答显示了一个很好的方法。

Storing objects of any type, dynamically动态存储任何类型的对象

This design is fundamentally dynamic and not typesafe.这种设计基本上是动态的,不是类型安全的。 When accessing an element of the store, you are accessing memory that could be of any type ;当访问存储的元素时,您访问的内存可能是任何类型的 the only way we know that it's of a particular type is the mapping between objects T and identifiers Id<T> , which is not a mapping known to the compiler.我们知道它是特定类型的唯一方法是对象T和标识符Id<T>之间的映射,这不是编译器已知的映射。 You're essentially implementing a dynamic type system since you're maintaining the types of objects as part of the store.您实际上是在实现一个动态类型系统,因为您将对象的类型作为存储的一部分进行维护。

Dynamic doesn't mean impossible , though;但是,动态并不意味着不可能 as a rule of thumb, what's possible in C++ is always possible in Rust -- typically by turning off the compiler by using unsafe code or code that can panic at runtime.根据经验,在 C++ 中可能的事情在 Rust 中总是可能的——通常是通过使用不安全的代码或在运行时可能会崩溃的代码来关闭编译器。

Illustrative implementation with Box<dyn Any> Box<dyn Any>的说明性实现

As you point out, perhaps we don't want to use Box<dyn Any> because it's a small runtime overhead, but it's easiest to see how to accomplish this design first with Box<dyn Any> to abstract a raw pointer-to-anything and using .downcast_ref() to cast these pointers to concrete types.正如您所指出的,也许我们不想使用Box<dyn Any> ,因为它的运行时开销很小,但最容易看到如何首先使用Box<dyn Any>来完成这个设计,以抽象一个原始指针到-任何东西并使用.downcast_ref()将这些指针转换为具体类型。 Here is how that works.这是它的工作原理。 I've replaced your Object trait with Default as that was the only required functionality in the below code.我已将您的Object特征替换为Default ,因为这是以下代码中唯一需要的功能。 Running the code shows that _obj_a and _obj_b have the correct type and that the code does not panic (showing that our dynamic type management is working).运行代码显示_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
}

Avoiding the runtime check避免运行时检查

In Rust nightly, we have 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.在 Rust nightly 中,我们在Any上有downcast_ref_unchecked() ,它完全符合我们的要求:它在运行时将值转换为具体类型而不执行检查。 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 eg this paper ).我们只需将.downcast_ref()替换为上述代码中未经检查的版本——如果您在实际项目中使用它,请确保进行一些性能检查,以确保不安全确实值得并有助于提高性能(我有点怀疑,因为未经检查的操作不一定能真正提高 Rust 代码的性能——参见例如这篇论文)。

If you don't want to use Nightly, what you need is an (unsafe) void pointer.如果你不想使用 Nightly,你需要的是一个(不安全的)空指针。 One way to get void pointers isc_void from the C FFI.获取 void 指针的一种方法是来自 C FFI 的c_void Another way is to just cast *const T to *const () (the () is just a placeholder type) and then uncast later using std::mem::transmute .另一种方法是将*const T转换为*const ()()只是一个占位符类型),然后稍后使用std::mem::transmute

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM