涵蓋 PinUnpinpin! macro、tokio::pin!,以及它們背後的設計邏輯。


一、問題的根源:自引用結構

要理解 Pin,得先知道它解決的是什麼問題。

在 Rust 中,「移動(move)」一個值,本質上是把它的記憶體內容複製到新位置,然後讓舊的失效。大多數情況下這完全沒問題,但有一種情況會爆炸——自引用結構(self-referential struct)

struct SelfRef {
    data: String,
    ptr: *const String,  // 指向自己的 data!
}

如果這個結構被移動了,data 跑到新地址,但 ptr 還指著舊地址,就成了懸空指標(dangling pointer),記憶體安全直接玩完。

這聽起來像是很少見的邊緣案例,但其實非常普遍——async/await 產生的 Future 幾乎都是自引用結構

async fn example() {
    let data = vec![1, 2, 3];
    some_await().await;
    println!("{:?}", data); // Future 需要跨 await 持有對 data 的引用
}

編譯器為這個 async fn 生成的狀態機,會同時持有 data 和指向它的引用,這就是自引用。


二、Pin 的核心思想

Pin<P> 是一個包裝器,它對指標 P(如 &mut TBox<T>)做出保證:

「被指向的值 T,在它被 drop 之前,不會再被移動。」

use std::pin::Pin;

// 普通的 Box,裡面的值可以被 move 出來
let b: Box<String> = Box::new("hello".into());
let s: String = *b; // ✅ 可以 move

// Pin 住的 Box,裡面的值無法被安全地 move 出來
let p: Pin<Box<String>> = Box::pin("hello".into());
// let s: String = *p; // ❌ 無法取得 &mut T 來 move

Pin 本身不是什麼魔法鎖,而是透過型別系統來防止你取得能夠 move 該值的 &mut T


三、Unpin:大多數型別其實不在乎

Rust 有個 auto trait 叫 Unpin

  • 實作了 Unpin 的型別,即使被 Pin 住也可以安全地移動,因為它們不是自引用結構。
  • 幾乎所有普通型別(i32StringVec、…)都自動實作 Unpin
  • async 產生的 Future 不實作 Unpin,因為它們可能是自引用的。
普通型別(String、Vec...)→ 自動 impl Unpin  → Pin 對它形同虛設
async fn 的 Future        → 不 impl Unpin   → Pin 真正發揮作用

四、Pin 的兩個重要方法

// 如果 T: Unpin,可以安全拿到 &mut T
impl<P: DerefMut<Target: Unpin>> Pin<P> {
    pub fn get_mut(self) -> &mut T
}

// 不管 T 是否 Unpin,都能繼續操作 Pin 住的值
impl<P: DerefMut> Pin<P> {
    pub fn as_mut(&mut self) -> Pin<&mut T>
}

五、手動實作 Future 時的 Pin

Pin 最直接的使用場景之一是手動實作 Future

impl Future for MyFuture {
    type Output = i32;

    // self 必須是 Pin<&mut Self>
    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<i32> {
        todo!()
    }
}

六、pin! macro:把值 pin 在 Stack 上

Box::pin() 把值放在 heap 上,有分配成本。很多情況下我們希望把 Future pin 在 stack 上,這時就需要 pin! macro。

手動在 stack 上 pin 既麻煩又需要 unsafe

let mut future = some_async_fn();
// 你必須保證之後不會 move future
let mut pinned = unsafe { Pin::new_unchecked(&mut future) };

pin! macro 讓這件事變得安全且方便。

tokio::pin!

tokio::pin! 接受變數名稱,直接在當前 scope 做 shadowing:

// tokio::pin! 大致展開成這樣:
macro_rules! pin {
    ($x:ident) => {
        let mut $x = $x;                                       // (1) move 到新綁定
        let mut $x = unsafe { Pin::new_unchecked(&mut $x) };  // (2) shadow 成 Pin<&mut T>
    }
}

使用方式:

use tokio::pin;

let future = some_async_fn();
pin!(future); // future 的型別現在是 Pin<&mut impl Future>
future.await;

步驟 (1) 把值 move 到新綁定,步驟 (2) 把名字蓋掉,改成 Pin<&mut T>。此後你只看得到 Pin 版本,原本可以 move 的那個綁定已不可見,借用規則自然防止你移動它。

std::pin::pin!(Rust 1.68+)

標準庫的版本接受任意表達式,靠的是另一個機制——Temporary Lifetime Extension(暫存值生命週期延伸)

use std::pin::pin;

let fut = pin!(some_async_fn());

展開後:

let fut = {
    let mut _pinned = some_async_fn();
    unsafe { Pin::new_unchecked(&mut _pinned) }
};

Rust 編譯器發現回傳值裡含有對 block 內部暫存值的引用,於是自動把 _pinned 的生命週期延伸到與 fut 相同的 scope,確保不會出現懸空指標。這是編譯器特殊規則,不是 unsafe hack。

兩者對比

tokio::pin! std::pin::pin!
接受 變數名 ident 任意表達式 expr
機制 變數 shadowing Temporary lifetime extension
語法 pin!(fut); let fut = pin!(expr);

七、「move 進 pin! 不就跟 Pin 的目的矛盾?」

這是個很常見的疑惑。答案是:move 發生在 pin 之前,Pin 的承諾是從 pin 的那一刻之後才開始的。

Pin 的合約不是「這個值永遠不能移動」,而是:

「一旦被 pin 住之後,就不能再移動。」

async fn 產生的 Future,在第一次被 poll 之前,內部根本還沒有任何自引用,只是一個普通的初始狀態機。自引用是在 poll 的過程中才逐漸形成的:

[建立 Future] → move 進 _pinned → [Pin 住] → [第一次 poll] → 自引用開始形成
                    ✅ 安全                       ✅ 此後不再 move

所以 pin! 在 pin 之前 move 值是完全合法的,並不矛盾。


八、最重要的使用場景:select!

這是你最常需要 pin! 的地方。tokio::select! 需要能夠跨多次 poll 同一個 Future,每次都必須是同一個 pinned Future,不能 move 它:

use tokio::{pin, time};
use std::time::Duration;

#[tokio::main]
async fn main() {
    let long_operation = do_something_slow();
    pin!(long_operation);

    let mut interval = time::interval(Duration::from_secs(1));

    loop {
        tokio::select! {
            result = &mut long_operation => {
                println!("完成:{:?}", result);
                break;
            }
            _ = interval.tick() => {
                println!("還在等待中...");
            }
        }
    }
}

如果沒有 pin!,每次進入 loop 都會 move long_operation,之前的 poll 狀態就丟失了。


九、什麼時候用哪個?

情境 建議
現代專案,不依賴 Tokio std::pin::pin!(Rust 1.68+)
Tokio 專案 tokio::pin!std::pin::pin! 皆可
需要跨執行緒傳遞 Box::pin()(heap 分配,Box 本身可 move)
效能敏感,避免 heap 分配 pin!(stack)

總結

  • Pin 是 Rust 用型別系統做出的「這個值不能再被移動」的承諾,專為自引用結構(尤其是 async Future)而設計。
  • Unpin 讓普通型別不受 Pin 影響,只有真正需要固定位置的型別(如 async Future)才會受到約束。
  • tokio::pin! 靠 shadowing,std::pin::pin! 靠 temporary lifetime extension,兩者都能安全地把值 pin 在 stack 上。
  • Pin 的承諾從「pin 的那一刻」開始,pin 之前的 move 完全合法。