通过原始指针克隆类型擦除的 Arc 是否安全?

Is it safe to clone a type-erased Arc via raw pointer?

我正在处理包裹在 Arc, and I sometimes end up using into_raw 中的数据,以获取指向基础数据的原始指针。我的用例还要求类型擦除,因此原始指针经常被转换为 *const c_void,然后在重新构造 Arc.

时转换回适当的具体类型

我已经 运行 遇到了这样一种情况,即能够克隆 Arc 而无需知道基础数据的具体类型。据我了解,仅出于调用 clone 的目的使用虚拟类型重建 Arc 应该是安全的,只要我从未真正取消引用数据。因此,例如,这应该是安全的:

pub unsafe fn clone_raw(handle: *const c_void) -> *const c_void {
    let original = Arc::from_raw(handle);
    let copy = original.clone();
    mem::forget(original);
    Arc::into_raw(copy)
}

有什么我遗漏的东西会让这实际上不安全吗?另外,我假设答案也适用于 Rc,但如果有任何差异,请告诉我!

这几乎总是不安全的。

Arc<T> 只是指向堆分配结构的指针,大致看起来像

struct ArcInner<T: ?Sized> {
    strong: atomic::AtomicUsize,
    weak: atomic::AtomicUsize,
    data: T,  // You get a raw pointer to this element
}

into_raw() 为您提供指向 data 元素的指针。 Arc::from_raw() 的实现采用这样一个指针, 假定 它是指向 ArcInner<T> 中的 data 元素的指针,返回内存并假定 在那里找到 ArcInner<T>。这个假设取决于 T 的内存布局,特别是它的对齐方式,因此它在 ArcInner.

中的确切位置

如果您在 Arc<U> 上调用 into_raw(),然后调用 from_raw() 就好像它是 Arc<V>,其中 UV对齐方式不同,U/VArcInner 中的偏移计算将是错误的,对 .clone() 的调用将破坏数据结构。 因此不需要取消引用 T 来触发内存不安全

在实践中,这可能不是问题:因为 data 是两个 usize 元素之后的第三个元素,大多数 T 可能会以相同的方式对齐。但是,如果 stdlib-implementation 发生变化,或者您最终为这个假设错误的平台编译,则重建由 Arc<U> 创建的 Arc<V>::from_raw,其中 V 的内存布局和U 不同会不安全和崩溃。


更新:

再考虑一下后,我将我的投票从 "might be safe, but cringy" 降级为 "most likely unsafe",因为我总能做到

#[repr(align(32))]
struct Foo;

let foo = Arc::new(Foo);

在此示例中,Foo 将对齐到 32 个字节,使 ArcInner<Foo> 大小为 32 个字节 (8+8+16+0),而 ArcInner<()> 仅为 16 个字节(8+8+0+0)。由于在删除类型后无法判断 T 的对齐方式,因此无法重建有效的 Arc.

有一个在实践中可能是安全的逃生舱口:通过将 T 包装到另一个 Box 中,ArcInner<T> 的布局始终相同。为了对任何用户强制执行此操作,您可以执行类似

struct ArcBox<T>(Arc<Box<T>>)

并在其上实施 Deref。使用 ArcBox 而不是 Arc 强制 ArcInner 的内存布局始终相同,因为 T 在另一个指针后面。然而,这意味着对 T 的所有访问都需要双重取消引用,这可能会严重影响性能。