Rust之数据固定Pin与Unpin

70 阅读4分钟

Rust 默认语义是:值可以被 move(按位搬迁)。在大多数类型上这是安全的,但存在一类特殊结构:自引用结构(self-referential struct)

  • data 被移动到了新地址
  • ptr 仍然指向旧地址 ❌(悬垂指针)
struct Bad {
    data: String,
    ptr: *const String,
}

let mut x = Bad {
    data: "hello".into(),
    ptr: std::ptr::null(),
};

x.ptr = &x.data;

let y = x; // 内存搬迁

Rust 没有 GC,也不跟踪引用修复,因此一旦对象进入“自引用状态”,就必须保证它的地址不再变化,这就是 Pin

若是没有 Pin,则很容易出现未定义行为(Undefined Behavior, UB)

Pin

Pin 是一个智能指针包装器; Pin 承诺:T指向的数据不会被移动;即程序必须确保 T 的析构器运行前,该引用没有被移:

  • 类型系统约束:Pin禁止获取 &mut T
  • 不允许: move(包括 mem::swap / replace)
  • 允许: 修改字段内容(只要不 move); 读写数据
  • unsafe 边界:若绕过 Pin 的约束,必须自己保证“不移动”

Box::Pin

Box::pin 将一个值放入 Box 中,并得到 Pin<Box<T>>。只要 T: 是!UnpinPin<Box<T>> 就会阻止从 Box 中移出 T

use std::marker::PhantomPinned;

struct SelfRef {
    data: String,
    ptr: *const String,
    
    // PhantomPinned 是一个零大小类型,它不实现 Unpin
    // 任何包含 PhantomPinned 的结构体都会变成 !Unpin
    _pin: PhantomPinned,
}

impl SelfRef {
    fn new(s: String-> Self {
        SelfRef {
            data: s,
            ptr: std::ptr::null(),
            _pin: PhantomPinned,
        }
    }

    fn init(&mut self) {
        self.ptr = &self.data as *const String;
    }

    fn get_ptr(&self-> &String {
        unsafe { &*self.ptr }
    }
}

fn main() {
    let mut pinned = Box::pin(SelfRef::new("hello".to_string()));
    
    // 使用 get_unchecked_mut()获取!Unpin的引用
    unsafe {
        pinned.as_mut().get_unchecked_mut().init();
    }
    println!("{}", pinned.get_ptr());
}

上面定义了一个自引用结构体 (self-referential struct);如果这个结构体被 移动 (move)到新的内存地址:

  • data 会在新地址
  • 但 ptr 仍然指向 旧地址
  • 访问 ptr 会导致 未定义行为 (use-after-free)

Pin 的关键在于:

  • 一旦值被 Pin 包裹,就无法安全地获得它的可变引用( &mut T )
  • 没有可变引用,就无法移动这个值(移动需要 &mut T )
  • 这样就保证了值的内存地址稳定
┌─────────────────────────────────────────────────────────────┐
│                     内存安全保证                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1Box::pin() 分配堆内存,值被钉住                          │
│                     │                                       │
│                     ▼                                       │
│   2. PhantomPinned 标记类型为 !Unpin                         │
│                     │                                       │
│                     ▼                                       │
│   3. 编译器阻止通过安全代码获取 &mut T                         │
│                     │                                       │
│                     ▼                                       │
│   4. 必须用 unsafe { get_unchecked_mut() } 才能修改           │
│                     │                                       │
│                     ▼                                       │
│   5. 程序员承诺:只初始化,不移动                              │
│                     │                                       │
│                     ▼                                       │
│   6. 自引用指针永远有效                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Pin!

pin! 宏或手动 Pin<&mut T> 将栈上的值钉住。

一旦用 pin! 钉住一个栈上变量,就不能再通过原变量名移动它,因为编译器会禁止对已经被钉住的变量进行移动操作。

use std::pin::pin;
let value = SelfRef::new("stack".to_string());
let mut pinned = pin!(value);
unsafe {
    pinned.as_mut().get_unchecked_mut().init();
}
println!("{}", pinned.get_ptr());

常见使用场景

异步编程与 Future 状态机

Rust 引入 Pin 的直接动力是:async/await 语法会被编译器生成为一个复杂的枚举状态机。

async 块中定义一个局部变量,并跨越 await 点引用它时,编译器生成的 Future 结构体会包含一个指向自身内部字段的指针

async fn example() {
    let mut buffer = [0u8; 1024];
    let mut reader = MyReader::new(&mut buffer); // reader 持有对 buffer 的引用
    reader.read().await// 挂起,此时 buffer 和 reader 都在 Future 结构体里
}

FFI 与 C 库交互

许多 C 库(如 libuv 或底层内核驱动)要求提供一个指向结构的指针,并由 C 库长期持有。

如果 Rust 端的对象因为作用域结束、重新分配或闭包捕获而被移动,C 库手中的指针就变成了悬空指针 (Dangling Pointer)。通过 Pin<Box<T>> 暴露给 C 接口,可以从类型系统层面承诺该内存在被 Drop 前不会变动。

rust-app-c.png

Unpin

Unpin 为标准库中的一个自动 trait(auto trait),默认“可移动”标记

  • 几乎所有类型默认都是 Unpin。
  • 只有显式选择不实现 Unpin 的类型才是 !Unpin
  • 如果 T: Unpin,那么 Pin<'a, T> 完全等价于 &'a mut T;意味着这个类型被移走也没关系,就算已经被固定了,即Pin 对这样的类型毫无影响。
类型是否可移动
T: Unpin✅ 可以 move
T: !Unpin❌ 一旦被 Pin,就不能 move

!Unpin

固定 !Unpin 类型到堆上,能给数据一个稳定的地址,指向的数据不会在被固定之后被移动走:

  • 如果 T: !Unpin, 获取已经被固定的 T 类型实例 &mut T需要 unsafe。
  • 对于 T: !Unpin 的被固定数据,必须保证数据内存的有效性从固定时起直到释放。

PhantomPinned 会自动让类型变成 !Unpin

use std::marker::PhantomPinned;

struct MyType {
    data: String,
    _pin: PhantomPinned,
}