あっぽログ
← 記事一覧に戻る

RustのPin・Unpinを理解する:非同期処理の舞台裏を支える仕組みを解説する

はじめに

Rustでasync/awaitを使って非同期処理を書いていると、コンパイラエラーにPinUnpinという見慣れない単語が出てくることがあります。

error[E0277]: `*mut ()` cannot be unpinned

これらは一体何者なのでしょうか?この記事では、PinUnpinがなぜ存在し、どのように使うのかをわかりやすく解説します。


なぜPinが必要なのか

Futureの自己参照問題

Rustのasync fnはコンパイラによって**ステートマシン(状態機械)**に変換されます。このステートマシンは、awaitポイントをまたいで変数の値を保持するために、自分自身のフィールドへの参照を持つ構造体になることがあります。

たとえば次のようなコードを考えてみましょう。

async fn example() {
    let data = vec![1, 2, 3];
    let slice = &data[..]; // dataへの参照
    some_async_fn().await;
    println!("{:?}", slice); // awaitをまたいでsliceを使う
}

このとき生成されるステートマシンはdatasliceの両方を保持しており、slicedataを指しています。これが自己参照構造体です。

自己参照構造体をムーブすると何が起きるか

通常のRustの値はメモリ上で自由に移動(ムーブ)できます。しかし自己参照構造体をムーブすると、内部のポインタが古いアドレスを指したままになり、ダングリングポインタが生じてしまいます。

ムーブ前: [ data: [1,2,3] | slice: → data ]
ムーブ後: [ data: [1,2,3] | slice: → (古いアドレス) ] ← 壊れた状態!

この問題を解決するのがPinです。


Pinとは何か

Pin<P>は「ポインタPが指す値をメモリ上に固定(pin)する」型です。Pinでラップされた値は、明示的に許可しない限りムーブできなくなります。

use std::pin::Pin;
use std::marker::PhantomPinned;

struct SelfRef {
    data: String,
    ptr: *const String, // dataを指す生ポインタ
    _pin: PhantomPinned, // Unpinを実装させない
}

PhantomPinnedをフィールドに追加するだけで、その構造体はUnpinを実装しなくなり、Pinで安全に固定できます。


Unpinとは何か

Unpinトレイトであり、「ムーブしても安全ですよ」という印です。ほとんどのRustの型(i32StringVec<T>など)はデフォルトでUnpinを実装しています。

fn is_unpin<T: Unpin>() {}

is_unpin::<i32>();    // OK
is_unpin::<String>(); // OK

Unpinな型はPinでラップしても自由にムーブできます。Unpinでない型(PhantomPinnedを含む構造体や、async fnが生成するFutureなど)は、Pinの保護が意味を持ちます。


Pinの使い方:実際のコード例

Box::pinでヒープに固定する

最も簡単な方法はBox::pinを使ってヒープに値を固定することです。

use std::pin::Pin;

async fn hello() {
    println!("Hello, Pin!");
}

fn main() {
    // async fnが返すFutureをPinで固定
    let fut: Pin<Box<dyn std::future::Future<Output = ()>>> = Box::pin(hello());

    // TokioランタイムでFutureを実行
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(fut);
}

pin!マクロでスタックに固定する

Tokioが提供するpin!マクロを使うと、スタック上の変数をその場で固定できます。

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

#[tokio::main]
async fn main() {
    let sleep_future = sleep(Duration::from_secs(1));
    pin!(sleep_future); // スタック上で固定

    // PinされたFutureはtokio::selectでも使える
    tokio::select! {
        _ = &mut sleep_future => {
            println!("スリープ完了");
        }
    }
}

tokio::select!は同じFutureを複数回pollするため、FutureをムーブさせないようPinが必要になります。

get_mut・get_uncheckedで内部値にアクセスする

use std::pin::Pin;

fn increment(mut pinned: Pin<&mut i32>) {
    // i32はUnpinなので、安全にget_mutできる
    *pinned.as_mut().get_mut() += 1;
}

fn main() {
    let mut value = 42;
    let pinned = Pin::new(&mut value);
    increment(pinned);
    println!("{}", value); // 43
}

Unpinな型はget_mut()で安全に内部参照を取り出せます。Unpinでない型ではunsafeget_unchecked_mut()を使う必要があり、使用には細心の注意が必要です。


まとめ

概念説明
Pin<P>ポインタPが指す値をメモリ上に固定する型
Unpinムーブしても安全な型が実装するトレイト
PhantomPinnedUnpinを実装させないためのマーカー
Box::pinヒープ上に値を固定する最も簡単な方法
pin!マクロスタック上に値を固定するTokio提供マクロ

PinUnpinはRustの非同期処理を安全に動かすための重要な仕組みです。普段のasync/awaitの利用では意識しなくても済む場面がほとんどですが、tokio::select!の繰り返し利用や独自のFuture実装をするときに必ず出てきます。

「自己参照構造体をムーブさせないための保護装置」という本質を理解しておくと、エラーに出会ったときに慌てずに対処できるようになります。ぜひ実際にコードを書いて動作を確かめてみてください。

← 記事一覧に戻る