每当学习一门新的编程语言时,我一定会首先搞明白一件事情:当写下那个再熟悉不过的 = 时,背后究竟发生了什么?不同于许多语言背后的 Deep-copy 或 Ref-count +1,Rust 非常少见地为 = 赋予了 Move 语义,并在此基础上延伸出了 Ownership 和 Borrow 的概念,进而搭起了这座名为 Safety 的大厦。不过在 Move 作为一等公民的光鲜亮丽背后,同样有阴暗面的存在:这就是 Pin,一个让我入门一次“懂”一次、下次还得再入门的有趣概念。

Move 这么好,我可以一直 Move 吗?

Rust 借助 Move 清晰地表达了一个对象 Ownership 的转移,与此同时,也不会带来任何隐式的构建、分配或销毁。这是因为 Move 背后做的事情非常简单:一次 Shallow Copy,即拷贝了对象栈上的内存。例如,当我们向一个函数传递一个 Vec 参数时,如果不考虑编译器优化,可以简单地认为 Rust 事实上是将 Vec 内部的指针、长度和容量这三个 usize 复制到被调用函数的栈帧上,进而重组成函数的形参,同时标记实参的 Ownership 不再可用。

既然 Move 用起来这么香,我可以一直 Move 吗?Hmm… 事情似乎并没有这么简单。想象我们有一个 struct,其内部的 Field 互相之间存在引用:

struct SelfReferencial {
    array: [i32; 10],
    elem_ref: *mut i32,
}

impl SelfReferencial {
    fn new() -> Self {
        let mut this = Self {
            array: [0; 10],
            elem_ref: std::ptr::null(),
        };
        this.elem_ref = &this.array[0] as *mut i32;
        this
    }
}

在我们第一次构造一个 SelfReferencial 对象时,elem_ref 指向了 array 的第一个元素,一切似乎都符合我们的预期。然而,一旦 SelfReferencial 发生了 Move,array 就可能被拷贝到另一个地址上去,elem_ref 便会指向一个无效的地址,进而之后任何试图通过解引用 elem_ref 的行为都可能导致内存不安全发生。事实上,这段短短的 new 函数就已经踩了一次雷:当构造好的 SelfReferencial 被返回给 Caller 时就已经发生了一次 Move,所以我们永远不能用它正确地构造出我们想要的内存结构(😄)。

不过,上面的例子看起来实在是没有什么意义。然而在实际编程中,Self Referencial Type 有许多真实有意义的场景,其中最为重要的便是:它在 Async Rust 编程中随处可见!如果读者了解 Async Rust 无栈协程的模型,便知道我们写出的 async fn 都会被编译成一个 Future 状态机,直观来讲,其每一种状态都对应着函数中的某一个 await 点,并捕获着当时跨越这个 await 点的栈上临时变量。临时变量间的互相引用,这件再正常不过的事情,在状态机的匿名 Struct 内就会成为哈人的 Self Reference!为了保证其内存结构不被破坏,我们必须保证不能随意地在内存中 Move 这个状态机。

有什么办法能让我们形式化地避免这类坏事情发生吗?这就是 Pin 的用武之地。

Pin 的契约

Pin,顾名思义,就是将一个对象“钉”在内存中的某一个位置,让人不能(轻易地)把它 Move 到另一个位置上。在实现上,Pin 是一个另类的智能指针;它包装了一个(智能)指针 P<T>,但把指针 DerefT 的能力藏了起来:只要不给你 &T&mut T,赋值和 mem::replace 就通通用不了,阁下还有什么手段把 T Move 走,搞坏其中的 Self Referencial 内存结构呢?

在日常编码中,常见的 Pin 的形状主要是 Pin<Box<T>>Pin<&mut T>,分别代表对 T 的所有权和借用。尽管我们已经了解了 Pin 的基本含义和工作原理,但当它们真正出现在我们面前时,我们应如何解读其背后的含义呢?用尽可能通俗的语言来说:

  • 当作为 Caller 去构造一个 Pin 时,我们是在像之后所有的 Callee 承诺:从此刻开始一直到 T 被 Drop,我们都不会随意地 Move T,进而保证其内存结构始终有效。
  • 当作为 Callee 在参数中写一个 Pin 时,我们便是在要求 Caller 提供这样的保证;同时因为自己只能拿到被 Pin 包装后的指针,也相当于承诺自己也会尊重 Pin 的约束,而不破坏 T 的内存结构。

显然,如果 Pin 真的是把其中的 T 藏的严严实实,使得我们只能像过家家一样玩这套传来传去的“契约小游戏”,而无法在 T 身上做任何有意义的 Mutation,它就完全没有存在的价值了。事实上,Pin 只是将获取 P<T> 的能力藏到了 Unsafe 接口里。可以想象,在调用栈的最底层,一定有人通过这些 Unsafe 接口在真正地做事情。

impl<'a, T> Pin<&'a mut T> {
    /// Gets a mutable reference to the data inside of this `Pin`.
    pub unsafe fn get_unchecked_mut(self) -> &'a mut T;
    /// Construct a new pin by mapping the interior value.
    pub unsafe fn map_unchecked_mut<U, F>(self, func: F) -> Pin<&'a mut U>;
}

impl<P: Deref> Pin<P> {
    /// Unwraps this `Pin<P>` returning the underlying pointer.
    pub unsafe fn into_inner_unchecked(pin: Pin<P>) -> P;
}

在大体上了解了 Pin 的契约意义后,接下来就让我们一起来看看 Pin 从构造、使用到销毁的一生。

构造 Pin

为了构造一个 Pin,我们只需要调用一个 Unsafe 函数,传入需要包装的指针 P

impl<P: Deref> Pin<P> {
    /// Construct a new Pin<P> around a reference to some data of a type
    /// that ...🤫.
    pub unsafe fn new_unchecked(pointer: P) -> Pin<P>;
}

函数签名中的 unchecked 赤裸裸地展现了编译器对我们的不信任,因为它并没有能力验证我们是否真正地 Pin 住了指针背后的对象本身。例如,我们可以私藏另一个指向同一个对象的指针(如 Rc<T> ),然后在做出 Pin 的承诺之后,悄悄地用这个私藏的指针再次获得 T 的原始引用;甚至,我们可以传入一个自己实现的指针,并在其 DerefDerefMut 的实现中参杂 Move 的私货。

不过,真的没有安全的办法来构造 Pin 而给出这样的承诺吗?值得指出的是,当我们在讨论 Pin 时,“让它不能被 Move”只是手段,而“不破坏其内存结构”才是根本目的。如果我们已经确切知道一个类型 T 无论怎样被 Move 都不可能导致其内存结构受到影响,那我们便可以安全地调用 new 将它放到 Pin 里,即给出“内存结构不被破坏”的承诺。此时,上述讨论的 new_unchecked 安全性问题都被认为不再重要了。

impl<P> Pin<P>
where
    P: Deref,
    <P as Deref>::Target: Unpin,
{
    /// Construct a new `Pin<P>` around a pointer to some data of a type
    /// that implements `Unpin`. <- 🤔
    pub fn new(pointer: P) -> Pin<P>
}

Unpin:不不 Move?

Unpin 这个 Trait 便是 Rust 对“不在乎自己是否被 Move”的概念抽象,用正式点的术语来讲,它“取消了 Pin 的保证/承诺”。即使我已经在编码中接触了它无数遍,也不得不承认 Unpin 这个名字实在是有些晦涩。不过,多绕几个弯似乎也能理解:Pin 就像是“不能乱 Move”,Unpin 则是说“不把它 Pin 住也没事”,也就是它可以“安全地 Move”。

幸运的是,除了 Self Referencial Type 之外,我们用 Safe Rust 所能接触的其它类型几乎都不在乎自己是否会被 Move,即它们都实现了 UnpinUnpinSend/Sync 一样,是一个 Auto Trait,只要一个 Struct 的 Field 都实现了 Unpin,则 Struct 本身也会自动实现 Unpin

不难理解,在面对实现了 Unpin 的类型时,Pin 总是能网开一面,而并不约束我们必须蹑手蹑脚地行事。这就是为什么 PinUnpin 的 Bound 下提供了安全的 new 方法。事实上,不仅仅是构造,在面对 Unpin 类型时,Pin 也会将其内部雪藏的指针 P<T> 放心大胆地交出来,使得我们可以通过 Safe 的接口实现与上文提到的 xxx_unchecked 相同的能力。

impl<'a, T> Pin<&'a mut T> {
    /// Gets a mutable reference to the data inside of this `Pin`.
    pub fn get_mut(self) -> &'a mut T
    where
        T: Unpin;
}

impl<P> Pin<P>
where
    P: Deref,
    <P as Deref>::Target: Unpin,
{
    /// Unwraps this `Pin<P>` returning the underlying pointer.
    pub fn into_inner(pin: Pin<P>) -> P
}

不过,只有 Unpin 的世界还是太理想了:讨论并理解 Pin 的难点一定是在那些 !Unpin (即没有实现 Unpin)的类型上。是的,又多一层否定(🥵)!不过如今我们应该不难理解这个概念,即回到了我们此前讨论的 Self Referencial Type 上:那些真正在意自己被 Move 会导致内存结构被破坏的类型。

Pin 在堆上:Box::pin

前面我们提到,Rust 将 Pin 的构造设计为 Unsafe 是因为不信任我们传进来的智能指针实现,但自家的指针总是能多少信任一下的(😄)。Box,智能指针中的清流,带有 Ownership 所以不可能私藏另一个指针,Deref 实现也不带任何私货。因此,Rust 为我们提供了 Safe 的 Box::pin 方法帮助我们构造一个 Pin<Box<T>>,其实现和 Box::into_pin(Box::new(x)) 完全一致。而 Box::into_pin,实际上就是对 Pin::new_unchecked 的简单封装。

impl<T> Box<T> {
    /// Constructs a new `Pin<Box<T>>`.
    /// If `T` does not implement `Unpin`, then `x` will be pinned in memory and unable to be moved.
    pub fn pin(x: T) -> Pin<Box<T>>;

    /// Converts a `Box<T>` into a `Pin<Box<T>>`.
    /// If `T` does not implement `Unpin`, then `*boxed` will be pinned in memory and unable to be moved.
    pub fn into_pin(boxed: Box<T>) -> Pin<Box<T>> {
        // It's not possible to move or replace the insides of a `Pin<Box<T>>`
        // when `T: !Unpin`, so it's safe to pin it directly without any
        // additional requirements.
        unsafe { Pin::new_unchecked(boxed) }
    }
}

是否觉得 Box::pin 这个方法异常眼熟?没错,在编写 Async Rust 代码时,我们或许经常遇到长达数百行的 “require … to be Unpin” 报错,但此时我们总是能在最后看到一条提示:“consider using Box::pin”。不得不承认的是,这剂灵丹妙药它真的管用!这是因为任何类型 T 被装箱为 Box<T> 后,Move Box 这个指针本身都不再会导致 T 在堆上受到任何影响,因此我们非常粗暴地得到了:

impl<T: ?Sized> Unpin for Box<T> {}

由于 PinUnpin 是 Auto Trait 实现,因此 Pin<Box<T>> 也一定实现了 Unpin。也就是说,我们总是能够通过 Box::pin 来让一个东西变成 Unpin 并同时给出 Pin 的承诺,因此常常被应用在 FutureStream 的处理上。

Pin 在栈上:pin!

调用 Box::pin 一定会将 T 从栈上 Move 到堆上,进而造成分配的开销。不过即使 T 在栈上,我们也是可以拿到 &mut T 这样的指针以通过 Pin::new_unchecked 来构造 Pin<&mut T>

然而,这样的接口无法保证 Safety,其原因正是我们曾在上文提过的:构造 Pin<P<T>> 时必须要将 Pin 的承诺从此刻起贯彻到 T 被 Drop 为止。然而根据 Rust 的 Borrow Checker,&mut T 的 Lifetime 需要被包含在 T 之内。因此,当 Pin<&mut T> 的 Scope 结束后,我们便又有机会重新访问 T 本身,甚至从其上得到裸的 &mut T 以调用 mem::replace,打破这一承诺。

let mut fut = MyFuture::default();

let pinned_fut: Pin<&mut MyFuture> = unsafe { Pin::new_unchecked(&mut fut) };
do_something(pinned_fut);

let _moved_fut = std::mem::take(&mut fut); // <- COMPILED: Moved then dropped 😱

为此,Rust 标准库在最近的版本中提供了 std::pin::pin!来防止这种情况发生,进而使得 Pin 在栈上也可以成为 Safe 的操作。使用 pin! 来实现上面相同的代码,便可以得到这样的编译错误,阻止我们打破 Pin 的承诺:

let fut = MyFuture::default();

let pinned_fut: Pin<&mut MyFuture> = std::pin::pin!(fut);
do_something(pinned_fut);

let _moved_fut = std::mem::take(&mut fut); // ERROR: borrow of moved value: `fut`

Pin 在栈上的操作不得不使用宏来实现,是因为为了达成这样的效果,必须要使用一些 Lifetime 和 Scope 的“微操”黑科技。例如,std::pin::pin! 就使用了 Temporary Lifetime Extension 的特性。无独有偶,我们更为熟悉的 futures::pin_mut! 利用了 Name Shadowing 的规则,使得后构造的 Pin<&mut T> 和原来的 T 具有相同的名字,这样无论如何也不能再次访问到 T

使用 Pin

以 Future 为例

Structural or not?

销毁 Pin

PinnedDrop