读懂 Pin,一次搞清 Rust 最难的指针

引言

这是Rust九九八十一难第13篇,介绍下pin指针。pin指针跟Future紧密相关,也算是多线程部分第三篇。关于这块,之前看了几篇文章,有评论这块难以理解,我也同感。忘记谁说的了,如果不能简单明了的描述某个东西,说明自己还没真正掌握。本篇尝试用简单方式整理Pin相关的知识,有问题请留言。

一、基本概念

入门pin,先要知道四个概念:pin,Unpin和!Upin,以及rust的move。通过这些概念的对比,理解pin的边界,在哪些范围发生作用。

1、Pin<T> 到底是什么

Pin是个智能指针包装器。比如:

rust 复制代码
use std::pin::Pin;

fn main() {
    let x = 10;
    let mut pinned = Pin::new(&x);
    println!("Pinned: {:?}", pinned);//Pinned: 10
}
  • 它包裹了一个类型 T(通常是放在堆上 的,如 Box<T>),对 !Unpin 类型 , 编译器会在语法层面阻止移动。
  • 简单说就是可以拿到 &T&mut T,但保证不会把整个 T 移到别的内存位置。

2、Unpin是什么?

Unpin 是一个 标记 trait,表示该类型可以安全地被移动。

  • 大多数普通类型(如 i32, String, Vec<T>默认实现Unpin

  • 但一些类型(如 Future、自引用结构)不会自动实现

    rust 复制代码
    fn need_unpin<T: Unpin>(x: T) {
        println!("可以安全移动");
    }

    如果某类型没有 Unpin,那么它就 必须被固定(pinned) 才能安全使用。

3、!Unpin是什么

!Unpin 就是 没有实现 Unpin 的类型 ,表示类型 不能随意移动!Unpin 并不是 Rust 的语法,而是"没有实现 Unpin 的类型"的意思。Rust 默认会自动为大部分类型实现 Unpin,只有少数类型(自引用、Future、PhantomPinned)才是 !Unpin

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

struct SelfRef {
    data: String,
    ptr: *const String,
    _pin: PhantomPinned, // 表示 !Unpin
}

fn main() {
    // 普通类型 Unpin
    let x = Box::new(42);
    let px = Pin::new(x);
    let moved_px = px; // ✅ 可以移动

    // 自引用类型 !Unpin
    let y = Box::pin(SelfRef {
        data: "hello".to_string(),
        ptr: std::ptr::null(),
        _pin: PhantomPinned,
    });
    // let moved_y = y; // ❌ 编译报错,不可移动
}

Unpin 类型 → 钉住也能搬,!Unpin 类型 → 钉住后无法搬。换句话**!Unpin 的作用是"固定对象的地址",而不是搬动堆上的数据**,如下图:

lua 复制代码
Stack                     Heap
+-------+                 +--------+
| a     | --------------> | "hello"|
| Box   |                 +--------+
+-------+
!Unpin 阻止 a 被 move
  • a(栈上的 Box)被 Pin , 栈上的地址不能被移动,堆上的 "hello" 数据不受影响

  • Pin 作用在 栈上的对象地址,确保内部指针不会悬空

4、Pin、Unpin和!Unpin三者对比

类型 Pin 是否生效 说明
Unpin ❌ 不生效(透明) 移动仍然允许,安全无问题
!Unpin ✅ 生效 栈上值被固定,移动会编译报错,保护内部引用安全
Copy 类型 ❌ 不生效 move 其实是复制,Pin 对它无意义

Pin 是动作,"把对象钉住",Unpin / !Unpin 决定钉住后能否移动。

5、Pin与move的关系

类型 Copy? Unpin? Pin 后移动? Pin 作用
i32 / bool / f64 ✅ 可以移动 不起作用(透明)
String / Vec / Box ✅ 可以移动 对 Unpin 类型不起作用
自引用 / Future ❌ 禁止移动 Pin 生效,保护内部引用

Pin 的实际意义只针对 !Unpin (Pin<T: !Unpin>)类型,Copy 或 Unpin 类型( String、Vec、Box) 标记了 Pin(Pin<T: Unpin>),本质上不起作用。

二、核心API使用入门

上一章节确定了边界,这一章看下Pin的api怎么用。

示例1:Pin<Box>

rust 复制代码
use std::pin::Pin;

struct Data {
    value: String,
}

fn main() {
  
  	let boxed = Box::new(123);
    let pinned = Pin::new(boxed);

    // i32 是 Unpin,所以可以安全取出
    let inner = Pin::into_inner(pinned); 
    println!("{}", inner);
   
    let mut data = Data {
        value: "hello".to_string(),
    };

    // 普通 Box,可以自由移动
    let mut boxed = Box::new(data);

    // Pin<Box<T>>:禁止移动内部 T
    let mut pinned: Pin<Box<Data>> = Pin::new(boxed);

    // 安全访问字段
    println!("Value = {}", pinned.value);

    // 尝试移动 pinned(编译失败),内部值(Data)被移动出原来的内存地址,原先地址上的那块堆内存变成空的,违反了pin的约束
    // let moved = *pinned; // ❌
}

说明:

  • Pin<Box<T>> 保证 T 在堆上的地址不会改变;仍然可以修改内容,但不能"移走"整个结构体。
  • Pin::new():对 !Unpin 类型(例如自引用结构体),不能直接用这个函数。编译器会强制你使用 unsafe { Pin::new_unchecked(...) }
  • 如果想只有当类型是 Unpin(可安全移动)时才能用into_inner。

示例2:自引用结构体(Pin 的典型场景)

rust 复制代码
use std::pin::Pin;
use std::ptr::NonNull;

struct SelfRef {
    data: String,
    ptr: Option<NonNull<String>>,
}

impl SelfRef {
    fn new(txt: &str) -> SelfRef {
        SelfRef {
            data: txt.to_string(),
            ptr: None,
        }
    }

    fn init(self: Pin<&mut SelfRef>) {
        // 安全地拿到内部可变引用
        let this = unsafe { self.get_unchecked_mut() };
        // 设置指针指向自己字段
        this.ptr = Some(NonNull::from(&this.data));
    }

    fn print(&self) {
        unsafe {
            println!("self.data = {}", self.data);
            if let Some(ptr) = self.ptr {
                println!("ptr -> {}", ptr.as_ref());
            }
        }
    }
}

fn main() {
    let mut s = Box::pin(SelfRef::new("Rust Pin!"));
    s.as_mut().init();  // 初始化自引用
    s.print();          // ✅ OK:data 没被移动
}

示例 3:在异步任务中(Future 的典型应用)

async fn 生成的状态机其实是 自引用结构体Pin 在运行时保护它不被移动。

rust 复制代码
use std::pin::Pin;
use std::future::Future;
use std::task::{Context, Poll};

struct MyFuture {
    counter: u8,
}

impl Future for MyFuture {
    type Output = u8;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.counter < 3 {
            self.counter += 1;
            println!("Counting: {}", self.counter);
            Poll::Pending
        } else {
            Poll::Ready(self.counter)
        }
    }
}

#[tokio::main]
async fn main() {
    let result = MyFuture { counter: 0 }.await;
    println!("Done: {}", result);
}

Pin 确保 MyFuture 内部状态(如引用)不会在 .await 期间被移动。这就是为什么 async 语法能安全地处理复杂状态。

示例4:unsafe

a) Pin::new_unchecked

rust 复制代码
let mut x = SelfRef { ... }; // !Unpin 类型
let px = unsafe { Pin::new_unchecked(&mut x) };
  • 为什么 unsafe
    • 编译器不能保证后续不会移动 x
    • 需要开发者保证 x 在生命周期内不会移动
  • Pin::new_unchecked:必须 100% 确保 这个值不会在 pinned 后被移动,一般只在底层框架(如 tokio、futures)或自引用实现中用,业务代码一般不用。

b) Pin::get_unchecked_mut

rust 复制代码
let mut px: Pin<&mut SelfRef> = Pin::new(&mut x);

unsafe {
    let mut_ref: &mut SelfRef = Pin::get_unchecked_mut(px.as_mut());
    mut_ref.ptr = &mut mut_ref.data; // 自引用赋值
}
  • get_unchecked_mut():不安全适用任何类型,跳过移动安全检查,手动保证 pinned 值不会移动

c) into_inner 堆上对象拆回原类型(!Unpin 类型不可行)

rust 复制代码
let px: Pin<Box<SelfRef>> = Box::pin(SelfRef { ... });
// let b: Box<SelfRef> = Pin::into_inner(px); // ❌ 不安全,编译禁止
  • 对 Unpin 类型安全,可以拆回 Box
  • 对 !Unpin 类型,拆回 Box 需要 unsafe 并自己保证移动不会破坏安全(一般不推荐)

三、为什么要自引用

pin指针常用场景是自引用,这里聊聊自引用是啥,为什么有自引用。

1、对比方法访问和内部引用

假设我们有一个字符串字段 data,想访问前两个字符:

a、方法访问(推荐做法)

rust 复制代码
struct MyStruct {
    data: String,
}

impl MyStruct {
    fn slice(&self) -> &str {
        &self.data[0..2] // 每次调用都生成切片
    }
}

fn main() {
    let s = MyStruct { data: "Hello".into() };
    println!("{}", s.slice()); // "He"
}
  • 优点: Rust borrow checker 安全,没有悬空指针,简单、可维护

  • 缺点: 每次调用都会生成一个切片(非常轻量级,但在高性能/大量数据场景下可能产生微小开销)

b、内部引用(self_ref / slice 指向 data)

rust 复制代码
struct SelfRef {
    data: String,
    slice: *const str, // 内部指针
}

impl SelfRef {
    fn new(txt: &str) -> Self {
        let mut s = SelfRef {
            data: txt.to_string(),
            slice: std::ptr::null(),
        };
        s.slice = &s.data[0..2] as *const str; // 指向 data
        s
    }

    fn get_slice(&self) -> &str {
        unsafe { &*self.slice }
    }
}
  • 优点:

    • 零拷贝:切片预先计算好,访问不需要每次切分
    • 可用于异步/自引用结构,避免在 Future 状态机 poll 时重复生成切片
  • 缺点:

    • 必须使用 Pin 或堆分配保证 data 地址不变
    • 使用裸指针,需要 unsafe,风险大
    • 程序复杂度高,可维护性差

2、自引用场景

a、零拷贝解析

HTTP、JSON、CSV、文本流等大量数据处理,想存储对 buffer 的切片,而不是复制字符串。那内部引用可以直接保存 slice,避免每次 data[0..n] 生成新切片

b、异步状态机 / Future 自引用

状态机字段之间可能互相引用Poll 时不希望重新计算 slice 或临时变量

c、高性能图结构 / AI / Tensor

节点存指针指向数据的一部分,而不是每次生成新对象

3、不加pin的有什么问题

假设2的场景没加pin,Rust 编译器在面对"直接自引用",通常会:

  • 如果是安全引用(&str),直接拒绝编译

    rust 复制代码
    struct BadRef<'a> {
        data: String,
        slice: &'a str, // 引用自身字段
    }
    
    impl<'a> BadRef<'a> {
        fn new(txt: &str) -> Self {
            let s = txt.to_string();
            Self {
                data: s,
                slice: &s, // ❌ 编译错误
            }
        }
    }
    swift 复制代码
    error[E0505]: cannot move out of `s` because it is borrowed
     --> src/main.rs:9:13
      |
    8 |         let s = txt.to_string();
      |             - binding `s` declared here
    9 |         Self { data: s, slice: &s }
      |                    ^ move out of `s` occurs here
      |                    |
      |                    borrow later used here

    Rust 检测到 &s 引用了局部变量 s,但又 move 了它(所有权转移),这违反了生命周期规则。

  • 如果用裸指针(*const T),编译能过但属于未定义行为(UB)

    rust 复制代码
    use std::ptr;
    
    struct SelfRef {
        data: String,
        ptr: *const String,
    }
    
    impl SelfRef {
        fn new(s: &str) -> Self {
            let data = String::from(s);
            let ptr = &data as *const String;
            Self { data, ptr }
        }
    
        fn print(&self) {
            unsafe {
                // 访问 ptr 指向的旧地址
                println!("ptr -> {}", &*self.ptr);
            }
        }
    }
    
    fn main() {
        let x = SelfRef::new("hello");
        let mut y = x; // move 发生
        // 此时 y.ptr 仍指向 x.data 的旧地址(已被 drop)
    
        y.print(); // ❌ UB: 访问已释放内存
    }

    interrupted by signal 11:SIGSEGV,或者出现乱码,因为 b.slice 指向了 a.data 的旧位置。有的rust版本可能不崩,有点随机,所以是未定义行为。

四、Pin具体是怎么固定的

Pin 本身不保证对象真的"不会被移动",它只是 在类型系统层面限制移动 ,依赖于"不能获取 &mut T 原始引用:

  • 对于 Pin<Box<T>>,堆上分配 + 无法替换指针 = 地址稳定
  • 对于 Pin<&mut T>,编译器禁止 Tmem::replace()move

1、PhantomPinned + !Unpin

Rust 编译器通过 类型系统约束 固定内部指针。PhantomPinned 用来标记一个类型 不可移动 。默认情况下,所有类型都实现 Unpin

  • Unpin 意味着可以安全移动。

  • 自引用类型必须显式禁用 Unpin(通过 PhantomPinned)。

    rust 复制代码
    use std::marker::PhantomPinned;
    use std::pin::Pin;
    
    struct SelfRef {
        data: String,
        ptr: *const String,
        _pin: PhantomPinned, // 禁止移动
    }

2、Pin 的 API 层约束

Pin的核心api定义:

rust 复制代码
impl<T: ?Sized> Pin<&mut T> {
    pub fn as_mut(self: Pin<&mut T>) -> Pin<&mut T> { ... }
    pub unsafe fn get_unchecked_mut(self: Pin<&mut T>) -> &mut T { ... }
}
  • as_mut 安全获取可变引用,但仍然被 Pin 约束。

  • get_unchecked_mutunsafe的,允许手动移动内部字段,但风险自担。

  • 编译器只允许安全 API 移动外部包裹指针,但内部 T 地址固定。

3、阻止move的例子

堆上移动禁止的例子,Pin在编译期就会阻止

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

struct SelfRef {
    data: String,
    ptr: *const String,
    _pin: PhantomPinned, // !Unpin,禁止移动
}

fn main() {
    // 堆上创建 SelfRef 并 Pin
    let mut boxed: Pin<Box<SelfRef>> = Box::pin(SelfRef {
        data: String::from("hello"),
        ptr: std::ptr::null(),
        _pin: PhantomPinned,
    });

    // 初始化内部自引用指针
    let ptr = &boxed.data as *const String;
    unsafe {
        let mut_ref = Pin::as_mut(&mut boxed);
        Pin::get_unchecked_mut(mut_ref).ptr = ptr;
    }

    // ❌ 尝试 move 内部 SelfRef 会报错
    let moved = *boxed; // error: cannot move out of `*boxed` because it is pinned

    // // 但是 Box 本身可以移动
    // let moved_boxed = boxed; // ✅ Box 可移动,但堆上地址固定
    //
    // println!("data via pinned: {}", unsafe { &*moved_boxed.ptr });
}

原理:

  • Box 是智能指针,移动 Box 只移动指针,不移动堆数据,如下图,堆没变;
  • Pin API 保证 T 在堆上的内存地址不会改变。

五、为什么 Future 要有 Pin 设计

1、future自引用

一个 async fn 编译后其实会生成一个状态机结构体

rust 复制代码
async fn foo() {
    let s = String::from("hello");
    bar(&s).await;
    println!("{s}");
}

编译器大致会生成:

rust 复制代码
enum FooFuture {
    State0,                        // 初始状态
    State1 { s: String, bar_fut: BarFuture<'_> },  // await 之后
    Done,
}

每次 .poll() 时,Future 会被推进到下一个状态。

State1 中,bar_fut 持有一个对 s 的引用:BarFuture<'_> // 生命周期依赖 FooFuture::s

这意味着整个 FooFuture 结构体内部出现了**自引用(self-referential)**关系:

rust 复制代码
FooFuture
 ├─ s: String
 └─ bar_fut: BarFuture<'s>
              ↑
              └── 引用了 s

2、Future何时被移动

当 Future 被运行(比如通过 tokio::spawnblock_on)时,执行器会做这样的事:

rust 复制代码
let fut = foo();          // 创建 Future
executor.spawn(fut);      // 把 Future 移进任务队列(Move #1)

在任务系统中,执行器通常会:

  • fut 移进某个堆上分配的任务结构体;
  • 再从任务结构体中取出 Pin<&mut fut> 去调用 poll()

这样,在被 poll 之前,它已经被 move 过一次了。

更隐蔽的情况:

rust 复制代码
let fut = foo();
let f1 = async { fut.await };

fut 被嵌入另一个 Future f1 中。 f1 也会被 executor 再次移动。 也就是说 fut 可能经历:

rust 复制代码
foo() -> 创建 Future
   ↓ move
async { fut.await } -> 另一个 Future 包装
   ↓ move
executor.spawn() -> 放入任务
   ↓ move
poll() -> 固定到堆上

如果 FooFuture 在 poll 过程中被 移动 (move) ,那么 s 在内存中的地址就变了。 但 bar_fut 仍然保存着旧地址的引用,则引用失效,属于UB(未定义行为)!

所以Rust通过Pin来固定Future内存位置

3、流程如下

说明:

  • 当写一个 async fn foo() { ... }async { ... },Rust 编译器把它转成一个状态机 struct,其字段包括局部变量、状态标记、可能的 Waker 等。
  • 如果这个状态机 在 await 点之后 还持有对自身结构体内部字段(例如 &mut self.field、或 &self.field)的引用,那么它就是一个 自引用 Future。也就是说,它存储了指向自身内容的引用。
  • 若这种 Future 被移动(即地址变化),那么这些内部引用就变成悬垂引用,可能引起 UB。
  • 因此,为了安全,Rust 要求:若一个 Future 有可能是自引用的(即生成器状态机可能这样),那么它必须 先固定住地址(pinned) ,再被 poll。这就是为什么 poll(self: Pin<&mut Self>, ...) 而不是 &mut Selfstackoverflow.com/questions/7...
  • Pin<&mut Self> 的约束下,类型系统禁止你再偷偷将 Self 移动。只有当 Self 类型实现了 Unpin (表示"即便被 Pin 包装,也可安全移动")时,才允许"解除固定"的操作。
  • 总结来说,在 poll 期间,保证 Future 所表示的内存位置不会改变**,从而底层状态机字段中的"self-引用"依然有效。

六、总结

Pin主要保证 !Unpin 类型在内存中的地址固定,比如自引用类型、异步 Future 类型(async/await 内部状态机)、底层异步 I/O 驱动(如 Tokio 内部 task、io_uring buffer)。理解起来有点复杂,对普通类型或短生命周期变量没必要使用,除非是明确的需求。如果用的话,能用安全 API 就不用 unsafe;只能 unsafe 的地方,要保证对象绝对不动。

如果觉得有用,请点个关注吧。

相关推荐
2301_796512524 小时前
Rust编程学习 - 如何利用代数类型系统做错误处理的另外一大好处是可组合性(composability)
java·学习·rust
星释4 小时前
Rust 练习册 :Proverb与字符串处理
开发语言·后端·rust
Source.Liu4 小时前
【ISO8601库】Serde 集成模块详解(serde.rs文件)
rust·time·iso8601
星释6 小时前
Rust 练习册 :Pig Latin与语言游戏
游戏·rust·c#
2301_795167206 小时前
玩转Rust高级应用 如何让让运算符支持自定义类型,通过运算符重载的方式是针对自定义类型吗?
开发语言·后端·算法·安全·rust
ftpeak9 小时前
《Rust+Slint:跨平台GUI应用》第八章 窗体
开发语言·ui·rust·slint
百锦再12 小时前
第10章 错误处理
java·git·ai·rust·go·错误·pathon
2301_7951672013 小时前
玩转Rust高级应用. ToOwned trait 提供的是一种更“泛化”的Clone 的功能,Clone一般是从&T类型变量创造一个新的T类型变量
开发语言·后端·rust
星释14 小时前
Rust 练习册 :Phone Number与电话号码处理
开发语言·机器学习·rust