Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ピン留め

ピン留めは、悪名高いほど難しい概念であり、いくつかの微妙で紛らわしい性質があります。このセクションでは、このトピックを詳細に(議論の余地はありますが、詳細すぎるほど)扱います。ピン留めは Rust における async プログラミングの実装にとって重要です1が、ピン留めに一度も遭遇せずに、ましてや深く理解する必要もなく、かなり先まで進むことは可能です。

最初のセクションでは、ピン留めの概要を説明します。ほとんどの async プログラマーにとっては、これを知っていれば十分であることを期待しています。この章の残りは、実装者、高度または低レベルの async プログラミングを行う人、そして好奇心のある人向けです。

概要の後、この章ではピン留めに入る前に、ムーブセマンティクスに関する背景を説明します。一般的な考え方を扱い、その後 Pin 型と Unpin 型、ピン留めがその目的をどのように達成するか、そして実際にピン留めを扱う際のいくつかのトピックを取り上げます。その後、ピン留めと async プログラミングに関するセクション、およびピン留めの代替案や拡張(本当に好奇心のある人向け)に関するセクションがあります。章の最後には、別の説明や参考資料へのリンクがあります。

TL;DR

Pin は、ポインターが、ドロップされるまで移動しないオブジェクトを指していることを示します。ピン留めは言語やコンパイラーに組み込まれているものではありません。参照先への可変参照へのアクセスを単に制限することで機能します。unsafe コードではピン留めを破るのは十分に簡単ですが、unsafe コードにおけるすべての安全性保証と同様に、そうしないことはプログラマーの責任です。

オブジェクトが移動しないことを保証することで、ピン留めは構造体のあるフィールドから別のフィールドへの参照(自己参照と呼ばれることもあります)を持つことを安全にします。これは async 関数の実装に必要です(async 関数は、変数がフィールドとして保存されるデータ構造として実装されます。変数同士が参照し合う可能性があるため、async 関数を実装する Future のフィールド同士も参照し合える必要があります)。ほとんどの場合、プログラマーはこの詳細を意識する必要はありませんが、Future を直接扱う場合には、Future::poll のシグネチャーが self にピン留めされていることを要求するため、意識する必要があるかもしれません。

Future を参照で使用している場合、その参照が引き続き Future トレイトを実装することを保証するために、pin!(...) を使って参照をピン留めする必要があるかもしれません(これは select マクロでよく発生します)。同様に、Future に対して手動で poll を呼び出したい場合(通常は別の Future を実装しているため)、その Future へのピン留めされた参照が必要になります(pin! を使用するか、引数がピン留めされた型を持つことを保証してください)。Future を実装している場合、または何らかの別の理由でピン留めされた参照を持っており、オブジェクトの内部への可変アクセスが必要な場合は、ピン留めされたフィールドに関する以下のセクションを理解し、どのように行うか、いつ安全なのかを知る必要があります。

ムーブセマンティクス

ピン留めや関連するトピックを議論するうえで有用な概念が、場所 という考え方です。場所とは、値が存在できるメモリの塊(アドレスを持つもの)です。参照は実際には値を指しているのではなく、場所を指しています。だからこそ *ref = ... は意味を持ちます。デリファレンスによって得られるのは値のコピーではなく、場所です。場所は言語実装者にはよく知られていますが、通常、プログラミング言語では暗黙的です(Rust でも暗黙的です)。プログラマーは通常、場所についてのよい直感を持っていますが、それを明示的に考えてはいないかもしれません。

参照だけでなく、変数やフィールドアクセスも場所として評価されます。実際、代入の左辺に現れることができるものは何であれ、実行時には場所でなければなりません(そのため、コンパイラー用語では場所は「lvalue」と呼ばれます)。

Rust では、可変性は場所の性質であり、借用の結果として「凍結」されることも同様です(その場所は借用されている、と言うこともできます)。

Rust における代入はデータを 移動 します(ほとんどの場合です。一部の単純なデータはコピーセマンティクスを持ちますが、ここではあまり重要ではありません)。let b = a; と書くと、a によって識別される場所のメモリにあったデータが、b によって識別される場所へ移動されます。つまり、代入後にはデータは b に存在しますが、a にはもはや存在しません。別の言い方をすれば、そのオブジェクトのアドレスは代入によって変更されます2

移動元の場所を指すポインターが存在していた場合、それらのポインターはもはやそのオブジェクトを指していないため無効になります。これが、借用参照がムーブを防ぐ理由です。let r = &a; let b = a; は不正です。r の存在によって a の移動が防がれます。

コンパイラーが認識しているのは、オブジェクトの外部からそのオブジェクトへの参照だけです(上の例や、オブジェクトのフィールドへの参照など)。オブジェクトの完全に内部にある参照は、コンパイラーからは見えません。次のようなものを書くことが許されていたと想像してみてください。

#![allow(unused)]
fn main() {
struct Bad {
    field: u64,
    r: &'self u64,
}
}

Bad のインスタンス b があり、b.rb.field を指しているとします。let a = b; では、b.field への内部参照 b.r はコンパイラーから見えないため、b への参照は存在しないように見え、したがって a へのムーブは問題ないように見えます。しかし、もしそれが起きた場合、ムーブ後には a.r は期待どおり a.field を指すのではなく、b.field の古い位置にある無効なメモリを指すことになり、Rust の安全性保証に違反します。

データの移動は値に限定されません。データは一意参照から取り出して移動することもできます。Box をデリファレンスすると、データはヒープからスタックへ移動します。takereplaceswap(いずれも std::mem にあります)は、可変参照(&mut T)からデータを移動します。Box から移動すると、指されていた場所は無効になります。可変参照から移動すると、その場所は有効なままですが、別のデータを含むようになります。

抽象的には、ムーブは元の位置から宛先へビットをコピーし、その後元のビットを消去することで実装されます。しかし、コンパイラーはこれを多くの方法で最適化できます。

ピン留め

重要な注意: まず、ピン留めという抽象的な概念について説明します。これは、特定の型によって表現されるものと正確に同じではありません。進むにつれてこの概念をより具体化し、最終的には異なる型が何を意味するのかについて正確な定義に到達しますが、これらの型のどれも、最初に扱うピン留めの概念と完全に同じ意味を持つわけではありません。 オブジェクトは、移動されたりその他の形で無効化されたりしない場合、ピン留めされています。上で説明したように、これは新しい概念ではありません。オブジェクトを借用すると、その借用の期間中はオブジェクトの移動が防止されます。オブジェクトを移動できるかどうかは Rust の型では明示されませんが、コンパイラには把握されています(そのため “cannot move out of” エラーメッセージが出ることがあります)。借用(および借用によって生じる一時的な移動制限)とは異なり、ピン留めされていることは永続的です。オブジェクトはピン留めされていない状態からピン留めされた状態に変化できますが、いったんピン留めされると、drop されるまでピン留めされたままでなければなりません3

ポインター型が指し先の所有権や可変性を反映する(たとえば Box&&mut&)のと同じように、ピン留めされているかどうかもポインター型に反映したいと考えます。これはポインターの性質ではありません。ポインター自体がピン留めされている、あるいは移動可能であるわけではありません。これは指されている場所の性質、つまり指し先をその場所から移動できるかどうかです。

大まかに言えば、Pin<Box<T>> は所有されたピン留め済みオブジェクトへのポインターであり、Pin<&mut T> は一意に借用された可変なピン留め済みオブジェクトへのポインターです(これに対して &mut T は、一意に借用された可変なオブジェクトへのポインターであり、そのオブジェクトはピン留めされている場合もされていない場合もあります)。

ピン留めの概念は Rust 1.0 より後に追加されたものであり、後方互換性の理由から、オブジェクトがピン留めされているかどうかを明示的に表現する方法はありません。表現できるのは、参照がピン留め済みオブジェクトを指しているか、ピン留めされていないオブジェクトを指しているかだけです。

ピン留めは可変性と直交します。オブジェクトは可変で、かつピン留めされている(Pin<&mut T>)場合も、されていない(&mut T)場合もあります(つまり、オブジェクトは変更可能であり、さらにその場にピン留めされているか、移動可能であるかのどちらかです)。また、不変で、かつピン留めされている(Pin<&T>)場合も、されていない(T)場合もあります(つまり、オブジェクトは変更できず、さらに移動もできないか、変更はできないが移動はできるかのどちらかです)。&T は変更も移動もできませんが、ピン留めされているわけではありません。なぜなら、その移動不能性は一時的なものにすぎないからです。

Unpin

移動するかしないかという観点からピン留めを導入しましたし、名前からもある程度それが示唆されますが、Pin は実際には、指し先が実際に移動するかどうかについて多くを教えてくれるものではありません。

何だって? はぁ。

ピン留めは実際には、移動に関するものではなく、有効性に関する契約です。これは、オブジェクトがアドレスに依存する場合、そのアドレスは変化しない(したがって、そのフィールドのアドレスなど、そこから派生したアドレスも変化しない)ことを保証します。Rust のほとんどのデータはアドレスに依存しません。移動されてもすべて問題ありません。Pin は、指し先がそのアドレスに関して有効であり続けることを保証します。指し先がアドレスに依存する場合、それは移動できません。アドレスに依存しない場合、移動されるかどうかは問題ではありません。

Unpin は、オブジェクトがアドレスに依存するかどうかを表すトレイトです。オブジェクトが Unpin を実装している場合、それはアドレスに依存しません。オブジェクトが !Unpin である場合、それはアドレスに依存します。別の見方をすれば、ピン留めをオブジェクトをその場所に保持する行為と考えるなら、Unpin はその行為を解除してオブジェクトの移動を許可しても安全であることを意味します。

Unpin は auto-trait であり、ほとんどの型は Unpin です。!Unpin なフィールドを持つ型、または明示的にオプトアウトした型だけが Unpin ではありません。オプトアウトするには、PhantomPinned フィールドを持たせるか、(nightly を使用している場合は)impl !Unpin for ... {} を使います。

Unpin を実装する型に対しては、Pin は本質的に何もしません。Pin<Box<T>>Pin<&mut T> は、Box<T>&mut T> とまったく同じように使用できます。実際、Unpin 型については、Pin::newPin::into_inner を使って、Pin されたポインターと通常のポインターを自由に相互変換できます。改めて述べる価値があります。Pin<...> は指し先が移動しないことを保証するものではなく、指し先が !Unpin である場合にのみ移動しないことを保証します。

上記の実用上の含意は、Unpin 型とピン留めを扱うことは、Unpin ではない型を扱う場合よりもはるかに簡単だということです。実際、Pin マーカーは Unpin 型および Unpin 型へのポインターに対しては基本的に効果を持たず、ピン留めに関する保証や要件は基本的に無視できます。

Unpin はオブジェクト単体の性質として理解すべきではありません。Unpin が変える唯一のことは、オブジェクトが Pin とどのように相互作用するかです。ピン留めの文脈以外で Unpin 境界を使っても、コンパイラの振る舞いや、そのオブジェクトに対して何ができるかには影響しません。Unpin を使う唯一の理由は、ピン留めと組み合わせるため、またはその境界をピン留めとともに使われる場所まで伝播させるためです。

Pin

Pin はマーカー型です。型検査において重要ですが、コンパイル時に消去され、実行時には存在しません(Pin<Ptr>Ptr と同じメモリレイアウトおよび ABI を持つことが保証されています)。これはポインター(Box など)のラッパーであるため、ポインター型のように振る舞いますが、間接参照を追加するわけではありません。プログラムの実行時には、Box<Foo>Pin<Box<Foo>> は同じです。Pin はポインターそのものというより、ポインターに対する修飾子と考える方が適切です。

Pin<Ptr> は、Ptr 自体ではなく、Ptr の指し先がピン留めされていることを意味します。つまり、Pin は、指し先が drop されるまで、そのアドレスに関して有効であり続けることを保証します。指し先がアドレスに依存する(つまり !Unpin である)場合、その指し先は移動されません。

値のピン留め

オブジェクトはピン留めされた状態で作成されるわけではありません。オブジェクトはピン留めされていない状態から始まり(自由に移動される可能性があり)、そのオブジェクトを指すピン留めポインターが作成されたときにピン留めされた状態になります。オブジェクトが Unpin であれば、これは Pin::new を使えば些細なことです。しかし、オブジェクトが Unpin でない場合、それをピン留めするには、エイリアスを通じて移動または無効化されないことを保証しなければなりません。 ヒープ上のオブジェクトをピン留めするには、Box::pin を使用して新しいピン留め用の Box を作成するか、Box::into_pin を使用して既存の Box をピン留め用の Box に変換できます。どちらの場合でも、最終的には Pin<Box<T>> が得られます。他のいくつかのポインター(ArcRc など)にも同様の仕組みがあります。そうでないポインター、または独自のポインター型については、Pin::new_unchecked を使用してピン留めされたポインターを作成する必要があります4。これは unsafe 関数であるため、プログラマーは Pin の不変条件が維持されることを保証しなければなりません。つまり、指し先はどのような状況でも、そのデストラクタが呼び出されるまで有効なままでなければなりません。これを保証するにはいくつか微妙な詳細があります。詳細については、その関数のドキュメント または以下のセクション pinning の仕組み を参照してください。

Box::pin はオブジェクトをヒープ内の場所にピン留めします。スタック上のオブジェクトをピン留めするには、pin マクロを使用して、可変参照(Pin<&mut T>)を作成し、ピン留めできます5

Tokio にも pin マクロがあり、std のマクロと同じことを行い、さらにマクロ内で変数への代入もサポートしています。futures-rs クレートと pin-utils クレートには pin_mut マクロがあり、以前は一般的に使われていましたが、現在は std のマクロを優先するため非推奨になっています。

Pin::static_refPin::static_mut を使用して static 参照をピン留めすることもできます。

ピン留めされた型の使用

理論上、ピン留めされたポインターの使用は他のポインター型の使用と同じです。しかし、これは最も直感的な抽象化ではなく、言語サポートもないため、ピン留めされたポインターの使用はかなり扱いにくくなりがちです。pinning を使用する最も一般的なケースは Future と Stream を扱う場合であり、それらの詳細については以下でより詳しく説明します。

ピン留めされたポインターを不変借用参照として使用するのは、PinDeref を実装しているため簡単です。必要であれば明示的な deref() を使用しつつ、ほとんどの場合 Poll<Ptr<T>>&T として扱えます。同様に、Pin<&T> を取得するのも as_ref() を使用すれば非常に簡単です。

ピン留めされた型を扱う最も一般的な方法は Pin<&mut T> を使用することです(たとえば Future::poll)。しかし、ピン留めされたオブジェクトを生成する最も簡単な方法は Box::pin であり、これは Pin<Box<T>> を返します。後者は Pin::as_mut を使用して前者に変換できます。しかし、参照を再利用するための言語サポート(暗黙の再借用)がないため、その結果を再利用するのではなく、as_mut を呼び続ける必要があります。例(as_mut のドキュメントより):

#![allow(unused)]
fn main() {
impl Type {
    fn method(self: Pin<&mut Self>) {
        // 何かを行う
    }

    fn call_method_twice(mut self: Pin<&mut Self>) {
        // `method` は `self` を消費するため、`as_mut` を介して `Pin<&mut Self>` を再借用する。
        self.as_mut().method();
        self.as_mut().method();
    }
}
}

ピン留めされた指し先に他の方法でアクセスする必要がある場合は、Pin::into_inner_unchecked を介して行えます。ただし、これは unsafe であり、Pin の安全性要件が尊重されることを保証するために、非常に 注意しなければなりません。

pinning の仕組み

Pin は、ポインターのための単純なラッパー構造体(別名 newtype)です。ジェネリックパラメータに Deref 境界を要求することで、有用なことを行うにはポインターに対してのみ機能するよう強制されていますが、これは安全性を保つためというより、意図を表現するためのものにすぎません。ほとんどの newtype ラッパーと同様に、Pin は実行時の効果のためではなく、コンパイル時に不変条件を表現するために存在します。実際、ほとんどの状況では、Pin と pinning の仕組みはコンパイル中に完全に消えます。

正確には、Pin が表現する不変条件は、単なる移動可能性ではなく有効性に関するものです。また、これはポインターがピン留めされた後にのみ適用される有効性の不変条件でもあります。それ以前には Pin は効果を持たず、何かがピン留めされる前に何が起こるかについて何の要件も課しません。ポインターがいったんピン留めされると、Pin は、その指し先のオブジェクトが、オブジェクトのデストラクタが呼び出されるまで、メモリ内の同じアドレスで有効なままであることを要求します(そして安全なコードでは保証します)。

不変ポインター(たとえば借用参照)の場合、Pin は効果を持ちません。指し先は変更も置換もできないため、無効化される危険がないからです。

変更を許すポインター(たとえば Box&mut)の場合、そのポインターへ直接アクセスできること、または指し先への可変参照(&mut)にアクセスできることにより、指し先の変更や移動が可能になる場合があります。Pin は、ポインターまたは可変参照への直接アクセスを得るための(unsafe でない)方法を一切提供しません。ポインターがその指し先への可変参照を提供する通常の方法は DerefMut を実装することですが、Pin は指し先が Unpin の場合にのみ DerefMut を実装します。

この実装は非常に単純です!まとめると、Pin はポインターを包むラッパー構造体であり、指し先への不変アクセスのみを提供します(指し先が Unpin の場合は可変アクセスも提供します)。それ以外は詳細です(そして unsafe コードのための微妙な不変条件です)。利便性のため、PinPin 型間で変換するための機能などを提供します(ポインターが Pin から抜け出せないため常に安全です)。

Pin は、ピン留めされたポインターを作成したり、基盤となるデータへアクセスしたりするための unsafe 関数も提供します。すべての unsafe 関数と同様に、安全性の不変条件を維持する責任はコンパイラではなくプログラマーにあります。残念ながら、pinning の安全性の不変条件はいくぶん散在しています。つまり、それらは異なる場所で強制されており、グローバルで統一された形で説明するのが困難です。ここでは詳細には説明せずドキュメントを参照してもらいますが、要約を試みます(詳細な概要についてはモジュールドキュメントを参照してください):

  • 新しいピン留めされたポインター new_unchecked を作成すること。プログラマーは、参照先がピン留めされていること(つまり、ピン留め不変条件に従っていること)を保証しなければなりません。この要件は、ポインター型だけで満たされる場合もあれば(例: Box の場合)、参照先の型の関与が必要な場合もあります(例: &mut の場合)。これには次が含まれます(ただし、これらに限定されません)。
    • Deref および DerefMutself からムーブしないこと。
    • Drop を適切に実装すること。drop 保証を参照してください。
    • ピン留め保証が必要な場合は、PhantomPinned を使用して Unpin からオプトアウトすること。
    • 参照先は #[repr(packed)] であってはなりません。
  • ピン留めされた値 into_inner_uncheckedget_unchecked_mutmap_unchecked、および map_unchecked_mut にアクセスすること。データがアクセスされた瞬間から、そのデストラクタが実行されるまで、ピン留め保証(データをムーブしないことを含む)を強制する責任はプログラマーにあります(この責任範囲は unsafe 呼び出しの範囲を超えて広がり、基になるデータに何が起きても適用されることに注意してください)。
  • ピン留めされた型からデータをムーブする他の手段を一切提供しないこと(そのような手段には unsafe な実装が必要になります)。

ピン留めポインター型

前述のとおり、Pin はポインター型をラップします。Pin<Box<T>>Pin<&T>Pin<&mut T> はよく見かけます。技術的には、ピン留めポインター型に対する唯一の要件は Deref を実装していることです。しかし、他の任意のポインター型について Pin<Ptr> を作成する方法は、unsafe コードを使用する場合(new_unchecked 経由)を除いてありません。そうする場合、ピン留め契約を保証するためにポインター型に対する要件があります。

  • ポインターの Deref および DerefMut の実装は、その参照先からムーブしてはなりません。
  • Pin が作成された後は、Pin がドロップされた後であっても、いかなる時点でも参照先への &mut 参照を取得できてはなりません(これが、&mut T から Pin<&mut T> を安全に構築できない理由です)。これは複数の手順を経由しても、参照を経由しても真であり続けなければなりません(これにより RcArc の使用が妨げられます)。
  • ポインターの Drop の実装は、その参照先をムーブしてはならず、また他の方法で無効化してもなりません。

詳細については、new_uncheckedドキュメントを参照してください。

ピン留めと Drop

ピン留め契約は、ピン留めされたオブジェクトがドロップされるまで適用されます(技術的には、これは drop メソッドが呼び出された時点ではなく、戻った時点を意味します)。通常、オブジェクトが破棄されると drop は自動的に呼び出されるため、これはかなり単純です。オブジェクトのライフサイクルに関して手動で何かを行っている場合は、もう少し考慮が必要になるかもしれません。ピン留めされている(またはピン留めされている可能性がある)オブジェクトがあり、そのオブジェクトが Unpin ではない場合、そのオブジェクトのメモリまたはアドレスを解放または再利用する前に、その drop メソッドを(drop_in_place を使用して)呼び出さなければなりません。詳細については、std ドキュメントを参照してください。

アドレスに敏感な型(つまり !Unpin である型)を実装している場合は、Drop の実装に特別な注意を払わなければなりません。drop における self 型は &mut Self ですが、その self 型を Pin<&mut Self> として扱わなければなりません。言い換えると、drop 関数が戻るまでオブジェクトが有効なままであることを保証しなければなりません。これをソースコードで明示する方法の 1 つは、次のイディオムに従うことです。

#![allow(unused)]
fn main() {
impl Drop for Type {
    fn drop(&mut self) {
        // この値はドロップ後に二度と使われないことが分かっているため、
        // `new_unchecked` は問題ない。
        inner_drop(unsafe { Pin::new_unchecked(self)});

        fn inner_drop(this: Pin<&mut Self>) {
            // 実際の drop コードはここに記述する。
        }
    }
}
}

有効性要件は、実装される型に依存することに注意してください。特にオブジェクトの破棄に関して、これらの要件を正確に定義することが推奨されます。複数のオブジェクトが関与する可能性がある場合(例: 侵入型リンクリスト)は特にそうです。ここで正しさを保証することは、興味深いものになるでしょう!

メソッドにおけるピン留めされた self

ピン留めされた型でメソッドを呼び出すと、それらのメソッドにおける self 型について考えることになります。メソッドが self を変更する必要がない場合、Pin<...> は借用参照へ逆参照できるため、引き続き &self を使用できます。しかし、self を変更する必要があり(かつ型が Unpin ではない)場合は、&mut selfself: Pin<&mut Self> のどちらかを選択する必要があります(ピン留めポインターは後者の型へ暗黙的に型強制できませんが、Pin::as_mut を使用して簡単に変換できます)。

&mut self を使用すると実装は容易になりますが、そのメソッドはピン留めされたオブジェクトでは呼び出せないことになります。self: Pin<&mut Self> を使用すると、ピン射影(次のセクションを参照)を考慮する必要があり、ピン留めされたオブジェクトでしか呼び出せません。これは少し分かりにくいものの、ピン留めが段階的な概念であることを思い出すと直感的に理解できます。オブジェクトはピン留めされていない状態で始まり、ある時点で段階の変化を経てピン留めされた状態になります。&mut self メソッドは最初の(ピン留めされていない)段階で呼び出せるものであり、self: Pin<&mut Self> メソッドは 2 番目の(ピン留めされた)段階で呼び出せるものです。

drop は(どちらの段階で呼び出される可能性があるにもかかわらず)&mut self を受け取ることに注意してください。これは言語の制限と後方互換性への要望によるものです。これにはコンパイラでの特別な扱いが必要であり、安全性要件を伴います。

ピン留めされたフィールド、構造的ピン留め、ピン射影

オブジェクトがピン留めされている場合、そのフィールドの「ピン留めされている」性質について何が分かるでしょうか? 答えはデータ型の実装者が行った選択に依存し、普遍的な答えはありません(実際、同じオブジェクトの異なるフィールドで異なる場合があります)。

オブジェクトのピン留めされている性質がフィールドへ伝播する場合、そのフィールドは「構造的ピン留め」を示す、またはピン留めがそのフィールドに射影される、と言います。この場合、射影メソッド fn get_field(self: Pin<&mut Self>) -> Pin<&mut Field> があるべきです。フィールドが構造的にピン留めされていない場合、射影メソッドはシグネチャ fn get_field(self: Pin<&mut Self>) -> &mut Field を持つべきです。どちらのメソッドを実装する場合(または類似のコードを実装する場合)も unsafe コードが必要であり、どちらの選択にも安全性への影響があります。ピンの伝播は一貫していなければならず、フィールドは常に構造的にピン留めされているか、そうでないかのいずれかでなければなりません。フィールドがある時点では構造的にピン留めされ、別の時点ではそうでないというのは、ほとんど常に非健全です。 ピン留めは、そのフィールドが集約データ型のアドレスに敏感な部分である場合、そのフィールドへ射影されるべきです。つまり、ピン留めされている集約が、ピン留めされているフィールドに依存している場合、ピン留めはそのフィールドへ射影されなければなりません。たとえば、集約の別の部分からそのフィールドへの参照がある場合や、フィールド内に自己参照がある場合、ピン留めはそのフィールドへ射影されなければなりません。一方、ジェネリックなコレクションでは、コレクションはその内容の振る舞いに依存しないため、ピン留めをその内容へ射影する必要はありません(これは、コレクションが含んでいるジェネリックな要素の実装に依存できないため、コレクション自体もその要素のアドレスに依存できないからです)。

unsafe コードを書く場合、ピン留めの保証が適用されると仮定できるのは、構造的にピン留めされたオブジェクトのフィールドに対してのみです。一方で、構造的にピン留めされていないフィールドは安全に移動可能として扱うことができ、それらに対するピン留め要件を気にする必要はありません。特に、あるフィールドが Unpin でなくても、そのフィールドが常に構造的にピン留めされていないものとして扱われる限り、構造体は Unpin になれます。

フィールドが構造的にピン留めされている場合、集約構造体に対するピン留め要件はそのフィールドにも及びます。集約がピン留めされている間は、いかなる状況でもコードがそのフィールドの内容を移動することはできません(これには常に unsafe コードが必要になります)。構造的にピン留めされたフィールドは、移動される前(割り当て解除を含む)に、パニックの場合であっても drop されなければなりません。つまり、集約の Drop 実装内では注意が必要です。さらに、集約構造体は、その構造的にピン留めされたフィールドがすべて Unpin でない限り、Unpin になることはできません。

ピン射影のためのマクロ

ピン射影を助けるために利用できるマクロがあります。

pin-project クレートは、#[pin_project] 属性マクロ(および #[pin] ヘルパー属性)を提供します。これは、アノテーションされた型のピン留め版を作成し、その型上の project メソッドを使ってアクセスできるようにすることで、安全なピン射影を実装してくれます。

Pin-project-lite は、宣言的マクロ(pin_project!)を使う代替手段で、pin-project と非常によく似た方法で動作します。Pin-project-lite は、手続き型マクロではないため、手続き型マクロを実装するための依存関係をプロジェクトに追加しないという意味で軽量です。ただし、pin-project より表現力が低く、カスタムエラーメッセージも提供しません。手続き型マクロの依存関係を追加したくない場合は Pin-project-lite が推奨され、それ以外の場合は pin-project が推奨されます。

Pin-utils は、ピン射影の実装を助ける unsafe_pinned マクロを提供しますが、クレート全体が上記のクレートおよび現在 std にある機能を優先して非推奨になっています。

ピン留めされたポインターへの代入

一般に、ピン留めされたポインターへ代入することは安全です。これは通常の方法(*p = ...)ではできませんが、Pin::set を使えば可能です。より一般には、unsafe コードを使ってポインティーのフィールドへ代入できます。

Pin::set を使うことは常に安全です。これは、以前にピン留めされていたポインティーが drop されてピン留め要件を満たし、新しいポインティーはピン留めされた場所への移動が完了するまでピン留めされないためです。個々のフィールドへ代入することは、自動的にピン留め要件に違反するわけではありませんが、オブジェクト全体として有効であり続けることを保証するよう注意が必要です。たとえば、あるフィールドへ代入された場合、そのフィールドを参照している他のフィールドは、新しいオブジェクトでも引き続き有効でなければなりません(これはピン留め要件の一部ではありませんが、そのオブジェクトの他の不変条件の一部である可能性があります)。

あるピン留めされたオブジェクトを別のピン留めされた場所へコピーすることは、unsafe コードでのみ可能です。安全性がどのように維持されるかは、個々のオブジェクトに依存します。ピン留め要件に対する一般的な違反はありません。置き換えられるオブジェクトは移動しておらず、コピーされるオブジェクトも移動していないためです。ただし、置き換えられるオブジェクトの有効性には、通常はピン留めによって保護される安全性要件があるかもしれませんが、この場合はプログラマーが確立しなければなりません。たとえば、ab という 2 つのフィールドを持つ構造体があり、ba を参照している場合、その参照が有効であり続けるにはピン留めが必要です。そのような構造体が別の場所へコピーされる場合、b の値は古い a ではなく新しい a を指すように更新されなければなりません。

ピン留めと非同期プログラミング

うまくいけば、async Rust でやりたいことはすべて実現でき、ピン留めを気にする必要はまったくないでしょう。ときには、ピン留めを使う必要がある特殊なケースに遭遇することもあります。また、future、ランタイム、または同様のものを実装したい場合は、ピン留めについて知る必要があります。このセクションでは、その理由を説明します。

async 関数は future として実装されます(セクション TODO を参照してください。これは要約的な概要であり、他の場所でより深く、例を交えて説明するようにしてください)。各 await ポイントで関数の実行が一時停止される可能性があり、その間、生存している変数の値を保存しておく必要があります。それらは本質的に構造体(enum の一部)のフィールドになります。そのような変数は、future 内に保存されている他の変数を参照することがあります。たとえば、次を考えてみてください。

#![allow(unused)]
fn main() {
async fn foo() {
  let a = ...;
  let b = &a;
  bar().await;
  // b を使う
}
}

ここで生成される future オブジェクトは、おおよそ次のようになります。

#![allow(unused)]
fn main() {
struct Foo {
  a: A,
  b: &'self A,  // 不変条件 `self.b == &self.a`
}
}

(少し単純化しており、実行状態などは無視していますが、重要なのは変数/フィールドです)。

これは直感的には理解できますが、残念ながら Rust には 'self は存在しません。そして、それには十分な理由があります。Rust のオブジェクトは移動できることを思い出してください。そのため、次のようなコードは非健全になります。

#![allow(unused)]
fn main() {
let f1 = Foo { ... }; // f1.b == &f1.a
let f2 = f1; // f2.b == &f1.a だが、f1 は f2 へ移動したためもはや存在しない
}

これは単にライフタイムに名前を付けられないという問題ではないことに注意してください。たとえ raw ポインターを使ったとしても、そのようなコードは依然として不正です。

しかし、一度作成されたら Foo のインスタンスは決して移動しないことが分かっているなら、すべてはそのまま機能します。(コンパイラーはそのような場合のために内部的に 'self に似た概念を持っていますが、プログラマーとしては raw ポインターと unsafe コードを使う必要があります)。この移動しないという概念こそが、ピン留めが表すものです。

この要件は Future::poll のシグネチャに現れており、self(future)の型は Pin<&mut Self> です。たいていの場合、async/await を使っているときは、コンパイラーがピン留めとピン留め解除を処理してくれるため、プログラマーがそれを気にする必要はありません。

手動のピン留め

ピン留めが async/await の抽象化を通して漏れ出してくる箇所がいくつかあります。その根本には、Future::pollStream::poll_next のシグネチャにある Pin があります。Future や Stream を直接使う場合(async/await を通してではなく)、うまく動作させるためにピン留めについて考慮する必要があるかもしれません。ピン留めされた型が必要になる一般的な理由には、次のようなものがあります。

  • Future や Stream をポーリングする場合 - アプリケーションコード内で行う場合でも、自分自身の Future を実装する場合でも。
  • ボックス化された Future を使う場合。ボックス化された Future(または Stream)を使っていて、そのため async 関数を使うのではなく Future 型を書き出している場合、それらの型の中で多くの Pin<...> を目にする可能性が高く、Future を作成するために Box::pin を使う必要があります。
  • Future を実装する場合 - poll の中では self がピン留めされているため、self のフィールドへの可変アクセスを得るには、ピン射影や unsafe コードを扱う必要があります。
  • Future や Stream を組み合わせる場合。これはほとんどの場合そのまま動作しますが、Future への参照を取得してからそれをポーリングする必要がある場合(たとえば、ループの外で Future を定義し、ループの中の select! でそれを使う場合)、その参照を Future のように使うために、Future への参照をピン留めする必要があります。
  • Stream を扱う場合 - 現在 Rust では、Stream まわりの抽象化は Future ほど多くないため、Future を扱う場合よりも、コンビネータメソッド(技術的にはピン留めを必要としませんが、参照や Future/Stream の作成に関する問題がより表面化しやすくなるようです)を使ったり、場合によっては手動で poll したりする可能性が高くなります。

代替案と拡張

このセクションは、ピン留めに関する言語設計に興味がある人向けです。async プログラムを読み、理解し、書きたいだけであれば、このセクションを読む必要はまったくありません。

ピン留めは理解が難しく、少しぎこちなく感じられることがあるため、より良い代替案やバリエーションがあるのではないかと考える人はよくいます。ここではいくつかの代替案を取り上げ、それらがなぜ機能しないのか、あるいは予想以上に複雑なのかを示します。

ただしその前に、ピン留めの歴史的背景を理解することが重要です。まったく新しい言語を設計していて、async/await、自己参照、不動型をサポートしたいのであれば、Rust のピン留めよりも確実に良い方法があります。しかし、async/await、Future、ピン留めは Rust の 1.0 リリース後に追加され、強力な後方互換性保証の文脈で設計されました。その厳しい要件に加えて、この機能を合理的な期間で設計・実装したいという要件もありました。いくつかの解決策(たとえば線形型を含むもの)は、Rust プロジェクトのリソースと制約を考慮すると、現実的には数十年単位で測られるような基礎研究、設計、実装を必要としたでしょう。

代替案

まず、Rust の型をデフォルトで移動不能にする解決策の分類について考えてみましょう。これは Rust の基本的なセマンティクスに対する重大な変更であることに注意してください。この分類に属するどの解決策も、後方互換性を実現するには相当な労力が必要になる可能性が高いでしょう(特定の解決策についてそれが可能かどうかを推測するつもりはありませんが、auto trait、derive 属性、エディション、移行ツールなどの技法を使えば、もしかすると可能かもしれません)。

1 つの提案(実際には、セマンティクスを定義する方法がいろいろあるため提案群)は、Move マーカートレイト(Copy に似たもの)を用意して、オブジェクトを移動可能としてマークし、それ以外のすべての型を移動不能にするというものです。Pin とは対照的に、これはポインタではなく値の性質であるため、その影響ははるかに広範囲に及びます。たとえば、bMove を実装していなければ let a = b; はエラーになります。

このアプローチの根本的な問題は、現在のピン留めが段階的な概念(ある場所は最初はピン留めされておらず、その後ピン留めされる)であるのに対し、型は値のライフタイム全体に適用されることです。(ピン留めは値ではなく場所の性質として理解するのが最も適切でもありますが、型は値に適用されます。これがトレイトベースのアプローチにとって根本的な問題なのかどうかは、私にはわかりません)。これは次の 2 つのブログ記事で検討されています: Two Ways Not to MoveErgonomic Self-Referential Types for Rust

さらに、どのような Move トレイトも後方互換性の問題を抱える可能性が高く、「感染性のある境界」(つまり、非常に多くの場所で Move または !Move が必要になること)につながります。

別の提案は、C++ に似たムーブコンストラクタをサポートするというものです。しかし、これはオブジェクトは常にビット単位で移動できるという Rust の基本的な不変条件を破ります。その結果、Rust ははるかに予測しにくくなり、Rust プログラムの理解とデバッグがより困難になります。これは最悪の種類の後方非互換な変更です。なぜなら、コードの作者が前提としていた可能性のある基本的な仮定を変更するため、unsafe コードを静かに壊してしまうからです。さらに、そのような根本的な変更に必要な設計と実装の労力は莫大なものになるでしょう。こうした実務上の問題に加えて、それが実際に機能するかどうかも不明です。ムーブコンストラクタは、移動されるオブジェクト内の参照を修正するために使えるかもしれませんが、オブジェクトの外部から、移動されるオブジェクトへの参照が存在する場合、それらは修正できない可能性があります。

別の種類の潜在的な解決策として、オフセット参照という考え方があります。これは絶対的な参照ではなく相対的な参照です。つまり、別のフィールドへのオフセット参照であるフィールドは、オブジェクトがメモリ内で移動されたとしても、常に同じオブジェクト内を指すことになります。オフセットポインタの問題は、フィールドはオフセットポインタか絶対ポインタのどちらかでなければならないことです。しかし、async 関数内の参照はフィールドになり、そのフィールドはときには Future オブジェクト内部のメモリを参照し、ときにはその外部のメモリを参照します。

拡張

ピン留めをより強力にしたり、扱いやすくしたりするための提案は複数あります。これらの多くは、ピン留めを純粋なライブラリ上の概念ではなく、さまざまな形で言語のより第一級の一部にするための提案です(言語だけでなく std への拡張を含むこともよくあります)。ここでは、より発展したアイデアをいくつか取り上げます。それらは互いに関連しており、いずれも、特に構造的ピン留めと drop の周辺で、ピン留めされた場所の作成と使用を容易にすることで、ピン留めのエルゴノミクスを改善するという一般的な目標を持っています。

Pinned places は、ピン留めは値や型ではなく場所の性質であるという考え方を推し進め、mut に似た pin/pinned 修飾子を参照に追加します。これは再借用やメソッド解決と統合され、ピン留めされた self を持つメソッド呼び出しのエルゴノミクスを改善します。

UnpinCell は、フィールドのネイティブなピン射影をサポートするために、pinned places のアイデアを拡張します。MinPin は、ネイティブなピン射影とより良い drop サポートのための、より最小限の(かつ後方互換性のある)提案です。 Overwrite トレイトは、オブジェクトの一部を変更する権限(foo.f = ...)と、オブジェクト全体を上書きする権限(*foo = ...)の区別を明示する提案中のトレイトです。現在は、どちらもすべての可変参照で許可されています。この提案には、イミュータブルなフィールドも含まれます。OverwriteUnpin のある種の置き換えであり、(pinned places のアイデアの一部と組み合わせることで)pinning を扱う作業を改善できる可能性があります。残念ながら、後方互換性を保ったまま採用することは可能ですが、その移行は他の拡張よりもはるかに多くの作業を伴います。

参考資料

  • std ドキュメント Pin などの振る舞いと保証に関する信頼できる情報源です。優れたドキュメントです。
  • RFC 2349 pinning を提案した RFC です。安定化された API はここで提案されたものとは少し異なりますが、RFC には中核となる概念とその根拠についての優れた説明があります。
  • pinning を説明しているブログ記事やその他のリソース:

  1. ピン留めは、async Rust の実装のために特別に設計された低レベルの構成要素であることに注目する価値があります。async Rust に直接結び付いているわけではなく、他の目的にも使用できますが、汎用的な仕組みとして設計されたものではなく、特に自己参照フィールドに対するそのまま使える解決策ではありません。async コード以外の用途でピン留めを使う場合は、一般的に、厚い抽象化の層で包まれている場合にのみうまく機能します。というのも、多くの細かく扱いにくく、推論が難しい unsafe コードが必要になるからです。

  2. ここではソースコードと実行時を少し混同しています。完全に明確にすると、変数は実行時には存在しません。(コンパイルされた)スニペットは複数回実行されるかもしれません(たとえば、ループ内にある場合や、複数回呼び出される関数内にある場合)。各実行ごとに、ソースコード内の変数は実行時には異なるアドレスで表されます。

  3. 永続性はピン留めの根本的な側面ではなく、Rust におけるピン留めの位置づけと、それを取り巻く安全性保証の一部です。ピン留めを一時的にしても安全に表現でき、ピン留め保証の利用者がその時間的スコープを信頼できるのであれば、一時的なピン留めでも問題ありません。しかし、それは現在の Rust でも、現実的な拡張でも不可能です。

  4. Box(または他の std ポインター)は、pinning の実装でもコンパイラでも特別扱いされていません。BoxPin の API に含まれる unsafe 関数を使用して Box::pin を実装しています。Pin の安全性要件は、Box の安全性保証によって満たされます。

  5. これは厳密には、非 async 関数においてのみスタックへのピン留めです。async 関数では、すべてのローカル変数が async 疑似スタックに割り当てられるため、ピン留めされる場所は、その async 関数の基盤となる Future の一部としてヒープ上に格納される可能性が高いです。