Rust之数据固定Pin与Unpin

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

  • data 被移动到了新地址
  • ptr 仍然指向旧地址 ❌(悬垂指针)
rust 复制代码
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

rust 复制代码
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 )
  • 这样就保证了值的内存地址稳定
rust 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     内存安全保证                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. Box::pin() 分配堆内存,值被钉住                          │
│                     │                                       │
│                     ▼                                       │
│   2. PhantomPinned 标记类型为 !Unpin                         │
│                     │                                       │
│                     ▼                                       │
│   3. 编译器阻止通过安全代码获取 &mut T                         │
│                     │                                       │
│                     ▼                                       │
│   4. 必须用 unsafe { get_unchecked_mut() } 才能修改           │
│                     │                                       │
│                     ▼                                       │
│   5. 程序员承诺:只初始化,不移动                              │
│                     │                                       │
│                     ▼                                       │
│   6. 自引用指针永远有效                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Pin!

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

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

ini 复制代码
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 结构体会包含一个指向自身内部字段的指针

csharp 复制代码
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 前不会变动。

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

rust 复制代码
use std::marker::PhantomPinned;

struct MyType {
    data: String,
    _pin: PhantomPinned,
}
相关推荐
不爱学英文的码字机器17 小时前
[鸿蒙PC命令行移植适配]移植rust三方库bottom到鸿蒙PC的完整实践
华为·rust·harmonyos
W_LuYi18518 小时前
Tauri + Rust + Vue 3 打造极速轻量桌面应用
java·开发语言·vue.js·rust
星栈19 小时前
Makepad 界面怎么做得更像产品,而不是示例
前端·rust
特立独行的猫a19 小时前
MQTT Client的Tauri应用移植到 OpenHarmony 鸿蒙 PC/ARM64 实践记录
mqtt·华为·rust·harmonyos·tauri·移植·鸿蒙pc
techdashen20 小时前
深入理解 Rust Futures:从零开始,一头扎到底
开发语言·后端·rust
fox_lht21 小时前
GPUI 框架完整学习教程
学习·rust·gpui
好家伙VCC21 小时前
Rust+Bioinfo:80ms极速SNP注释引擎
java·开发语言·算法·rust
吴佳浩1 天前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
小宇子2B2 天前
一个 Vec 的数据到底在内存哪:栈、堆,和它们相向而行的真相
后端·编程语言
mit6.8242 天前
并发协调的代价
rust