Pin/Unpinについて

Rust の async 周りでは Pin/Unpin というものが使われている。 ユーザーレベルでは、直接使うことは少ないが、ライブラリ使用時にトレイト境界を書くときとか、自前でコンビネータを書こうとすると理解する必要がある。 ということで自分が理解した範囲の Pin/Unpin についてを残しておく。

async 関数について

Rust の async 関数や async block はコンパイラーマジックによって Future traitを実装したステートマシンに変換される。 Future trait は、poll メソッドを実装する必要がある。 このメソッドの self は Pin に包まれており、async は暗黙的に Pin を使用していることがわかる。

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;

まず Pin を考える前に、async がどういう変換が行われるか考えよう。

次の async 関数を考える。

async fn foo() -> () {
  let x = 1;
  let p = &x;
  bar().await
}

これは例えば次のような実装に変換される。 (イメージなのでコンパイルできないが)

struct Foo<F: Future<Output = ()>> {
    init: bool,
    x: u32,
    p: *const u32,
    bar: F,
}

impl Future for Foo {
    type Output = ();
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if !self.init {
            self.as_mut().x = 1;
            self.as_mut().p = &self.as_mut().x;
            self.as_mut().bar = bar();
            self.as_mut().init = true;
        }
        if let Poll::Ready(_) = self.as_mut().poll(cx) {
            return Poll::Ready(());
        } else {
            return Poll::Pending;
        }
    }
}

要するに bar 関数作った Future の poll メソッドが Poll::Ready を返すまでなんども Foo::poll が呼び出されるようなステートマシンに変換される。 この構造だと、async 関数では単なるローカル変数への代入に見えるものが、実際には構造体のフィールドへの代入にする必要がある。 値の場合は単に代入で済むが、ポインタの場合だと構造体へのフィールドへのポインタを取る必要がある。 つまり async により生成されるステートマシンは一般に自己参照構造体を生成することになる。

rust ではこれまで自己参照構造体をうまく扱う方法がなかった。 rust はすべての値を move できるものとして扱う。 自己参照構造体を move してしまうと、内部に保持しているポインタの参照先は変更されないので、 容易にダングリングポインタが作られてしまう。 safe rust ではこのようなものは許容されないので、async を safe な範囲で使えなくなってしまう。 これが原因で async はなかなか導入が進まなかったらしい。 逆に言えば、move できない型を作ることができれば自己参照構造体を扱えるはずだ。 このための型が Pin である。

Pin/Unpin

Pin は自己参照構造体を安全に保持するためのポインタのようなものだ。 ただし、自己参照構造体を指している場合に、Deref traitやポインタを取得するメソッドをは提供できない。 もしできるなら move ができてしまう。

let p = Pin::new(&mut self_referential);
let moved = mem::replace(p.deref_mut(), ...); // OMG

rust は trait を使って選択的に実装を行うことができるので、この機能を使う。 以下の記述では、P をデリファレンスした結果が Unpin であるときにだけ DerefMut が実装されるという意味になる。

https://doc.rust-lang.org/std/pin/struct.Pin.html#impl-DerefMut

impl<P> DerefMut for Pin<P> where
    P: DerefMut,
    <P as Deref>::Target: Unpin

Unpin は Pin に包まれていても move 可能であるという意味を示すトレイトだ。 具体的にいえば自己参照構造体ではないものを示す。 https://doc.rust-lang.org/std/marker/trait.Unpin.html

このトレイトは auto trait として表現されており、基本的にすべての型に対して実装される。 例外は、PhantomPinnedを含む型に対しては実装されない。 また async 関数やブロックで生成されるデータ構造にも実装されていない。コンパイラの実装としては PhantomPinned を使っているかもしれない。 自分で自己参照構造体を作ったときは PhantomPinned をつけておくことで、安全に Pin に入れることができるようになる。 逆にこれを忘れると、Pin に入っているにも関わらず DerefMut が実装されてしまい、move 可能になるので十分に注意する。

さて、ここで振り返ると、そもそも Pin に DerefMut なんか実装しなければ良いのではないかと思える。 それはある意味では正しいのだが、その場合 Future::poll では self が Pin を要求するので、 自分自身を書き換えるという普通のコード自体が safe rust ではかけなくなってしまう。 なのでこのあたりの仕組みは、unsafe を使う範囲を小さくするための設計なのだろう。

Upin の使いどころ

最初にユーザーレベルではあまり使うことはないと書いたが、例えば Future を受け取る関数を書く場合には、Unpin が必要になる。 Future トレイトと Unpin トレイトは直行しているので、関数の引数に Future を受け取る場合、自己参照構造体であるにも関わらず引数に渡すとそのタイミングで move されてしまう。 Future を扱うライブラリではそのようなが起こらないように、Unpin を要求していることがあるので、Unpin をつけておかないとメソッドがないと言われて怒られてしまうことがある。

例えば、future-rs が提供する FutureExt::map が生成する Mapでは poll メソッドで Unpin が要求されている。