はじめに
Rustでasync/awaitを使って非同期処理を書いていると、コンパイラエラーにPinやUnpinという見慣れない単語が出てくることがあります。
error[E0277]: `*mut ()` cannot be unpinned
これらは一体何者なのでしょうか?この記事では、PinとUnpinがなぜ存在し、どのように使うのかをわかりやすく解説します。
なぜ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を使う
}
このとき生成されるステートマシンはdataとsliceの両方を保持しており、sliceはdataを指しています。これが自己参照構造体です。
自己参照構造体をムーブすると何が起きるか
通常の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の型(i32、String、Vec<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でない型ではunsafeなget_unchecked_mut()を使う必要があり、使用には細心の注意が必要です。
まとめ
| 概念 | 説明 |
|---|---|
Pin<P> | ポインタPが指す値をメモリ上に固定する型 |
Unpin | ムーブしても安全な型が実装するトレイト |
PhantomPinned | Unpinを実装させないためのマーカー |
Box::pin | ヒープ上に値を固定する最も簡単な方法 |
pin!マクロ | スタック上に値を固定するTokio提供マクロ |
PinとUnpinはRustの非同期処理を安全に動かすための重要な仕組みです。普段のasync/awaitの利用では意識しなくても済む場面がほとんどですが、tokio::select!の繰り返し利用や独自のFuture実装をするときに必ず出てきます。
「自己参照構造体をムーブさせないための保護装置」という本質を理解しておくと、エラーに出会ったときに慌てずに対処できるようになります。ぜひ実際にコードを書いて動作を確かめてみてください。