Module std::pin

1.33.0 · source ·
Expand description

键入将数据固定到其在内存中的位置的类型。

从对象在内存中的位置不变的意义上讲,保证对象不移动有时很有用。 这种情况的一个主要例子是构建自引用结构,因为移动一个带有指向自身的指针的对象会使它们无效,这可能导致未定义的行为。

在高层次上,Pin<P> 确保任何指针类型 P 的指针在内存中都有一个稳定的位置,这意味着它不能被移动到其他地方,并且它的内存不能被释放,直到它被丢弃。我们说该对象是 “pinned”。当讨论将固定数据与非固定数据结合在一起的类型时,事情变得更加微妙。查看下文 了解更多详细信息。

默认情况下,Rust 中的所有类型都是可移动的。 Rust 允许按值传递所有类型,以及常见的智能指针类型,例如 Box<T>&mut T 允许替换和移动它们包含的值:您可以移出 Box<T>,或者您可以使用 mem::swapPin<P> 包装一个指针类型 P,所以 Pin<Box<T>>函数很像一个普通的 Box<T>: 当 Pin<Box<T>>被丢弃,其内容也被丢弃,内存被释放。类似地,Pin<&mut T>很像 &mut T。 然而,Pin<P> 不让客户实际获得 Box<T>&mut T 固定数据,这意味着您不能使用 mem::swap 之类的操作:

use std::pin::Pin;
fn swap_pins<T>(x: Pin<&mut T>, y: Pin<&mut T>) {
    // `mem::swap` 需要 `&mut T`,但我们无法得到它。
    // 我们被困住了,我们不能交换这些引用的内容。
    // 我们可以使用 `Pin::get_unchecked_mut`,但这是不安全的,原因如下:
    // 我们不允许将其用于将物品移出 `Pin`。
}
Run

值得重申的是 Pin<P> 不会改变一个事实,即 Rust 编译器认为所有类型都是可移动的。 mem::swap 仍可用于任何 T。 相反,Pin<P> 防止某些值 (由 Pin<P>) 使其无法调用需要 &mut T 方法 (如 mem::swap) 而被移动。

Pin<P> 可用于包装任何指针类型 P,因此它与 DerefDerefMut 交互。Pin<P>其中 P: Deref 应被视为固定 P::Target的 “P-style pointer” - 因此,Pin<Box<T>>是指向固定 T 的拥有指针,以及 Pin<Rc<T>>Pin<Rc> 是指向固定 T 的引用计数指针。 为正确起见,Pin<P> 依赖于 DerefDerefMut 的实现不会移出它们的 self 参数,并且只在固定指针上调用它们时才返回指向固定数据的指针。

Unpin

即使不固定,许多类型也始终可以自由移动,因为它们不依赖于具有稳定的地址。这包括所有原始类型 (如 booli32 和引用) 以及仅由这些类型组成的类型。不关心 pinning 的类型实现了 Unpin auto-trait,取消了 Pin<P>. 对于 T: Unpin, Pin<Box<T>>Box<T> 函数相同,Pin<&mut T>&mut T

请注意,固定和 Unpin 仅影响指向类型 P::Target,而不影响包含在 Pin<P> 的指针类型 P 本身. 例如,是否 Box<T>UnpinPin<Box<T>> 的行为没有影响(这里,T 是指向类型)。

示例:自引用结构体

在我们详细解释与 Pin</code> 相关的保证和选择之前 Pin<P>,我们讨论了一些如何使用它的例子。 请随意 跳到理论讨论的地方继续

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::ptr::NonNull;

// 这是一个自引用结构体,因为切片字段指向数据字段。
// 我们无法通过正常的引用将其告知编译器,因为无法使用通常的借用规则来描述此模式。
//
// 取而代之的是,我们使用一个裸指针,尽管我们知道它指向的是一个不为 null 的指针。
//
struct Unmovable {
    data: String,
    slice: NonNull<String>,
    _pin: PhantomPinned,
}

impl Unmovable {
    // 为了确保函数返回时数据不会移动,我们将其放置在堆中,以保留对象的生命周期,唯一的访问方法是通过指向它的指针。
    //
    //
    fn new(data: String) -> Pin<Box<Self>> {
        let res = Unmovable {
            data,
            // 我们仅在数据到位后创建指针,否则数据将在我们开始之前就已经移动
            //
            slice: NonNull::dangling(),
            _pin: PhantomPinned,
        };
        let mut boxed = Box::pin(res);

        let slice = NonNull::from(&boxed.data);
        // 我们知道这是安全的,因为修改字段不会移动整个结构体
        unsafe {
            let mut_ref: Pin<&mut Self> = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).slice = slice;
        }
        boxed
    }
}

let unmoved = Unmovable::new("hello".to_string());
// 只要结构体没有移动,指针应指向正确的位置。
//
// 同时,我们可以随意移动指针。
let mut still_unmoved = unmoved;
assert_eq!(still_unmoved.slice, NonNull::from(&still_unmoved.data));

// 由于我们的类型未实现 Unpin,因此无法编译:
// let mut new_unmoved = Unmovable::new("world".to_string());
// std::mem::swap(&mut *still_unmoved, &mut *new_unmoved);
Run

示例:侵入式双向链表列表

在侵入式双向链表中,集合实际上并未为元素本身分配内存。 分配由客户端控制,元素可以驻留在比集合短的栈框架上。

为了使此工作有效,列表中的每个元素都有指向其前任和后任的指针。元素只能在固定时添加,因为四处移动元素会使指针无效。此外,链表元素的 Drop 实现将修补其前任和后继的指针以将其从列表中删除。

至关重要的是,我们必须能够依靠被调用的 drop。如果在不调用 drop 的情况下可以释放元素或使元素无效,则来自其相邻元素的指针将变为无效,这将破坏数据结构体。

因此,固定还会附带 丢弃 相关的保证。

Drop 保证

固定的目的是能够依靠某些数据在内存中的放置。 为了使这项工作有效,不仅限制了移动数据,还限制了数据的传输。限制用于存储数据的内存的重新分配,重新分配用途或以其他方式使之无效。 具体来说,对于固定的数据,必须保持不变,即从固定 its memory 到调用 drop,*its memory 都不会失效或重新使用。只有 drop 返回或 panics,才可以重用该内存。

内存可以通过释放为 “invalidated”,也可以通过将 Some(v) 替换为 None,或将 vector 中的某些元素从 Vec::set_len 调用到 “kill”。可以通过使用 ptr::write 覆盖它来重新利用它,而无需先调用析构函数。在不调用 drop 的情况下,不允许对固定数据进行任何此操作。

这正是上一节中的侵入式链表需要正确执行函数的一种保证。

请注意,此保证不代表内存不会泄漏! 永远不要在固定元素上调用 drop 仍然是完全可以的 (例如,您仍然可以在 Pin<Box<T>>)。在双向链表的示例中,该元素将仅保留在列表中。但是,您不得释放或重用调用 drop* 的存储 *without。

Drop 实现

如果您的类型使用固定 (例如上面的两个示例),则在实现 Drop 时必须小心。drop 函数采用 &mut self,但这被称为即使您的类型之前已固定! 好像编译器自动调用了 Pin::get_unchecked_mut

这绝不会导致安全代码出现问题,因为实现依赖于固定的类型需要不安全的代码,但请注意,决定在您的类型中使用固定 (例如通过在 Pin<&Self>Pin<&mut Self> 上实现某些操作) 会对您的 Drop 实现也是如此: 如果您的类型的元素可以被固定,您必须将 Drop 视为隐式采用 Pin<&mut Self>

例如,您可以按如下方式实现 Drop

impl Drop for Type {
    fn drop(&mut self) {
        // `new_unchecked` 可以,因为我们知道这个值在被丢弃后再也不会使用了。
        //
        inner_drop(unsafe { Pin::new_unchecked(self)});
        fn inner_drop(this: Pin<&mut Type>) {
            // 实际丢弃的代码在此处。
        }
    }
}
Run

函数 inner_drop 具有 应该 具有 drop 的类型,因此可以确保您不会以与固定冲突的方式意外使用 self/this

此外,如果您的类型是 #[repr(packed)],则编译器将自动移动字段以将其删除。它甚至可以对恰好足够对齐的字段执行此操作。因此,您不能使用 #[repr(packed)] 类型的固定。

投影和结构固定

在使用固定结构体时,问题是如何在只需要 Pin<&mut 结构体> 的方法中访问该结构体的字段。 通常的方法是编写辅助方法 (所谓的 projections),将 Pin<&mut 结构体> 转换为对字段的引用,但该引用应该具有什么类型? 是 Pin<&mut Field> 还是 &mut Field? enum 的字段以及在考虑 container/wrapper 类型 (例如 Vec<T>, Box<T>,或 RefCell<T>. (此问题适用于可变引用和共享引用,我们仅在此处使用可变引用的更常见情况进行说明。)

事实证明,实际上是由数据结构的作者决定特定字段的固定 projection 是将 Pin<&mut 结构体 > 转换为 Pin<&mut Field>&mut Field.但是有一些约束,最重要的约束是 consistency: 每个字段都可以 或者 投影到固定的引用,或者 * 可以删除固定作为投影的一部分。 如果两者都是针对同一个字段进行的,那很可能是不合理的!

作为数据结构体的作者,您可以为每个字段决定是否将 “propagates” 固定到该字段。 传播的固定也称为 “structural”,因为它遵循该类型的结构体。 在以下各小节中,我们描述了两种选择都必须考虑的因素。

Pinning 不是用于结构体的 field

固定结构体的字段可能不被固定似乎违反直觉,但这实际上是最简单的选择:如果从未创建 Pin<&mut Field> 则不会出错! 因此,如果您确定某个字段不具有结构固定,则只需确保您从未创建对该字段的固定引用即可。

没有结构固定的字段可能具有将 Pin<&mut 结构体 > 转换为 &mut Field 的 projection 方法:

impl Struct {
    fn pin_get_field(self: Pin<&mut Self>) -> &mut Field {
        // 可以,因为 `field` 从未被视为固定。
        unsafe { &mut self.get_unchecked_mut().field }
    }
}
Run

您也可以 impl Unpin for Struct 即使 field 的类型不是 Unpin. 当没有创建 Pin<&mut Field>时,该类型对固定的看法 Pin<&mut Field>

Pinning 是结构体的 field

另一个选择是确定钉扎是 field 还是 field,这意味着如果钉扎结构体,则字段也钉扎。

这允许编写一个创建 Pin<&mut Field> 的 projection,从而见证该字段被固定:

impl Struct {
    fn pin_get_field(self: Pin<&mut Self>) -> Pin<&mut Field> {
        // 可以,因为 `self` 固定在 `field` 上。
        unsafe { self.map_unchecked_mut(|s| &mut s.field) }
    }
}
Run

但是,结构固定需要一些额外的要求:

  1. 如果所有结构字段均为 Unpin,则结构体必须仅为 Unpin。这是默认值,但 Unpin 是一个安全的 trait,因此作为结构体的作者,您有责任添加类似 impl[Unpin] for 结构体 <T>. (请注意,添加投影操作需要不安全的代码,因此 Unpin 是安全的 trait 的事实并没有破坏您只需要在使用 unsafe 时担心任何这些的原则。)

  2. 结构体的析构函数不得将结构域移出其参数。这正是 上一节 中提出的要点: drop 采用 &mut self,但是结构体 (以及它的字段) 之前可能已经被固定了. 您必须保证不会在您的 Drop 实现中移动任何字段。特别是,如前所述,这意味着您的结构体 不能#[repr(packed)]。 有关如何编写 drop 的方法,请参见该部分,以使编译器可以帮助您避免意外破坏固定。

  3. 您必须确保遵守使用 Drop 保证: 一旦固定了您的结构体,包含内容的内存就不会被覆盖或释放,而无需调用内容的析构函数。 这可能很棘手,正如 VecDeque<T> 的析构函数 VecDeque<T>, 如果析构函数 panics 之一,则可能无法在所有元素上调用 drop。这违反了 Drop 保证,因为它可能导致元素在没有调用析构函数的情况下被释放。 (VecDeque<T> 没有固定 projection,所以这不会导致不稳定。)

  4. 固定类型时,不得提供可能导致数据移出结构字段的任何其他操作。例如,如果结构体包含一个 Option<T>并且有一个类似 take 的操作,类型为 fn (Pin<&mut 结构体 >) -> Option<T> fn (Pin<&mut 结构体 >) -> Option<T>,该操作可用于将 T 从固定的 Struct<T> 中移出 - 这意味着固定不能对保存此数据的字段进行结构化。

    有关将数据移出固定类型的更复杂示例,请想象如果 RefCell<T>有一个方法 fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut T>。 然后,我们可以执行以下操作:

    fn exploit_ref_cell<T>(rc: Pin<&mut RefCell<T>>) {
        { let p = rc.as_mut().get_pin_mut(); } // 在这里,我们可以固定访问 `T`。
        let rc_shr: &RefCell<T> = rc.into_ref().get_ref();
        let b = rc_shr.borrow_mut();
        let content = &mut *b; // 这里我们有 `&mut T` 到相同的数据。
    }
    Run

    这是灾难性的,这意味着我们可以先固定 RefCell<T> (使用 RefCell::get_pin_mut ) 然后使用我们稍后获得的 RefCell::get_pin_mut 引用移动该内容。

Examples

对于像 Vec<T> 这样的类型,两种可能性 (结构固定与否) 都有意义。Vec<T> 使用结构固定可以有 get_pin/get_pin_mut 方法来固定引用到元素。但是,它可能不允许在固定的 Vec<T> 上调用 pop因为那会移动 (结构固定的) 内容! 它也不允许 push,它可能会重新分配并因此也移动内容。

Vec<T>没有结构固定可以 impl[Unpin] for Vec<T> impl[Unpin] for Vec<T>,因为内容永远不会被固定并且 Vec<T> 本身也可以移动。 那时,固定对 vector 完全没有影响。

在标准库中,指针类型通常不具有结构固定,因此它们不提供固定投影。这就是为什么 Box<T>: Unpin 适用于所有 T。对指针类型这样做是有意义的,因为移动 Box<T> 实际上并没有移动 T: Box<T> 即使 T 不是,也可以自由移动 (又名 Unpin)。 事实上,即使 Pin<Box<T>>Pin<&mut T> 总是 Unpin 本身,原因相同: 它们的内容 (T) 是固定的,但指针本身可以在不移动固定数据的情况下移动。 对于 Box<T>Pin<Box<T>>,内容是否固定完全独立于指针是否固定,意味着固定是结构的。

当实现 Future 组合器时,通常需要对嵌套的 futures 进行结构钉扎,因为您需要将引用的钉扎到 poll 上。 但是,如果您的组合器包含任何其他不需要固定的数据,您可以使这些字段不是结构化的,因此即使您只有 Pin<&mut Self> (例如在您自己的 poll 实现中)。

Macros

  • 通过在本地固定 value: T 来构建 Pin<&mut T>

Structs

  • 固定的指针。