Rust Pin Unpin 使用介绍

2024-02-26 language rust

简介

其中 Pin 是为了标识某个变量不能在内存中移动,而 Unpin 则允许移动。考虑异步编程中引用了某个变量,例如:

async {
    let mut data = [0; 256];
    let fetch_future = fetch_data(&mut data);
    fetch_future.await;
    println!("{:?}", data);
}

会转换为类似如下的数据结构。

struct DataBuff<'a> {
    buff: &'a mut[u8], // 指向下面的data
}
struct AsyncFuture {
    data: [u8; 256];
    fetch_future: DataBuff<'_>,
}

也就是在结构体内部会有指针指向自己,如果 AsyncFuture 被移动,那么对应的 data 地址也发生了变动,但是,引用地址却没有修改,从而会导致不合法访问。解决方式就是让 Future 不移动即可。

基本概念

Rust 1.33 版本中引入了 Pin 相关的 API 实现,包括了:std::pin::Pin std::marker::{Unpin, PhantomPinned} impl !Unpin for T,部分实现如下。

// core/src/pin.rs
pub struct Pin<Ptr> {
    pub __pointer: Ptr,
}

impl<Ptr: Deref> Deref for Pin<Ptr> {
    type Target = Ptr::Target;
    fn deref(&self) -> &Ptr::Target {
        Pin::get_ref(Pin::as_ref(self))
    }
}

impl<Ptr: DerefMut<Target: Unpin>> DerefMut for Pin<Ptr> {
    fn deref_mut(&mut self) -> &mut Ptr::Target {
        Pin::get_mut(Pin::as_mut(self))
    }
}

pub auto trait Unpin {}
pub struct PhantomPinned;
impl !Unpin for PhantomPinned {}

Pin 是一个结构体,并实现了 Deref DerefMut 特征,这也就意味着是一个智能指针,其内部只能包含一个指针 (只能是指针),假设其指向的是 T 类型,只要 T 没有实现 Unpin 特征,那么就能够保证 T 永远不会被移动。

这也就意味着,如果 T 实现了 Unpin 特征,那么 Pin 的作用也就完全失效了,此时 Pin<P<T>> 就等价于 P<T> 了。

但是编译器会默认给所有类实现 Unpin 特征,其中有两个例外,实现的是 !Unpin:A) PhantomPinned 以及作为成员的类;B) 为 async 自动生成的 impl Future 结构体。后者很容易理解,而前者实际就是为了用户可以 Pin 自定义类型。

使用 Safe Rust 的时候,不被移动的核心原理就是,避免暴露可变指针 &mut

示例

当某个类型声明了为 Pin 之后,可以确保实现 !Unpin 的结构体不发生移动。

use std::{marker::PhantomPinned, pin::Pin};

struct Data(u32);
impl Data {
    fn dump(&self) {
        println!("{} {:p}", self.0, self);
    }
}

struct PinData(u32, PhantomPinned);
impl PinData {
    // 通过Pin能确保打印的是相同值
    fn dump(self: Pin<&Self>) {
        println!("{} {:p}", self.0, self);
    }
}

fn main() {
    let heap = Box::new(Data(42)); // on heap
    heap.dump();
    let stack = *heap; // moved back to stack
    stack.dump();

    let pinned = Box::pin(Data(42));
    pinned.as_ref().dump();
    let unpined = Pin::into_inner(pinned);
    let stack = *unpined;
    let pinned = Box::pin(stack);
    pinned.as_ref().dump();

    let pinned = Box::pin(PinData(42, PhantomPinned));
    pinned.as_ref().dump();
    //let unpined = Pin::into_inner(pinned);  // 这里执行会失败
}

总结

  • 实现了 Unpin 特征,可以通过 Pin::get_mut() 或者 &mut T (因为实现了 DerefMut 特征) 获取,不确定具体使用场景。
  • 如果不想自定义结构体移动,添加 _marker: PhantomPinned 即可,或者应用 nightly 中的 !Unpin 特征。
  • 对于 Unsafe 代码可以强制通过 get_unchecked_mut() 获取 &mut T 类型,不过需要保证如下的契约。
  • 简单来说,Pin 用来标识 T 不会再被移动,Unpin 意味着允许移动,!Unpin 不允许取消 Pin 动作。
  • 使用 PhantomPinned 的作用是,一旦结构体被 Pin 了,那么就不能 Unpin

上述说的契约为,对于 Pin<P<T>> 类型:

  • T 实现了 Unpin,那么 P<T> 从被 Pin 到销毁,需要保证 P<T> 不被订住。
  • T 实现了 !Unpin,那么 P<T> 从被 Pin 到销毁,需要保证 P<T> 被订住。

参考

  • 对于 Pin 异步编程中的 Pinning 章节介绍,最后有相关的总结。