允许库用户在结构中嵌入任意数据是std::mem::transmite的正确用法



我正在开发的一个库以类似图形的方式存储各种数据结构。我想让用户将元数据("注释")存储在节点中,以便以后可以检索它们。目前,他们必须创建自己的数据结构来镜像库的数据结构,这非常不方便。

我对注释的内容几乎没有限制,因为我不知道用户将来想要存储什么。这个问题的其余部分是关于我目前解决这个用例的尝试,但我也对完全不同的实现持开放态度。


用户注释用一个特征表示:

pub trait Annotation {
fn some_important_method(&self)
}

这个特性包含一些方法(都在&self上),这些方法对域很重要,但对用户来说,实现起来总是微不足道的。无法通过这种方式检索注释实现的实际数据。

我可以这样存储注释列表:

pub struct Node {
// ...
annotations: Vec<Box<dyn Annotation>>,
}

我想让用户检索他们之前添加到列表中的任何实现,比如这样:

impl Node {
fn annotations_with_type<T>(&self) -> Vec<&T>
where
T: Annotation,
{
// ??
}
}

我最初的目标是将dyn Annotation转换为dyn Any,然后使用downcast_ref,但是特性上广播强制是不可行的。

另一种解决方案是要求每个Annotation实现存储其TypeId,将其与annotations_with_type的类型参数的TypeId进行比较,并将std::mem::transmute与生成的&dyn Annotation&T进行比较……但transmute的文档非常可怕,我真的不知道这是否是允许的安全情况之一。我肯定会在C.中做一些void *

当然,也有可能有第三种(安全的)方式来度过难关。我愿意接受建议。

您所描述的内容通常由TypeMaps解决,允许类型与某些数据关联。

如果您对使用库持开放态度,您可能会考虑使用现有的实现,例如https://crates.io/crates/typemap_rev,以存储数据。例如:

struct MyAnnotation;
impl TypeMapKey for MyAnnotation {
type Value = String;
}
let mut map = TypeMap::new();
map.insert::<MyAnnotation>("Some Annotation");

如果你好奇的话。它底层使用HashMap<TypeId, Box<(dyn Any + Send + Sync)>>来存储数据。为了检索数据,它在稳定的Any类型上使用downcast_ref。如果需要,这也可以是一种自己实现的模式。

您不必担心这是否有效,因为它不会编译(操场):

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
--> src/main.rs:7:18
|
7 |     _ = unsafe { std::mem::transmute::<&dyn Annotation, &i32>(&*v) };
|                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: source type: `&dyn Annotation` (128 bits)
= note: target type: `&i32` (64 bits)

错误消息应该很清楚,我希望:&dyn Trait是一个胖指针,大小为2*size_of::<usize>()。另一方面,&T是一个瘦指针(与T: Sized一样长),大小只有一个usize,并且不能在不同大小的类型之间转换。

你可以用transmute_copy()来解决这个问题,但它只会让事情变得更糟:它会起作用,但它是不健全的,不能保证以任何方式起作用。在未来的Rust版本中,它可能会变成UB。这是因为&dyn Trait参考文献(截至目前)唯一有保证的是:

指向未大小类型的指针是按大小设置的。大小和对齐保证至少等于指针的大小和对齐。

没有什么能保证字段的顺序。它可以是(data_ptr, vtable_ptr)(就像现在一样,因此transmute_copy()工作)或(vtable_ptr, data_ptr)。内容甚至没有任何保证。它根本不能包含数据指针(尽管我怀疑有人会做这样的事情)。transmute_copy()从一开始就复制数据,这意味着要让代码工作,数据指针应该在那里,并且应该在第一个(事实就是这样)。为了使代码健全,这需要得到保证(事实并非如此)。

那么我们能做什么呢?让我们来看看Any是如何发挥其魔力的:

// SAFETY: caller guarantees that T is the correct type
unsafe { &*(self as *const dyn Any as *const T) }

因此,它使用as进行转换。它有效吗?当然这意味着std可以做到这一点,因为std可以做一些没有保证的事情,并依赖于实践中的工作方式。但我们不应该。那么,它有保证吗?

我没有一个确切的答案,但我很确定答案是否定的。我没有找到任何权威来源来保证从未大小到大小指针的强制转换行为。

编辑:@CAD97在Zulip上指出,该引用承诺*[const|mut] T as *[const|mut V] where V: Sized将是一个指针到指针的情况,并且可以被解读为这将起作用的保证。

但我仍然觉得依赖它很好。因为,与transmute_copy()不同的是,人们在生产中这样做。没有比这更好的稳定方式了。因此,它成为未定义行为的可能性非常低。它更有可能被定义。

有保障的方式存在吗?嗯,是和否。是,但仅使用不稳定的指针元数据API:

#![feature(ptr_metadata)]
let v: &dyn Annotation;
let v = v as *const dyn Annotation;
let v: *const T = v.to_raw_parts().0.cast::<T>();
let v: &T = unsafe { &*v };

总之,如果您可以使用夜间功能,我更希望指针元数据API更加安全。但如果你做不到,我认为演员阵容的方法很好。

最后一点,可能有一个板条箱已经做到了。如果存在的话,更喜欢这样。

相关内容

  • 没有找到相关文章

最新更新