幽灵数据

在处理不安全代码时,我们经常会遇到这样的情况:类型或生命周期在逻辑上与结构相关,但实际上并不是字段的一部分。这种情况最常发生在生命周期上。例如,&'a [T]Iter(大约)定义如下:

#![allow(unused)]
fn main() {
struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
}
}

但是由于'a在结构体中是未使用的,所以它是无约束的。由于这在历史上造成的麻烦,在结构定义中,不受约束的生命周期和类型是禁止的,因此我们必须在主体中以某种方式引用这些类型,正确地做到这一点对于正确的变异性和丢弃检查是必要的。

我们使用PhantomData来做这个,它是一个特殊的标记类型。PhantomData不消耗空间,但为了静态分析的目的,模拟了一个给定类型的字段。这被认为比明确告诉类型系统你想要的变量类型更不容易出错,同时也提供了其他有用的东西,例如 auto traits 和 drop check 需要的信息。

Iter 逻辑上包含一堆&'a T,所以这正是我们告诉PhantomData要模拟的。

#![allow(unused)]
fn main() {
use std::marker;

struct Iter<'a, T: 'a> {
    ptr: *const T,
    end: *const T,
    _marker: marker::PhantomData<&'a T>,
}
}

就是这样,生命周期将被限定,而你的迭代器将在'aT上进行协变。所有的东西都是有效的。

泛型参数和 drop 检查

在过去,曾经有另一个事情是需要仔细思考的,这篇文档曾经这么说:

另一个重要的例子是 Vec,它(大约)定义如下:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
}
}

与前面的例子不同的是,看起来一切都和我们想的一样。Vec 的每个通用参数至少在一个字段中出现。很好,可以开始了!

不对,不是这样。

丢弃检查器将慷慨地确定Vec<T>不拥有任何 T 类型的值。这将反过来使它得出结论,它不需要担心 Vec 在其析构器中丢弃任何 T 来确定丢弃检查的合理性。这将反过来允许人们使用 Vec 的析构器来制造不健壮性。

为了告诉 dropck 我们确实拥有 T 类型的值,因此在我们丢弃时可能会丢弃一些 T,我们必须添加一个额外的PhantomData,正如这样:

#![allow(unused)]
fn main() {
use std::marker;

struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
    _marker: marker::PhantomData<T>,
}
}

但自从RFC 1238之后,这就不正确也并不需要了。

如果你这么写:

#![allow(unused)]
fn main() {
struct Vec<T> {
    data: *const T, // `*const`是可变异的!
    len: usize,
    cap: usize,
}
#[cfg(any())]
impl<T> Drop for Vec<T> { /* … */ }
}

那么impl<T> Drop for Vec<T>这条语句会让 Rust 知道Vec<T>拥有T类型的值(更准确地说:可能会在Drop实现中使用T类型的值),那么当Vec<T>被 drop 的时候,Rust 就不会允许它们 悬垂

当一个类型已经有了 Drop impl 时,添加一个额外的 _owns_T: PhantomData<T> 字段是多余的,而且没有任何效果,从 dropck(Drop 检查)的角度来看(它仍然会影响变量和自动特征)。

  • (高级边缘情况:如果包含 PhantomData 的类型根本没有 Drop 实现,但仍然有 drop glue(通过拥有另一个带有 drop glue 的字段),那么这里提到的 dropck/#[may_dangle] 规则也同样适用:一个 PhantomData<T> 字段将要求 T 在包含类型作用域结束时可被丢弃)。

但是这在某些场景下,会导致过于严格,这也是为啥标准库使用了一个不稳定并且unsafe的属性来切换回旧的unchecked的 drop 检查行为,也是接下来这个文档所警告的:#[may_dangle]属性。

一个例外:标准库的特殊情况及不稳定的#[may_dangle]

如果你只是写自己的库代码,那你可以跳过这章;但是如果你想知道标准库中真正的Vec是怎么实现的,你会发现它仍然需要_owns_T: PhantomData<T>字段来保证可靠性。

点这里查看原因

思考以下这个例子:

fn main() {
    let mut v: Vec<&str> = Vec::new();
    let s: String = "Short-lived".into();
    v.push(&s);
    drop(s);
} // <- `v`在这里被 drop 了

对于一个经典的impl<T> Drop for Vec<T> {定义,上面这段代码会被编译器拒绝

实际上,在这个例子中,我们的Vec的类型实际上是Vec</* T = */ &'s str>,是一个元素为's生命周期的string 的 Vec,但是由于上面还有一行定义let s: String,它在Vec drop 之前就被 drop 了,所以在Vec被 drop 的时候,'s已经不再有效了,这时候Drop的实际签名为:impl<'s> Drop for Vec<&'s str> {

这意味着,Drop被调用时,它将会面对一个无效的,或者说悬垂(dangling)的生命周期's。这是违背了 Rust 原则的,Rust 原则要求所有的函数中的 Rust 引用都必须有效,解引用操作必须是合法的。

这也是为什么 Rust 会保守地拒绝这段代码。

然而,在真正的Vec中,Drop的实现并不关心&'s str,毕竟它(译者注:&'s str)没有自己的Drop实现(since it has no drop glue of its own),它想做的只是把它自己的 buffer 给释放掉。

换句话说,如果上述这个片段能被 Rust 接受那就再好不过了,我们通过封装Vec,或者说可以依赖于Vec一些特殊的属性:Vec可以承诺当它被 drop 时不会使用它拥有的&'s str

这是一种unsafe的承诺,可以通过#[may_dangle]来表达:

unsafe impl<#[may_dangle] 's> Drop for Vec<&'s str> { /* … */ }

或者,更通用化的:

unsafe impl<#[may_dangle] T> Drop for Vec<T> { /* … */ }

这就是一个unsafe的方法用来摆脱 Rust drop 检查器这个保守的假设——一个 drop 的实例的类型参数不允许是悬垂的。

并且当这样做时,例如在标准库中,我们需要小心T有自己的Drop实现。比如,在这种情况下,想象用struct PrintOnDrop<'s> /* = */ (&'s str);替换&'s str,这将具有Drop impl,其内部的&'s str将被解引用并打印到屏幕上。

实际上,Drop for Vec<T> {,在释放自己的 Buffer 之前,确实必须在每个T类型的元素具有自定义Drop实现时递归地删除它;在 PrintOnDrop<'s>的情况下,这意味着Vec<PrintOnDrop<'s>>Drop必须在释放 Buffer 之前递归地删除PrintOnDrop<'s>的元素。

所以当我们说's #[may_dangle] 时,这是一个过于宽松的说法。我们更期望这么说说:“'s可能会悬垂,前提是它不涉及一些Drop自定义实现”。或者,更一般地说,“T可能会悬空,前提是它不涉及某些Drop自定义实现”。每当**我们拥有一个T**时,这种“例外的例外”是一种普遍的情况。这就是为什么 Rust 的#[may_dangle]足够聪明,当泛型参数以拥有的方式 被 struct 的某个字段所保存时,会被禁用。(原文:That's why Rust's #[may_dangle] is smart enough to know of this opt-out, and will thus be disabled when the generic parameter is held in an owned fashion by the fields of the struct.)

这就是为什么最终标准库是这么写的:

#![allow(unused)]
fn main() {
#[cfg(any())]
// 我们拉勾说好,当 drop `Vec`的时候不去用`T`
unsafe impl<#[may_dangle] T> Drop for Vec<T> {
    fn drop(&mut self) {
        unsafe {
            if mem::needs_drop::<T>() {
                /* … 除了这里,也就是说,… */
                ptr::drop_in_place::<[T]>(/* … */);
            }
            // …
            dealloc(/* … */)
            // …
        }
    }
}

struct Vec<T> {
    // … 除非事实上`Vec`拥有了`T`类型的元素,并且可能在 drop 时 drop 它们
    _owns_T: core::marker::PhantomData<T>,
    ptr: *const T, // `*const`是可变异的(但这本身并不能表达对`T`的所有权)
    len: usize,
    cap: usize,
}
}

拥有内存分配的原始指针是如此普遍的模式,以至于标准库为自己整了一个名为Unique<T>的类型:

  • 包装一个*const T,用于变异
  • 包括一个PhantomData<T>
  • 根据包含的 T 自动派生Send/Sync
  • 空指针的优化,将指针标记为NonZero

PhantomData模式表

下面是一个关于所有可以使用PhantomData的神奇方式的表格: (covariant:协变,invariant:不变,contravariant:逆变)

Phantom typevariance of 'avariance of TSend/Sync
(or lack thereof)
dangling 'a or T in drop glue
(e.g., #[may_dangle] Drop)
PhantomData<T>-covariantinheriteddisallowed ("owns T")
PhantomData<&'a T>covariantcovariantSend + Sync
requires
T : Sync
allowed
PhantomData<&'a mut T>covariantinvariantinheritedallowed
PhantomData<*const T>-covariant!Send + !Syncallowed
PhantomData<*mut T>-invariant!Send + !Syncallowed
PhantomData<fn(T)>-contravariantSend + Syncallowed
PhantomData<fn() -> T>-covariantSend + Syncallowed
PhantomData<fn(T) -> T>-invariantSend + Syncallowed
PhantomData<Cell<&'a ()>>invariant-Send + !Syncallowed
  • 注意: opt-out Unpin 自动特性需要专用的 PhantomPinned 类型。