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,
}
相关推荐
左小左3 小时前
🔥🔥🔥 我用AI基于 Tauri + Vue 3 写了个 ADB 桌面工具,把命令行的脏活全干了
android·vue.js·rust
kyriewen114 小时前
你的前端滤镜慢得像PPT?用Rust+WebAssembly,一秒处理4K图
开发语言·前端·javascript·设计模式·rust·ecmascript·powerpoint
小杍随笔4 小时前
【Tauri 2 + Rust 配置 WebView2 缓存数据存储到安装目录】
开发语言·后端·rust·tauri
代码羊羊5 小时前
Rust 迭代器完全通俗易懂指南(零基础全覆盖)
java·开发语言·rust
iDao技术魔方16 小时前
DeepSeek TUI:原生 Rust 打造的终端 AI 编码 Agent
开发语言·人工智能·rust
古城小栈16 小时前
封神!Rust 出品 HTTP 压测神器 cargo-whero,轻量碾压 JMeter、wrk,新手也能秒上手
jmeter·http·rust
kyriewen17 小时前
你的前端滤镜慢得像PPT?用Rust+WebAssembly,一秒处理4K图
前端·rust·webassembly
kyriewen1117 小时前
你等的Babel编译,够喝三杯咖啡了——用Rust重写的SWC,只需眨个眼
开发语言·前端·javascript·后端·性能优化·rust·前端框架
weixin_4217252621 小时前
【期末考试】一篇文章带你系统回顾C语言,轻松应对考试!
编程语言·知识点·期末考试·【c语言】·系统复习