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,
}
相关推荐
doiito31 分钟前
【Agent Harness】Gliding Horse 核心设计理念,不跟风开发自己的AI Agent
ai·rust·架构设计·系统设计·ai agent
花褪残红青杏小10 小时前
Rust图像处理第6节- 均值模糊 & 中值模糊:3×3 邻域的两种经典玩法
rust·webassembly·图形学
子兮曰14 小时前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
星栈16 小时前
写 Dioxus Demo 不难,难的是把它写成项目
前端·rust·前端框架
mCell18 小时前
【锐评】桌面端技术营销:别拿跑分当工程判断
前端·rust·electron
武子康1 天前
调查研究-201 Rust 里的 dev build 和 release build:为什么同一份代码性能差这么多?
后端·架构·rust
doiito1 天前
【Agent Harness】Gliding Horse 的 L2 作战地图:让多 Agent 协作从“摸黑”变成“透明”
ai·rust·架构设计·系统设计·ai agent
星栈2 天前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:再把新建、编辑和交付补上
前端·rust·前端框架
独孤留白2 天前
从C到Rust:基本类型 C 的隐式不确定 vs Rust 的显式确定
rust
清晨很温柔啊2 天前
# 用 Rust 手搓 AI 自演化主板:当 18 个异构器官长出 C++ 骨骼
rust