
Rust 里的Pin一直是新手和老手之间的一道分水岭。很多人会用Box::pin包一个Future但问他Pin到底保证了一件什么事往往答不上来也有人见过Pinmut T出现在Future::poll签名里却很难解释它为什么必须长这样。这篇文章换个角度切入不逐行背标准库文档而是从零手写一个简化版Pin把构造、读取、可变访问、安全边界全部拆开用代码直接回答“Pin 到底禁了什么”。文章的核心只有一条线索Pin 是怎么做到“禁止值被移动”的。围绕它我会先用一个自引用结构体演示移动的后果再分析Unpin与PhantomPinned在类型系统里扮演的角色接着手写一个 MiniPin 模拟标准库行为最后用 MiniPin 跑通一个完整示例并解释它与Future::poll、async 状态机之间的深层关系。如果你正在学 Rust 异步编程、准备自定义Future、或者在写类似自引用结构体的底层代码这篇可以直接收藏。读完你不仅能知道Pin怎么用还能知道它为什么这样设计、哪些位置是 unsafe 的重灾区以及遇到编译错误或运行时崩溃时怎么排查。1. Pin 核心概念速览先把结论摆出来所有细节后面展开。能力项说明项目主题从零理解并模拟 Rust 标准库中的Pin类型核心问题防止自引用结构体在内存移动后产生悬空指针关键技术Unpin、PhantomPinned、unsafe 指针包装、Deref设计是否依赖第三方库否只用 Rust 标准库运行环境任意 rustc 环境CPU 架构无关前端要求掌握所有权、借用、生命周期、简单的裸指针操作实战能力跑通自引用结构体的初始化、访问、验证流程与 async 的关系能解释Future::poll为什么使用Pinmut Self简单说Pin是一个“指针包装器”。它包装一个指针比如mut T、BoxT然后向外承诺被指向的值在生命周期内不会发生内存移动。这个承诺是 Rust 安全保证的一部分但因为编译器无法在类型层面自动判断一个类型是否允许移动所以标准库把“是否安全移动”的决策权交给了每个类型本身通过Unpin这个 auto trait 表达。2. Pin 要解决的问题自引用结构体的移动陷阱2.1 一个会失控的自引用结构体先看一个最经典的自引用结构体。struct SelfRef { data: String, ptr: *const String, } impl SelfRef { fn new(data: String) - Self { let mut s SelfRef { data, ptr: std::ptr::null(), }; // 记录 data 字段在内存中的地址 s.ptr s.data as *const String; s } fn get(self) - str { assert!(!self.ptr.is_null(), ptr 未初始化); unsafe { *self.ptr } } } fn main() { let a SelfRef::new(String::from(hello)); let b a; // Struct 被移动到 b println!({}, b.get()); // 危险 }这段代码在编译期没有任何警告但运行结果完全不可预期。问题出在这里a在栈上占据一块内存a.data是这个结构体内部的一个字段。a.ptr被赋值为a.data它保存的是a.data在栈上的地址。执行let b a;时Rust 会把a的整块字节复制到b的内存位置。结构体本身被移动了但b.ptr仍然指向旧的a.data地址。旧地址在移动之后已经不属于这个对象后续访问就是悬空指针。你可能会想String的数据不指向堆吗对String的堆缓冲区不会随结构体移动而改变地址但ptr指向的是data字段本身也就是String结构体头部在栈上的位置。这个位置是跟着结构体走的。移动后ptr悬挂。2.2 为什么编译器不直接禁止自引用一个自然的疑问是Rust 这么强调内存安全为什么不干脆在编译期禁止这种自引用结构体答案很现实通用自引用检测是不可判定的。一个结构体完全可以先构造完所有字段再通过某种方法把自己的地址存进另一个字段编译器无法在类型层面追踪这种复杂的字段间关系。Rust 的选择是提供一个“逃生通道”默认情况下编译器不检查你是否写了自引用。如果写了你需要用unsafe操作裸指针。如果你希望这个对象在后续使用中不被移动就把所有权交给Pin。所以Pin不是编译器魔法它是建立在所有权和生命周期规则之上的一种“契约工具”。3. 从零实现 Pin 的关键地基Unpin 与 PhantomPinned要从零实现一个 Pin首先得弄清楚Unpin和PhantomPinned。这两个概念比 Pin 本身更抽象却是理解 Pin 安全边界的钥匙。3.1 Unpin 是 auto traitUnpin是 Rust 标准库里的一个 auto trait。auto trait 的意思是编译器会自动为几乎所有类型实现它除非遇到明确阻止它的字段。pub trait Unpin {} // 编译器自动为绝大多数类型实现 Unpin如果一个类型T是Unpin的那它在任何场景下都可以安全移动。普通结构体、枚举、整数、String 都是Unpin。因为移动它们不会产生悬空问题——如果一个类型里没有自引用移动它当然安全。struct NormalStruct { a: i32, b: String, } // 自动实现了 UnpinPin对Unpin类型非常宽松Pinmut T可以直接安全地得到mut T。原因很简单既然移动安全那我允许你通过Pin拿可变引用也不会破坏任何东西。3.2 PhantomPinned让类型自愿放弃 Unpin但自引用结构体必须被标记为“不允许移动”。Rust 的标准做法是在结构体里加一个PhantomPinned字段。pub struct PhantomPinned; impl !Unpin for PhantomPinned {}这里的impl !Unpin是标准库内部的负 impl意思是PhantomPinned不实现Unpin。当一个结构体包含PhantomPinned字段时编译器自动推导出的 auto trait 结果也会变成!Unpin。use std::marker::PhantomPinned; struct SelfReferential { data: String, self_ptr: *const String, _pin: PhantomPinned, } // 编译器自动推导为 !Unpin一旦类型是!Unpin所有针对它的Pin操作都必须特别小心因为任何移动都可能触发悬空指针。3.3 为什么说从零实现不能做到 100%严格来说Unpin是语言层面的 auto traitPhantomPinned对Unpin的否定是标准库的“特权”行为。普通 Rust 代码不能写impl !Unpin for MyType {}因为负 impl 在 stable 上是不稳定的特性。所以“从零实现 Pin”在本文的语境下指的是实现 Pin 的包装逻辑和访问控制如何用 unsafe 构造一个固定引用、如何通过Deref安全读取、如何在!Unpin类型上限制可变访问。至于Unpin推导和PhantomPinned对负 impl 的触发依赖语言和标准库的底层支持。4. 手写简化版 MiniPin 核心代码下面开始实现一个MiniPinP。它和标准库PinP的核心思路一致内部包裹一个指针 P通过Deref提供安全的只读访问可变访问则必须用 unsafe 控制。4.1 MiniPin 结构定义与构造use std::ops::{Deref, DerefMut}; pub struct MiniPinP { pointer: P, } implP MiniPinP { /// 构造一个 MiniPin。 /// /// # Safety /// /// 调用者必须保证 P 指向的值在 MiniPin 存活期间不会被移动。 pub unsafe fn new_unchecked(pointer: P) - Self { MiniPin { pointer } } /// 获取内部指针的引用。 pub fn as_ref(self) - P { self.pointer } }new_unchecked是核心 unsafe 入口。调用者要承诺不会移动被指向的值。这个承诺后续全靠类型系统的Unpin约束来强制但MiniPin本身只负责声明“我不会自己移动目标”。4.2 通过 Deref 实现安全读取为了让MiniPinmut T用起来像普通的引用需要实现Deref。implP: Deref Deref for MiniPinP { type Target P::Target; fn deref(self) - Self::Target { *self.pointer } }有了DerefMiniPinmut SelfReferential可以直接调用SelfReferential的只读方法。因为deref返回的是不可变引用不会允许移动。4.3 可变访问只能是 unsafe为了防止!Unpin类型被移动不能安全地提供DerefMut。如果提供了DerefMut调用者就能通过mut T使用std::mem::replace、std::mem::take把值搬走固定保证会被击穿。implP: DerefMut MiniPinP { /// 获取可变引用。 /// /// # Safety /// /// 当且仅当目标类型 T 是 Unpin 时调用者才能安全使用这个方法。 /// 如果 T 是 !Unpin 类型获取 mut T 后用 mem::replace / swap 会破坏固定保证。 pub unsafe fn get_unchecked_mut(mut self) - mut P::Target { mut *self.pointer } }这里把可变访问限制为 unsafe相当于把“移动安全”的判断权交还给调用者。标准库里的Pin::get_unchecked_mut也有同样的签名和语义。5. 实战用 MiniPin 管理自引用结构体现在把 MiniPin 和自引用结构体组合起来跑一个完整示例。这里用标准库的Pin还是用 MiniPin既然目标是验证“从零实现”的思路我们用 MiniPin 做一个小而完整的案例。5.1 完整代码use std::marker::PhantomPinned; use std::ops::{Deref, DerefMut}; // ---------- MiniPin ---------- pub struct MiniPinP { pointer: P, } implP MiniPinP { pub unsafe fn new_unchecked(pointer: P) - Self { MiniPin { pointer } } pub fn as_ref(self) - P { self.pointer } } implP: Deref Deref for MiniPinP { type Target P::Target; fn deref(self) - Self::Target { *self.pointer } } implP: DerefMut MiniPinP { pub unsafe fn get_unchecked_mut(mut self) - mut P::Target { mut *self.pointer } } // ---------- 自引用结构体 ---------- struct SelfReferential { data: String, self_ptr: *const String, _pin: PhantomPinned, } impl SelfReferential { fn new(data: String) - Self { SelfReferential { data, self_ptr: std::ptr::null(), _pin: PhantomPinned, } } fn init(mut self) { self.self_ptr self.data as *const String; } fn get_data(self) - str { assert!(!self.self_ptr.is_null(), self_ptr 未初始化); unsafe { *self.self_ptr } } fn update_data(mut self, new_data: String) { self.data new_data; } } fn main() { let mut value SelfReferential::new(String::from(hello)); // 通过 MiniPin 固定 value let mut pinned unsafe { MiniPin::new_unchecked(mut value) }; // 初始化自引用指针 unsafe { pinned.get_unchecked_mut() }.init(); // 通过 Deref 读取 println!(data {}, pinned.get_data()); // 修改内容在固定位置进行 unsafe { pinned.get_unchecked_mut() }.update_data(String::from(world)); println!(data {}, pinned.get_data()); // 验证 self_ptr 仍然有效 let data_ref pinned.get_data(); println!(data_ref {}, data_ref); }5.2 验证步骤与预期输出运行这段代码预期输出data hello data world data_ref world验证逻辑MiniPin::new_unchecked构造固定引用这里我们自己承诺value不会被移动。init()通过get_unchecked_mut把self_ptr设置为data字段的地址。get_data()通过裸指针解引用访问data能正确打印说明指针没有悬挂。修改data后再次读取仍然正确说明引用关系在整个MiniPin生命周期内保持有效。5.3 如果一不小心移动了作为对照把value移动到另一个变量let mut value SelfReferential::new(String::from(hello)); let mut pinned unsafe { MiniPin::new_unchecked(mut value) }; unsafe { pinned.get_unchecked_mut() }.init(); // 主动移动 value编译器不会阻止 let moved value; // 此时再访问 pinnedself_ptr 指向旧地址已经悬空 // println!({}, pinned.get_data()); // 危险这段代码能编译但在debug模式下可能直接 panicrelease模式下可能输出错误数据。这个对照实验恰恰证明了 Pin 的价值如果没有 Pin 把值钉住自引用结构体就是一个随时会爆的内存隐患。6. Pin 与 Future/async 的深层关系MiniPin 实现完之后很多读者会问这和 async 有什么关系为什么Future::poll的签名要写成Pinmut Self6.1 poll 签名为什么是 Pinmut Self标准库里的Futuretrait 长这样pub trait Future { type Output; fn poll(self: Pinmut Self, cx: mut Context_) - PollSelf::Output; }poll的第一个参数不是普通的mut Self而是Pinmut Self。原因在于编译器为实现 async 块生成的状态机在跨 await 点保存借用时可能产生自引用结构体。看一个典型例子async fn demo() - String { let x String::from(hello); let y x; some_future().await; format!({} world, y) }整个 async 函数会编译成一个状态机结构体里面同时保存x和y。其中y指向x在这个状态机结构体中的位置。如果poll接受普通mut Self执行tokio::spawn或Box::pin以外的移动操作就可能让y悬空。Pinmut Self的约束让执行器在poll之间不能搬动这个状态机。所以几乎所有异步运行时里的任务调度都要依赖Pin来保证状态机地址稳定。6.2 async 状态机如何变成自引用结构体其实编译器不一定每次都会生成自引用状态机Rust 语言设计上必须支持这种场景。一旦y的生命周期跨过.await编译器就有可能在状态机中同时保存被借用者x和借用者y。因为y是引用它内部保存的是x在状态机里的偏移地址。状态机一旦整体移动偏移地址立即失效。这就是为什么async块编译后可以被Futuretrait 统一抽象但唯一能让它安全运作的访问方式就是Pinmut Self。6.3 编译器到底生成了什么你可以用cargo expand或生成 MIR 来观察状态机结构。实际生成的类型非常复杂包含每个局部变量和临时值。Rust 编译器会根据借用分析决定是否生成自引用布局如果所有临时值都能放在Unpin类型中状态机可以直接Unpin。如果存在跨 await 的借用编译器会让状态机包含类似PhantomPinned的标记。最终poll只能在Pin上调用。从 MiniPin 的视角看Future的状态机就是“一个被 MiniPin 固定的自引用结构体”。你在poll里拿到的self是Pinmut Self它能让你安全地访问字段但不能把它移动出当前内存位置。7. 安全边界哪里安全哪里必须 unsafe理解了 MiniPin 和 Future 的关系再从安全边界的角度总结一下 Pin 的关键约束。7.1 安全构造与 unsafe 构造的划分构造方式类型约束安全级别Pin::new(mut t)T: Unpin安全Pin::new(Box::new(t))T: Unpin安全Box::pin(t)任意T: Sized安全但固定在堆上Pin::new_unchecked(mut t)T: !Unpinunsafe调用者必须保证不移动MiniPin::new_unchecked(mut t)任意Tunsafe同上Box::pin之所以安全是因为Box的堆分配地址不会随着Box变量移动而改变。只要不把Box里的值移出来*boxed这种操作在稳定上不能对!Unpin使用堆地址就是稳定的。7.2 为什么禁止 mut T 可以防止移动这是 Pin 最核心的安全模型。Rust 里所有移动操作都要经过mut Tmem::replace、mem::swap、std::mem::take都需要一个mut T。如果PinP不为!Unpin类型提供安全的deref_mut调用者就无法拿到mut T自然也就无法把这些移动函数应用上去。MiniPin 里的get_unchecked_mut之所以必须标记 unsafe就是因为一旦开放了mut T安全保证就被绕过了。正确使用时你应该只在“这个类型是 Unpin”的前提下调用它或者在非常确信不会移动的情况下对固定位置做简单的字段更新。7.3 Drop 和析构函数的坑Drop和 Pin 的交互也是一个隐藏雷区。标准库对Drop的处理是当Pin包裹的!Unpin值被 drop 时drop方法拿到的其实还是mut T。如果你在Drop::drop中尝试把字段移动出去那同样会造成 UB。实际工程里自定义Drop的自引用结构体要格外小心。一般建议优先让自引用字段在构造阶段完成绑定。Drop::drop里只做资源清理不做字段移动。如果必须在析构时访问某些字段用裸指针读取不要用mut转移。8. 常见问题与排查方法问题现象可能原因排查方式解决方案编译错误cannot move out of dereference of Pinmut T尝试从mut T中移动值比如用mem::take查看报错位置是否在poll或自定义方法内改用Pin::set或 clone 后再移除引用自引用指针访问崩溃或读到垃圾值结构体被移动后旧指针未更新在init和读取处打印self_ptr与self.data地址将对象放入Box::pin确保不手动移动自定义 Future 在 poll 后行为异常状态机被移动或 poll 内部用了get_unchecked_mut绕过固定检查poll是否在多个地方持有可变引用使用Pin::as_mut正确投影不要获取整个结构的mut Self编译器报E0277: Unpin相关错误类型包含PhantomPinned尝试将Pinmut T转为mut T查看错误信息中的类型约束不要在!Unpin类型上直接调用需要mut T的方法初始化自引用指针为空导致 panicself_ptr未被调用init初始化在new和访问处加断言在Pin构造后立刻调用init(self: Pinmut Self)Box::pin后无法取回所有权BoxT被 Pin 包裹后不能安全into_inner移动!Unpin值查看是否需要回收对象如果类型实际是Unpin先确认如果真正需要移动需要 unsafe 并保证对齐MiniPin 的DerefMut没有实现编译失败从零实现时不能为DerefMut暴露安全mut T检查代码是否需要可变访问用 unsafe 方法封装可变操作或者重新设计结构体避免可变需求9. 最佳实践与使用建议读完上面的实现最需要记住的是Pin 的价值在于省去你手动追踪“这个对象有没有被移动”的心智负担。按下面这些实践经验来使用能少踩很多坑。9.1 优先用标准库的 Box::pinMiniPin 是教学工具实际工程里不要重复造轮子。标准库的Box::pin和Pin::new_unchecked是经过大量测试的实现。如果你只是需要把一个值固定起来直接写let pinned_value: PinBoxSelfReferential Box::pin(SelfReferential::new(String::from(hi)));既安全又简洁。9.2 自引用字段用裸指针存储自引用结构体里的指针字段一般存储为*const T或*mut T。不要用普通引用类型因为在结构体初始化阶段无法安全地先借出自己。字段类型选择struct SelfReferential { value: i32, self_ptr: *const i32, _pin: PhantomPinned, }使用裸指针只是为了绕过借用检查器的限制真正解引用时一定要保证Pin约束有效。9.3 初始化方法用 Pinmut Self 签名对!Unpin类型初始化方法最好写成impl SelfReferential { fn init(self: Pinmut Self) { let this unsafe { self.get_unchecked_mut() }; this.self_ptr this.value as *const i32; } }这样调用者从创建到初始化整个过程都被 Pin 约束着不容易误移动。9.4 处理字段投影时用 pin-project标准库 Pin 对字段的“投影”没有直接支持。如果你需要分别固定结构体的不同字段建议用pin-project或pin-project-lite这类 crate。它们通过宏生成安全的投影方法避免你在 unsafe 里手写一堆容易出错的分支。9.5 给工程加一条 CheckList每次使用 Pin 前可以按这个列表检查[ ] 目标类型是否真的!Unpin还是可以安全移动[ ] 有没有在mut T上调用mem::replace、mem::take、swap[ ] 自引用指针的初始化是否在移动前完成[ ] 自定义 Drop 里是否存在字段移动[ ] 是否只用了Pinmut T的安全方法Deref、as_ref避免滥用 unsafe10. 进一步学习路线从零实现 MiniPin 只是理解 Pin 的第一步。如果你要继续深入建议按这个顺序往下走读标准库std::pin模块的完整文档重点看Pin::set、Pin::get_unchecked_mut、Pin::as_mut的文档注释。读futures-rs里的pin_mut宏看看它如何把局部变量安全地包装成Pinmut T。用pin-projectcrate 改写你的自引用结构体观察派生宏生成的投影代码。自己实现一个极简TimerFuture在poll里管理内部状态体验Pin在异步运行时中的实际价值。用cargo expand查看编译器为 async 块生成的状态机结构确认它是否真的自引用。Rust 的Pin并不难一旦从“实现者”的角度理解了它你再看Future::poll、再看Box::pin就不会再是背签名了。文中的 MiniPin 和自引用结构体代码可以直接复制到本地跑建议顺手跑一下移动版本的对照实验亲眼看看悬空指针是怎么发生的体验会更深刻。