允许图书馆用户在您的结构中嵌入任意数据是 std::mem::transmute 的正确用法吗?
Is allowing library users to embed arbitrary data in your structures a correct usage of std::mem::transmute?
我正在研究的一个库以类似图形的方式存储各种数据结构。
我想让用户在节点中存储元数据(“注释”),以便他们以后可以检索它们。目前,他们必须创建自己的数据结构来反映图书馆的数据结构,这非常不方便。
我对注释的内容几乎没有限制,因为我不知道用户将来想要存储什么。
这个问题的其余部分是关于我目前解决这个用例的尝试,但我也对完全不同的实现持开放态度。
用户注释用特征表示:
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
, however trait upcasting coercion is unsable。
另一种解决方案是要求每个 Annotation
实现存储其 TypeId
,将其与 annotations_with_type
的类型参数的 TypeId
和 std::mem::transmute
结果 &dyn Annotation
到 &T
… 但是 transmute
的文档非常可怕,老实说我不知道这是否是允许的安全情况之一。我肯定会在 C 中做一些 void *
。
当然也有可能有第三种(安全的)方法来解决这个问题。我乐于接受建议。
您不必担心这是否有效 - 因为它无法编译 (playground):
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()
, but it will just make things worse: it will work, but it is unsound and is not guaranteed to work in any way. It may become UB in future Rust versions. This is because the only guaranteed thing (as of now) for &dyn Trait
references is 来解决这个问题:
Pointers to unsized types are sized. The size and alignment is guaranteed to be at least equal to the size and alignment of a pointer.
无法保证字段的顺序。它可以是 (data_ptr, vtable_ptr)
(就像现在一样,因此 transmute_copy()
有效)或 (vtable_ptr, data_ptr)
。内容甚至没有任何保证。它根本不能包含数据指针(尽管我怀疑有人会做那样的事情)。 transmute_copy()
从头开始复制数据,这意味着要使代码正常工作,数据指针应该在那里并且应该在第一位(它是)。为了使代码健全,需要保证这一点(事实并非如此)。
那我们能做什么呢?让我们检查一下 how Any
does its magic:
// SAFETY: caller guarantees that T is the correct type
unsafe { &*(self as *const dyn Any as *const T) }
因此它使用 as
进行转换。它有效吗?当然。这意味着 std 可以做到这一点,因为 std 可以做一些没有保证的事情,这取决于事情在实践中的运作方式。但我们不应该。那么,有保障吗?
我没有确切的答案,但我很确定答案是否定的。我没有找到任何权威来源来保证从未调整大小的指针到调整大小的指针的转换行为。
编辑: @CAD97 指出 on Zulip that the reference promises *[const|mut] T as *[const|mut V] where V: Sized
将是一个 pointer-to-pointer 案例,并且可以将其视为保证将工作。
但我仍然觉得依靠它很好。因为,与 transmute_copy()
不同,人们正在这样做。在生产中。并且没有更好的方法稳定。所以它成为未定义行为的可能性非常低。它更有可能被定义。
有保证的方法吗?好吧,是的,不是。是的,但只使用不稳定的 pointer metadata 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 只是为了更加安全。但如果你不能,我认为强制转换方法很好。
最后一点,可能有一个板条箱已经这样做了。更喜欢那个,如果它存在的话。
您所描述的内容通常由 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
。如果需要,这也可以是您自己实现的模式。
我正在研究的一个库以类似图形的方式存储各种数据结构。 我想让用户在节点中存储元数据(“注释”),以便他们以后可以检索它们。目前,他们必须创建自己的数据结构来反映图书馆的数据结构,这非常不方便。
我对注释的内容几乎没有限制,因为我不知道用户将来想要存储什么。 这个问题的其余部分是关于我目前解决这个用例的尝试,但我也对完全不同的实现持开放态度。
用户注释用特征表示:
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
, however trait upcasting coercion is unsable。
另一种解决方案是要求每个 Annotation
实现存储其 TypeId
,将其与 annotations_with_type
的类型参数的 TypeId
和 std::mem::transmute
结果 &dyn Annotation
到 &T
… 但是 transmute
的文档非常可怕,老实说我不知道这是否是允许的安全情况之一。我肯定会在 C 中做一些 void *
。
当然也有可能有第三种(安全的)方法来解决这个问题。我乐于接受建议。
您不必担心这是否有效 - 因为它无法编译 (playground):
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()
, but it will just make things worse: it will work, but it is unsound and is not guaranteed to work in any way. It may become UB in future Rust versions. This is because the only guaranteed thing (as of now) for &dyn Trait
references is 来解决这个问题:
Pointers to unsized types are sized. The size and alignment is guaranteed to be at least equal to the size and alignment of a pointer.
无法保证字段的顺序。它可以是 (data_ptr, vtable_ptr)
(就像现在一样,因此 transmute_copy()
有效)或 (vtable_ptr, data_ptr)
。内容甚至没有任何保证。它根本不能包含数据指针(尽管我怀疑有人会做那样的事情)。 transmute_copy()
从头开始复制数据,这意味着要使代码正常工作,数据指针应该在那里并且应该在第一位(它是)。为了使代码健全,需要保证这一点(事实并非如此)。
那我们能做什么呢?让我们检查一下 how Any
does its magic:
// SAFETY: caller guarantees that T is the correct type
unsafe { &*(self as *const dyn Any as *const T) }
因此它使用 as
进行转换。它有效吗?当然。这意味着 std 可以做到这一点,因为 std 可以做一些没有保证的事情,这取决于事情在实践中的运作方式。但我们不应该。那么,有保障吗?
我没有确切的答案,但我很确定答案是否定的。我没有找到任何权威来源来保证从未调整大小的指针到调整大小的指针的转换行为。
编辑: @CAD97 指出 on Zulip that the reference promises *[const|mut] T as *[const|mut V] where V: Sized
将是一个 pointer-to-pointer 案例,并且可以将其视为保证将工作。
但我仍然觉得依靠它很好。因为,与 transmute_copy()
不同,人们正在这样做。在生产中。并且没有更好的方法稳定。所以它成为未定义行为的可能性非常低。它更有可能被定义。
有保证的方法吗?好吧,是的,不是。是的,但只使用不稳定的 pointer metadata 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 只是为了更加安全。但如果你不能,我认为强制转换方法很好。
最后一点,可能有一个板条箱已经这样做了。更喜欢那个,如果它存在的话。
您所描述的内容通常由 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
。如果需要,这也可以是您自己实现的模式。