
之前讀 async 相關源碼的時候經常看到PinBoxdyn Future這種類型。一開始我以為它只是一種普通的智能指針后來才意識到Pin的核心并不是“管理內存”而是“禁止移動”。為了徹底弄懂它我嘗試從零手寫一個簡化版Pin并對照標準庫做了一次梳理。這個過程對理解 Rust 生命周期、Unpin、PhantomPinned以及 async/await 背后的機制幫助很大。這篇文章就是這次嘗試的完整記錄適合已經能編寫簡單 Rust 代碼、想深入理解Pin的讀者。全文會從自引用結構體的問題出發手寫一個 “More or Less” 版本的Pin再對照真實 Rust API 說明差異和常見誤區。1. 從問題說起自引用結構體為什么危險1.1 什么是自引用結構體Rust 中一個結構體的字段可能保存著指向同一結構體其它字段的指針這種結構體通常被稱為自引用結構體Self-Referential Struct。下面是一個非常典型的最小例子struct SelfReferential { value: String, ptr: *const String, }初始化時我們讓ptr指向value字段let mut sr SelfReferential { value: String::from(hello), ptr: std::ptr::null(), }; sr.ptr sr.value as *const String;這時sr.ptr保存的地址就是sr.value在當前棧幀中的地址。如果后續操作不移動sr那么這個指針是有效的一旦sr在內存中被搬到了其它位置sr.value的地址會發生變化而sr.ptr仍然保留舊地址成為懸垂指針。這種結構在實際開發中并不少見。例如手寫一個內部包含指向自身緩沖區的解析器、實現零拷貝解碼器、或者設計某些緩存結構時都可能產生自引用。Rust 的 async 狀態機內部也會生成類似結構這也是Pin被引入語言的重要原因之一。1.2 移動帶來的懸垂指針Rust 中的賦值、函數傳參、放入容器等操作都可能發生移動move。對普通結構體來說移動時整個值會被逐字節拷貝到新位置所有字段同步搬遷所以移動本身是安全的。問題在于如果我們手動保存了移動前的內部地址移動后這個地址不會自動更新。下面這段代碼展示了一個常見現象fn evaluate_move() { let mut sr SelfReferential { value: String::from(hello), ptr: std::ptr::null(), _pin: PhantomPinned, }; sr.ptr sr.value as *const String; println!(before move: value {:p}, ptr {:?}, sr.value, sr.ptr); let moved sr; println!(after move: value {:p}, ptr {:?}, moved.value, moved.ptr); }運行后before move和after move的輸出中value的地址往往會不同而ptr仍然是移動前的舊地址。這意味著moved.ptr已經指向了舊棧位置繼續對它解引用就是典型的內存安全問題。這里需要澄清一點編譯器并不保證每次移動后地址一定不同也可能出現地址恰好復用的情況。但從語言語義上講移動后value的地址是不可依賴的存在懸垂風險就足以判定這種設計不安全。1.3 為什么編譯器不能自動處理有讀者可能會問既然移動會導致自引用失效Rust 編譯器難道不能自動修復ptr嗎答案是否定的。Rust 的移動語義只是把值從一塊內存搬到另一塊內存編譯器不知道結構體內部哪些裸指針字段需要同步更新。更重要的是一旦引入了“移動時自動修復內部指針”的機制結構體的復制和移動規則就會變得異常復雜會直接破壞 Rust 對所有權的確定性管理。因此Rust 選擇在類型系統層面解決這個問題如果某個類型需要自引用它應該被標記為!Unpin然后用Pin來保證在固定期間不發生移動。Pin的本質承諾是一旦某個值被“釘住”安全代碼就拿不到它的mut T從而無法將它移動到其它位置也無法用其它值替換它。這正是自引用結構體需要的保護。2. 理解 Unpin、!Unpin 和 PhantomPinned2.1 Unpin 是什么Unpin是 Rust 標準庫中的一個自動 traitauto trait。所謂自動 trait意味著絕大多數類型無需手動實現就會自動獲得Unpin。T: Unpin語義上表示T類型的值可以在內存中安全地移動。普通結構體、枚舉、基本類型幾乎都是Unpin的。你可以把Unpin理解為“移動沒有額外代價和額外約束”的類型標記。舉個例子struct NormalStruct { a: u32, b: String, }NormalStruct默認就是Unpin的因為移動它不會破壞任何內部自定義指針。即使String內部包含堆指針也不受影響因為String的堆指針會隨整個結構體一起移動而String內部并不保存指向自身棧地址的引用。2.2 PhantomPinned 如何產生 !Unpin如果希望一個類型是!Unpin也就是“移動后可能不安全”穩定版 Rust 并不能直接寫impl !Unpin for MyType。這是未穩定功能我們只能借助標準庫里的PhantomPinned。PhantomPinned是一個零大小類型zero-sized type它本身實現了!Unpin。由于 Rust 的自動 trait 會沿著結構體字段傳播只要一個結構體里包含PhantomPinned字段整個結構體就會自動變成!Unpin。看下面的例子use std::marker::PhantomPinned; struct SelfReferential { value: String, ptr: *const String, _pin: PhantomPinned, // 使整個結構體變成 !Unpin }這個_pin字段不會占用任何內存它的唯一作用是參與類型系統的約束。引入它之后SelfReferential就不再實現Unpin從而進入Pin的保護范圍。2.3 “沒被 Pin 之前”仍然可以移動一個剛剛創建、還沒有被Pin包裹的!Unpin類型仍然可以隨意移動。!Unpin只表示“這類值被固定后不能移動”并不表示“永遠不能移動”。這一點非常重要。很多人看到!Unpin就認為這種類型完全不能移動這是一個常見的誤解。實際上PhantomPinned只是讓類型具備“需要被固定”的資格真正執行“禁止移動”約束的是Pin及借用檢查器。只有把一個!Unpin類型交給Pin管理之后安全代碼才無法再獲得它的mut T從而無法移動、替換、swap 它。3. 從零實現一個簡化版 Pin3.1 設計目標標準庫的PinP內部實現比較復雜它需要支持P是Box、mut T、Rc等各種指針類型。我們這里不打算復制整個標準庫而是實現一個“More or Less”版本只保留最核心的安全語義。我們的目標非常明確只能通過“安全構造”創建一個指向T: Unpin值的Pin。對于T: !Unpin的值只能通過unsafe方法創建Pin并且調用者需要保證固定期間不得移動該值。任何情況下都可以從Pin獲取不可變引用T。只有在T: Unpin時才能安全獲取mut T。當T: !Unpin時只能通過unsafe方法獲取mut T。這個設計正好對應標準庫最核心的安全邊界。為了簡化我們讓Pin持有a mut T。3.2 結構體定義在 Rust 中一個最簡單的最小實現可以寫成這樣use std::ops::{Deref, DerefMut}; pub struct Pina, T: ?Sized { inner: a mut T, }inner是被釘住值的可變借用。我們持有a mut T本身就能利用 Rust 的借用規則來阻止外部在Pin存活期間再次可變訪問原變量。注意T: ?Sized是必要的因為T可以是動態大小類型例如dyn Future或切片類型。a mut T本身可以指向 unsized 類型。3.3 安全構造new當T: Unpin時這個類型的值可以安全移動因此我們允許安全代碼直接構造Pinimpla, T: ?Sized Pina, T { pub fn new(inner: a mut T) - Self where T: Unpin, { Pin { inner } } }這里的關鍵是T: Unpin約束。編譯器看到這個約束后只有T確實實現了Unpin調用者才能使用Pin::new。對于!Unpin類型這條路被堵死。如果你嘗試對一個!Unpin的類型調用Pin::new會得到類似這樣的錯誤error[E0277]: the trait bound SelfReferential: Unpin is not satisfied3.4 不安全構造new_unchecked對于!Unpin類型我們只能提供一個unsafe構造函數impla, T: ?Sized Pina, T { pub unsafe fn new_unchecked(inner: a mut T) - Self { Pin { inner } } }為什么這是unsafe原因在于調用者必須承諾一個關鍵不變式在Pin存活期間被釘住的值不會發生移動。由于a mut T已經從原變量手中接管了可變借用借用檢查器已經能阻止很多危險操作但unsafe構造仍然要求調用者理解深層語義而不能只靠編譯器的借用規則兜底。舉個例子如果inner指向堆內存并且之后有人釋放了這塊內存即使沒有移動Pin持有的指針也可能懸垂。這類問題超出了普通借用檢查的范圍需要調用者謹慎處理。在真實項目中每次使用new_unchecked時都應該在注釋里寫明這個不變式由誰維護、在什么范圍有效。3.5 獲取引用的方法我們還需要提供三個核心方法impla, T: ?Sized Pina, T { pub fn get_ref(self) - T { self.inner } pub fn get_mut(self) - a mut T where T: Unpin, { self.inner } pub unsafe fn get_unchecked_mut(self) - a mut T { self.inner } }get_ref是安全的因為無論T是否Unpin共享引用都不會導致值被移動。get_mut只在T: Unpin時可用因為只有這種類型才允許被可變借用后移動或替換。get_unchecked_mut是unsafe的調用者必須保證拿回mut T后不會移動這個值、不會用std::mem::replace等操作替換它也不會在Pin仍存活時再次固定它。這些方法雖然簡單卻完整表達了Pin對可變訪問的約束邊界。3.6 實現 Deref 與 DerefMut為了讓Pin能像普通引用一樣方便地訪問內部字段我們需要實現Deref。Deref在任何情況下都應該實現因為共享引用不會破壞固定語義implT: ?Sized Deref for Pin_, T { type Target T; fn deref(self) - Self::Target { self.inner } }DerefMut則必須謹慎。它只應該在T: Unpin時實現implT: Unpin ?Sized DerefMut for Pin_, T { fn deref_mut(mut self) - mut Self::Target { self.inner } }如果對T: !Unpin也實現了DerefMut那么安全代碼就可以直接寫mut *pin從而把內部值拿出去移動或替換整個Pin的保護就形同虛設。因此DerefMut的Unpin約束是整個設計的關鍵防線。到這里一個簡化版Pin的核心已經完成。4. 用簡化版 Pin 實現自引用結構體4.1 完整示例代碼下面的示例把上面的Pin實現與自引用結構體放在同一個文件里。你可以直接復制到一個 Rust 項目中運行。use std::marker::PhantomPinned; use std::ops::{Deref, DerefMut}; // ---------- 簡化版 Pin ---------- pub struct Pina, T: ?Sized { inner: a mut T, } impla, T: ?Sized Pina, T { pub fn new(inner: a mut T) - Self where T: Unpin, { Pin { inner } } pub unsafe fn new_unchecked(inner: a mut T) - Self { Pin { inner } } pub fn get_ref(self) - T { self.inner } pub fn get_mut(self) - a mut T where T: Unpin, { self.inner } pub unsafe fn get_unchecked_mut(self) - a mut T { self.inner } } implT: ?Sized Deref for Pin_, T { type Target T; fn deref(self) - Self::Target { self.inner } } implT: Unpin ?Sized DerefMut for Pin_, T { fn deref_mut(mut self) - mut Self::Target { self.inner } } // ---------- 自引用結構體 ---------- struct SelfReferential { value: String, ptr: *const String, _pin: PhantomPinned, } fn demonstrate_move() { let mut sr SelfReferential { value: String::from(hello), ptr: std::ptr::null(), _pin: PhantomPinned, }; sr.ptr sr.value as *const String; println!(before move: value {:p}, ptr {:?}, sr.value, sr.ptr); let moved sr; println!(after move: value {:p}, ptr {:?}, moved.value, moved.ptr); } fn demonstrate_pin() { let mut sr SelfReferential { value: String::from(hello), ptr: std::ptr::null(), _pin: PhantomPinned, }; sr.ptr sr.value as *const String; // 這里是 unsafe我們承諾在 pinned 存活期間不會移動 sr let pinned unsafe { Pin::new_unchecked(mut sr) }; println!(after pin: value {:p}, ptr {:?}, pinned.value, pinned.ptr); assert_eq!(pinned.ptr, pinned.value as *const String); // 下面兩行一旦放開都無法編譯 // let r mut *pinned; // sr.value String::from(world); } fn main() { demonstrate_move(); demonstrate_pin(); }編譯運行后你會看到類似下面的輸出地址值每次運行可能不同before move: value 0x7ffd3f5e1aa8, ptr 0x7ffd3f5e1aa8 after move: value 0x7ffd3f5e1a90, ptr 0x7ffd3f5e1aa8 after pin: value 0x7ffd3f5e1a78, ptr 0x7ffd3f5e1a78可以看到before move階段ptr和value的地址一致指針有效。移動發生后value被搬到了新地址但ptr仍指向舊地址自引用已經失效。使用Pin固定后我們依然可以通過pinned.value訪問值且ptr與當前value地址保持一致自引用是有效的。4.2 安全性驗證在demonstrate_pin中我注釋了兩行代碼。如果你試著把這兩行注釋放開會遇到不同的編譯錯誤。第一行let r mut *pinned;這里會失敗因為我們的簡化版Pin只為T: Unpin實現了DerefMut而SelfReferential通過PhantomPinned已經變成了!Unpin。編譯器會告訴你無法通過Pin獲取可變引用這正是Pin的保護邊界。第二行sr.value String::from(world);這里會失敗因為sr的可變借用已經被pinned持有。借用檢查器知道Pin還存活所以不允許再通過原變量修改sr這從另一個角度防止了值被移動或替換。4.3 這個小版本缺了什么我們的簡化版Pin雖然體現了核心思路但還缺少很多工程能力它只能持有a mut T不能持有BoxT、RcT等智能指針。它沒有實現Drop相關的特殊處理無法描述析構期間的額外約束。它不支持 pin 投影pin projection即無法安全地把Pinmut Struct拆成Pinmut Field的形式。這些功能并不影響我們理解Pin的本質但到了真實項目里就會變得很重要。5. 真實 Rust 中 Pin 與簡化版的差異5.1 標準庫 Pin 的結構真實的標準庫Pin定義大概是這樣的思路pub struct PinP { pointer: P, }它包裝的是指針類型P而不是固定的a mut T。所以PinBoxT、Pinmut T、PinRcT本質上都是同一種PinP在不同指針參數下的實例。這讓標準庫Pin可以同時支持堆固定和棧固定也可以與Box、Rc等所有權模型無縫配合。我們實現的簡化版本只是截取了最核心的“不可移動約束”這一層語義。標準庫還規定PinP自身不保證P: Unpin但Pinmut T通常會實現Unpin。這些細節在簡化版中沒有展開但理解它們對深入使用 Rust 異步生態很重要。5.2 堆固定與棧固定在異步編程中最常見的兩種Pin形態是// 堆固定把值放入堆內存返回 PinBoxT let pinned_box: PinBoxSelfReferential Box::pin(sr); // 棧固定pin! 宏在棧上固定值返回 Pinmut T let pinned_ref: Pinmut SelfReferential pin!(sr);堆固定的好處是PinBoxT自身可以自由移動因為它內部保存的是指向堆內存的指針堆內存地址不會因為Box的移動而改變。這意味著PinBoxT可以跨函數返回、存入結構體或跨線程傳遞。棧固定通常更高效但需要保證Pinmut T自身不會被移動。很多異步運行時內部會使用棧固定來避免不必要的堆分配。5.3 為什么 async 中到處都是 Pin如果你寫過 Rust 的異步代碼一定見過類似這樣的類型PinBoxdyn FutureOutput ()Future 本質上是一個狀態機。當代碼中存在跨await的局部變量引用時狀態機內部可能生成自引用結構。例如async fn example() { let s String::from(hello); let r s; some_future().await; println!({}, r); }在編譯器生成的 Future 狀態機中s和r有可能以自引用形式存在。由于這種狀態機可能被移動、被傳入 executor 或跨線程調度Rust 需要Pin來保證狀態機在被 poll 期間不會被移動。這也是為什么Future::poll的簽名要求self: Pinmut Self而不是普通的mut Self。理解了自引用結構體和Pin的關系之后再看異步源碼就不會覺得Pin是憑空出現的魔法了。6. 常見理解誤區與排查思路6.1 誤區Pin 就是堆分配很多人第一次看到PinBoxT會誤以為Pin等同于堆分配。實際上Pin是一種類型層面的約束堆分配只是Box::pin的一種實現方式。pin!宏可以棧上固定不需要堆分配。反過來Box::new也只是堆分配如果不配合Pin并不能給自引用結構體提供移動保護。6.2 誤區!Unpin 類型永遠不能移動正如前文強調的那樣!Unpin只表示“一旦被Pin固定之后不能移動”。類型自己并不阻止移動。一個包含PhantomPinned的普通變量在尚未被Pin包裹時依然可以賦值給其他變量或作為參數傳入函數。換句話說!Unpin不是全局禁止移動而是把“是否允許移動”的決策權交給Pin和 unsafe 代碼。6.3 常見編譯錯誤下面整理了一些實際開發中常見的錯誤信息、原因和處理思路。不同 Rust 版本下錯誤文案可能略有差異但排查邏輯是通用的。錯誤現象常見原因解決思路error[E0277]: the trait bound T: Unpin is not satisfied對!Unpin類型使用了Pin::new或其它要求Unpin的安全 API改用unsafe Pin::new_unchecked并確保固定期間不移動或者讓類型重新實現Unpinerror[E0596]: cannot borrow as mutable可變借用已交給Pin又嘗試通過原變量再次修改通過Pin的只讀接口訪問如果確認安全可以使用unsafe get_unchecked_muterror[E0594]: cannot assign ...嘗試通過解引用Pin修改一個!Unpin內部值檢查設計是否需要可變訪問切勿盲目加unsafeerror[E0505]: cannot move out ... because it is borrowed在Pin存活期間嘗試移動原始變量調整生命周期讓Pin先析構再處理原變量future is not Send異步狀態機中持有跨await的自引用或非Send類型檢查狀態機包含的字段必要時加鎖或調整結構避免持有自引用6.4 排查清單如果你在一個!Unpin相關問題上卡了很久我建議按下面的順序排查