在前面介绍Future模式的文章里, 简单讲了下poll方法里的入参Pin类型, 引入Pin是为了解决自指类型的问题, "固定"Future在内存中的位置. 这篇文章就来介绍自指类型是什么, 和Future有什么关系以及Pin是如何解决问题的这些内容.

自指类型是什么, 会有什么问题

struct SelfReference {
  data: i32,
  // pointer指向data
  pointer: * const i32
}

let mut a1 = SelfReference { data: 1, pointer: std::ptr::null() };
// a1.pointer 指向了 a1.data
a1.pointer = &a1.data;
// SelfReference { data: 1, pointer: 0x7fffffffd9d0 }, 1
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }

自指类型也叫做自引用类型, 指的是包含指向自身空间的指针的类型. 也就是说, 自指类型的实例里会有指针字段, 这个指针指向的地址又是和实例本身相关的. 比如上面的SelfReference就是自指类型, pointer字段指向了data字段, 把pointer指向的内容和data打印出来的结果是一样的.

let mut a1 = SelfReference { data: 1, pointer: std::ptr::null() };
a1.pointer = &a1.data;
// SelfReference { data: 1, pointer: 0x7fffffffd7f0 }, 1
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }
let mut a2 = SelfReference { data: 0, pointer: std::ptr::null() };
a2.pointer = &a2.data;
// SelfReference { data: 0, pointer: 0x7fffffffd850 }, 0
unsafe { println!("{:?}, {}", a2, *(a2.pointer)); }

// 交换 a1 和 a2 的内容, 相当于 a1 和 a2 的内存地址变更了
std::mem::swap(&mut a1, &mut a2);
// SelfReference { data: 0, pointer: 0x7fffffffd850 }, 1 
unsafe { println!("{:?}, {}", a1, *(a1.pointer)); }
// SelfReference { data: 1, pointer: 0x7fffffffd7f0 }, 0
unsafe { println!("{:?}, {}", a2, *(a2.pointer)); 

当实例在内存中的位置移动(比特复制)时, 自指类型就会出现问题, 比如swap交换上面的a1和a2, 把a1的数据移到了a2的位置, a2的数据移到了a1的位置. a1中现在data是0, 而pointer指向的内容还是1, 因为pointer指向的不是"自己"的data了, a2也是同样的情况.

一方面, 会导致实例在内存中被移动的情况很常见, 比如函数入参, 数组扩容, swap, Box::new等等; 另一方面, 虽然自指类型不算常见, 但Rust并不限制我们定义自指类型, 而且在Future中出现了自指类型的身影. 所以对于这种可能导致未定义行为的陷阱, Rust应该会觉得有必要做些什么.

自指类型和Future有什么关系

async fn foo() {
  let a = 1;
  let b = &a;
  // 任务在这会让出执行
  future1().await;
  // a和b是跨await的变量
  let c = a + *b;
}

之前的文章说到编译器会把异步函数转换成Future实现, 指令和数据信息都会保存在状态机中. 在上面的异步函数中, a和b是跨域await点的变量, 状态机在下次继续执行时需要用到这两个变量, 所以a和b需要保存在状态机中, b又是指向a的指针, 所以编译器生成的匿名Future就会是自指类型.

不仅是Future, 各种各样的迭代器, 还有正在开发中的生成器, 都是用状态机来实现的. 如果这些状态机要维护的状态里, 有指向自身的指针字段, 那就变成了自指类型, 都会存在内存移动时出现问题. 由于历史原因, 迭代器Iterator的next方法里并不是Pin, 这和Future的poll方法不同, 但最近实现的生成器Coroutine的resume方法则已引入了Pin.

Pin的定义和工作原理

虽然显式的自指类型并不常见, 但Future, Iterator和Coroutine这些类型对Rust来说是如此重要, 所以自指类型会造成的潜的影响是如此广泛. 自指类型出问题发生在内存位置移动时, 所以要解决问题一方面可以从类型入手, 另一方面可以从"移动"入手.

Rust目前选择的方向是处理"移动", 给出的解决方案是Pin. 在开始介绍Pin前, 有必要先介绍下Unpin和!Unpin这两个标记类型, 根据类型实例在内存中移动是否安全, Rust把所有类型归入这两类.

pub auto trait Unpin {}
// 为类型实现Unpin特征
impl Unpin for PinStruct {}

Unpin的意思是不需要固定, 也就是说实现了Unpin的类型在内存中移动是安全的. 几乎能想到的类型, 比如i32, char, String等等, 都是Unpin类型. 因为在内存中移动Unpin类型不会导致未定义行为, 所以Unpin不需要Pin提供额外的保障, Pin对于Unpin类型的影响就像透明一样.

// 方式1: 显示实现 !Unpin
struct NotUnPinStruct1;
impl !Unpin for NotUnPinStruct1 {}

// 方式2: 包含 !Unpin 的字段
struct NotUnPinStruct2 {
  _marker: PhantomPinned  
}

!Unpin与Unpin相反, 意思是需要固定, 实现了!Unpin的类型在内存中移动是不安全的. !Unpin类型并不常见, 有可能会遇见的, 一是上面的Future这类状态机, 另一个是标记类型PhantomPinned. 我们要自定义!Unpin类型的话, 可以手动为类型实现!Unpin, 也可以在类型里包含!Unpin的字段. 因为在内存中移动!Unpin类型会导致未定义行为, 所以Pin方案要处理的主要就是!Unpin类型.

pub struct Pin<Ptr> {
  pub __pointer: Ptr,
}

Pin的完整写法是Pin<P<T>>, 其中Pin是"外层"指针, P是"中级"的指针. Pin<P<T>>给出的承诺是, 如果使用者按约定构造了Pin, 那么从T被Pin住开始到被释放, T在内存中的位置都不会改变(T为Unpin时不限制).

仅借助Rust的语法是没办法实现Pin应该提供的承诺的, 所以Pin需要使用者遵守一定的约定, 约定的内容针对"中级"的P指针的, 有两条:

  1. P指针的解引用方法derefderef_mut不能移动T, 因为Pin的as_refas_mut会依赖于这两个方法
  2. Pin<P<T>>被释放后, P指针指向的T也不能再被移动. 因为Pin只能在自己的生命周期内起作用, 所以超出Pin生命周期的"承诺"需要靠"约定"来实现.
// T 是 Unpin 类型, 方法是安全的
impl<P: Deref<Target: Unpin>> Pin<P> {
  pub const fn new(pointer: P) -> Pin<P> { unsafe { Pin::new_unchecked(pointer) }
}

// T 可以是 !Unpin 的, 方法是不安全的
impl<P: Deref> Pin<P> {
  pub const unsafe fn new_unchecked(pointer: P) -> Pin<P> { Pin { pointer }
}

所以Pin提供了两个构造方法, 一个是针对Unpin的new方法, 是safe的, 因为Unpin在内存中移动是安全的, 不需要Pin提供额外的保障; 另一个是针对!Unpin的new_unchecked方法, 是unsafe的, 因为使用这个方法需要开发者遵守上面的约定, 这边没法通过Rust语法来确保正确性.

归纳一下在内存中移动T的情况, 可以分成两种类型, 一是有T的所有权, 二是有T的可变引用&mut T. 而在Rust中能拿到变量的所以权时, 也就可以间接拿到拿到可变引用, 所以这两种情况可以合而为一, 即要想在内存中移动T, 就要能拿到T的可变引用&mut T.

前文讲到Pin是从"移动"方面入手来解决自指类型的问题, 所以Pin的思路就是构造出Pin<P<T>>后, 就不再通过安全方法暴露&mut T, 下面就来看Pin提供给开发者的那些方法.

// T 是 Unpin 类型, 方法是安全的
impl<P: Deref<Target: Unpin>> Pin<P> {
  pub const fn into_inner(pin: Pin<P>) -> P { pin.pointer }
}

// T 可以是 !Unpin 的, 方法是不安全的
impl<P: Deref> Pin<P> {
  pub const unsafe fn into_inner_unchecked(pin: Pin<P>) -> P { pin.pointer }
}

首先是获取P<T>的两个方法, 针对Unpin类型的into_inner是安全的, 因为Unpin本身就可以安全地移动, 直接把P<Unpin>返回出去不会有问题. 而针对!Unpin类型的into_inner_unchecked是不安全的, 因为不确定P<!Unpin>会不会把&mut T暴露出去, 所以需要开发者自己保证.

// 如果 P 实现了 Deref, Pin<P<T>> -> Pin<& T>
impl<P: Deref> Pin<P> {
  pub fn as_ref(&self) -> Pin<&P::Target> { unsafe { Pin::new_unchecked(&*self.pointer) } }
}

// 如果 P 实现了 DerefMut, Pin<P<T>> -> Pin<&mut T>
impl<P: DerefMut> Pin<P> {
  pub fn as_mut(&mut self) -> Pin<&mut P::Target> { unsafe { Pin::new_unchecked(&mut *self.pointer) } }
}

接着是把Pin<p<T>>转换到Pin<&T>的方法, 即把中间这一层可能是各种类型的P指针, 转换到统一的引用&形式. 根据P是否是可变的, 有as_refas_mut两个方法. 这两个方法都是安全的, 因为从Pin<&T>拿不到T的可变引用.

// Pin<P<T>>调用as_ref转换成Pin<&T>后就可以调用
impl<'a, T: ?Sized> Pin<&'a T> {
  pub const fn get_ref(self) -> &'a T { self.pointer }

  pub unsafe fn map_unchecked<U, F>(self, func: F) -> Pin<&'a U> 
    where U: ?Sized, F: FnOnce(&T) -> &U 
  {
    let pointer = &*self.pointer;
    let new_pointer = func(pointer);
    unsafe { Pin::new_unchecked(new_pointer) }
  }
}

然后是Pin<& T>的两个方法. 第一个方法get_ref是把Pin这一层去掉返回&T, 因为无法通过不可变引用移动T, 所以这个方法是安全的. 第二个方法map_unchecked用来做映射, 这边因为入参func是不可控的, 所以func(pointer)得到的new_pointer不一定会遵守Pin的约定, 所以只能通过不安全的new_unchecked来构造Pin, 所以这个方法也是不安全的.

// Pin<P<T>>调用as_mut转换成Pin<&mut T>后就可以调用
impl<'a, T: ?Sized> Pin<&'a mut T> {
  pub const fn get_mut(self) -> &'a mut T where T: Unpin { self.pointer }

  pub const unsafe fn get_unchecked_mut(self) -> &'a mut T { self.pointer }

  pub unsafe fn map_unchecked_mut<U, F>(self, func: F) -> Pin<&'a mut U>
    where U: ?Sized, F: FnOnce(&mut T) -> &mut U { ... }

  pub const fn into_ref(self) -> Pin<&'a T> { Pin { pointer: self.pointer } }
}

最后是Pin<&mut T>的几个方法. get_mut, get_unchecked_mut和上面的get_ref一样都是去掉Pin这一层, 但因为返回的是可变引用&mut T, 所以根据T是Unpin还是!Unpin, 分成了安全方法get_mut和不安全方法get_unchecked_mut. map_unchecked_mutmap_unchecked的可变版本, 也是不安全的, 理由map_unchecked. into_ref方法则是扩展as_ref, 把Pin<&mut T>也转换到Pin<&T>, 从可变到不可变, 这个方法是安全的.

常见的Pin<P<T>>类型

  • Pin<&mut T>

    fn move_pinned_ref<T>(mut a: T, mut b: T) {
      unsafe {
        // 通过可变引用 pin 住 a
        let p: Pin<&mut T> = Pin::new_unchecked(&mut a);
      }
      // 还可以再次拿到 &mut a, 不满足 pin 的约定
      std::mem::swap(&mut a, &mut b);
    }
    

    通过&mut T构造的Pin<&mut T>并不满足第二个约定条件, 比如下面的示例代码, 在释放掉p后依然可以拿到&mut a, 从而可以移动a指向数据的位置. 尽管不满足Pin的约定, 但这段代码是可以通过编译的, 只是超出Pin生命周期后的承诺无法兑现. 对Rust来说, 这可能会导致库层面的未定义行为, 但不是语言层面的未定义行为, 所以在这里并不能提供更多的帮助, T可能是!Unpin的, 所以只能用不安全的new_unchecked方法.

  • Pin<Box<T>>

    fn pin(x: T) -> Pin<Box<T>> { unsafe { Pin::new_unchecked(Box::new(x)) } }
    

    Box<T>是满足Pin的两个约定的, 一是Box的解引用方法不会移动内部的T, 二是Pin释放后, Box也会被释放, 里面的T也直接释放掉了. Box提供了一个安全的包装方法, 不管T是Unpin的还是!Unpin的, 都可以直接用Box::pin(T)固定T.

  • Pin<Rc<T>>

    // Rc 提供的构造 Pin<Rc<T>> 的方法
    fn pin(value: T) -> Pin<Rc<T>> { unsafe { Pin::new_unchecked(Rc::new(value)) } }
    
    fn move_pinned_rc<T>(mut a: Rc<T>) {
      // 通过Rc指针 pin 住 a
      let pinned = unsafe { Pin::new_unchecked(Rc::clone(&a)) };
      {
        let p: Pin<&T> = pinned.as_ref();
      }
      drop(pinned);
      // 还可以再次拿到 &mut T, 不满足 pin 的约定
      let t: &mut T = Rc::get_mut(&mut a).unwrap();
    }
    

    虽然和Box一样, Rc也提供了安全的包装方法, 但Pin<Rc<T>>实际上并不太安全, 因为Rc不一定会满足第二点. 如上面的代码所示, Pin被释放后了, Rc也被释放, 但依然可以从其他的Rc拷贝里拿到&mut T.


-> 如果文章有不足之处或者有改进的建议,可以在这边告诉我,也可以发送给我的邮箱