std::pin::Pinは、ポインタが指す値がそのポインタを通じて移動されないという型レベルの保証を表し、自分自身の内部を参照する型のように、アドレスが安定している必要がある値のために必要になる
async/awaitでは、.awaitをまたいで生き残るローカル変数や参照が、コンパイラが生成するステートマシンのフィールドになり得るため、ポーリング後にfutureが移動されるのを防ぐためにFuture::pollがPinを要求する
Pinは固定された値を安全なコードで移動することを防ぐが、通常の変更まで禁止するわけではなく、T: Unpinでない場合は安全にPinから&mut Tを取り出せない
- Rustの型の大半はデフォルトでUnpinなので、移動されてはならない自己参照構造体は通常、
PhantomPinnedフィールドを入れて!Unpinにする必要がある
- 実際にはfutureを直接
pollしたり、pinned futureを要求するAPIに渡したりするときにBox::pinまたはstd::pin::pin!を使い、直接Futureや低レベルのasyncプリミティブを実装するときはunsafeな不変条件まで扱う必要がある
Pinが必要な理由
std::pin::Pinはポインタラッパーであり、ポインタが指す値がそのポインタを通じて移動されないという保証を表す
- 核心的な問題は自己参照型で発生する
- 例の構造体
SelfRefはdata: i32とptr: *const i32を持ち、ptrはself.dataを指す
- 構造体インスタンスを別の変数へ移動したり、関数から返したりすると、メモリアドレスが変わる可能性がある
- 生ポインタ
ptrは以前のメモリ位置を指し続け、ダングリングポインタになる
- 自己参照が設定された後は、その値が再び移動されないようにする仕組みが必要になる
async/awaitとFutureで生じる問題
async/awaitとFutureは、Pinが頻繁に登場する代表的な領域である
.await地点をまたいで生き残るローカル変数は、コンパイラが生成するステートマシンのフィールドになる
- あるローカル変数への参照も同じ
.awaitをまたいで生き残ると、生成されたfutureが自己参照的になり得る
- ポーリングが始まった後、futureは自分自身の内部にある別のフィールドを指す参照に依存する場合がある
- この状態でfutureが移動されると、その参照は無効になる
- これを防ぐために、
Future::pollは&mut selfの代わりにPinを受け取る
pub trait Future {
type Output;
fn poll(self: Pin, cx: &mut Context Pin {
pub const fn get_mut(self) -> &'a mut T
where
T: Unpin
{ ... }
}
- 型が
Unpinを実装しない!Unpinであれば、安全なコードだけでは&mut Tを得られない
- この場合、
Pin::get_unchecked_mutのようなunsafeメソッドを使う必要があり、その参照の外へ値が移動されないという約束をコードが守らなければならない
UnpinとPhantomPinned
Unpinを実装する型は、メモリ安全性のためにpinningへ依存しない
// std::marker
pub auto trait Unpin {}
- Rustのほとんどの型は、移動されても問題がないためデフォルトで
Unpinである
Unpinは、明示的に!Unpinにしない限り、すべての型に自動実装される
std::marker::PhantomPinnedは、明示的に**!Unpin**であるマーカー構造体である
- auto traitは自動的に伝播するため、
PhantomPinnedフィールドを含む構造体も自動的に!Unpinになる
use std::marker::PhantomPinned;
struct SelfRef {
data: i32,
ptr: *const i32,
_phantom: PhantomPinned, // makes the entire struct !Unpin
}
- ユーザー定義構造体が固定された後に移動されると安全でないことを宣言する標準的な方法である
- コンパイラは通常、unsafeな生ポインタで作られる自己参照を自動検出できない
- そのため開発者は、自己参照構造体について明示的に
Unpinを放棄しなければならない
- 通常は
PhantomPinnedフィールドを含める方法で処理する
- 自己参照型が誤って
Unpinのまま残っていると、安全なコードがPinから可変参照を取り出して値を移動できてしまう
- そうなると、自己参照を作ったunsafeコードの仮定が破られる
Pinを作る方法
-
Pin自体が値を固定するわけではない
-
Pinを作るということは、そのpointeeがpinの生存期間中、安定したメモリ位置に残ることを証明する作業である
-
Pin::new
let mut value = 42;
let pinned = Pin::new(&mut value);
- このコンストラクタは
T: Unpinのときだけ使用できる
Unpin型はpinningに依存しないため、Pinで包んでも常に安全である
- この場合、pinningの保証は実質的にno-opである
-
std::pin::pin!
- ヒープ割り当てなしでローカルに値をpinする必要があるときは、
pin!マクロを使用できる
use std::pin::pin;
let future = pin!(async {
println!("Hello");
});
- このマクロはローカル変数を作り、その変数を指す
Pinを返す
- コンパイラがそのローカル変数を残りの生存期間中に移動されないよう保証するため、スタック上で
!Unpin値を安全にpinできる
- 名前とは異なり、
pin!はスタックメモリ自体をpinするわけではない
- ローカル変数に結びついた固定参照を作るだけであり、変数がスコープを抜けるとpinningの保証も終わる
-
Box::pin
!Unpin型で最も一般的なコンストラクタはBox::pinである
let pinned = Box::pin(SelfRef { ... });
pin!はローカル変数に結びついたPinを作るが、Box::pinはBoxが所有するPinを返す
- ヒープ割り当て自体は移動しないため、pointeeは
Boxの生存期間中、安定したメモリ位置を持つ
Box自体を移動しても所有する値は移動せず、Box内のポインタだけが移動する
- ヒープ割り当ては同じアドレスに残る
-
Pin::new_unchecked
- 安全なコンストラクタでは値がその場に残ることを証明できないとき、unsafeコードで
Pinを直接作成できる
let pinned = unsafe { Pin::new_unchecked(ptr) };
Pin::new_uncheckedの呼び出し側は、返されたPinの生存期間中、pointeeがどのポインタを通じても再び移動されないと約束する
- この約束が破られると、pinningの保証に依存するコードで未定義動作が発生する可能性がある
- したがって通常は、この不変条件を守れる低レベル抽象化を実装するときにだけ使われる
実際に気にする必要がある場合
- ほとんどのRust開発者にとって、
PinとUnpinは裏側で静かに動作する
- 直接気にする必要がある場合は、主に2つである
- asyncコードの利用: futureを直接
pollしたり、pinned futureを要求するAPIへ渡したりする必要がある場合は、Box::pin(future)でヒープにpinするか、std::pin::pin!(future)でローカルスタックにpinする
- Futureの直接実装: ユーザー定義ステートマシンや低レベルのasyncプリミティブを書くときは
Pinを扱う必要があり、pinningの不変条件を守るためにPhantomPinnedとunsafeコードが必要になる場合がある
Pinはアドレスに敏感な型の問題を扱うRustのzero-costな解法である
- これによりRustは、garbage collectorなしでメモリ安全性の保証を維持しながら、
async/awaitや他の自己参照抽象化を使用できる
1件のコメント
Lobste.rs の意見
std::pin::Pinは Rust 世界の Monad みたいなもの。いったん理解するとブログ記事を書かずにはいられなくなるPinを理解しようとするときに、私やほかの人がつまずいた点をいくつか扱うとよさそう。Unpinという名前はあまりよくない。より正確だがやはりいまひとつな名前としてはMovableWhenPinnedやPinIsNoOpがありえただろう。nightly の
!Unpinという二重否定は奇妙に見えるが、既存の型を 99% のデフォルトケースのままにするには、型が抜け出せる自動トレイトUnpinを追加する必要があったのでそうなっている。!MovableWhenPinnedだと思えばもう少し納得できる。stable 版の代替である
PhantomPinnedも名前はよくない。pinned 状態は pinned 参照があることで生じる一時的な状態であって、型の性質ではないからだ。代わりの名前はPhantomNotMovableWhenPinnedくらいだっただろう。こういうふうに頭の中で訳し始めたら、かなり理解しやすくなった。もちろん今でも混乱はするが、単に運がよかっただけかもしれない
!Unpinが頭痛の種だったが、UnpinをSafeToUnpinと読むようにしたら少し楽になった以前この質問をしたことがあり、誰かが丁寧に答えてくれた気がするのだが思い出せない。私の理解では
Pinは async から出てきたもので、ローカル変数への参照が、特定の関数の状態機械を表すデータの塊の中で 自己参照になる問題だった。async 状態が移動すると、そのローカル変数参照は以前の無効な場所を指すことになる。
でもそれは、参照が完全な絶対アドレスを持つ実際のポインタだからそうなるだけではないのか? なぜ解決策は参照を相対アドレスにすることではなく、移動能力を取り除くことだったのかが気になる。
答えがだいたい「コンパイラ、CPU、OS がポインタを非常にうまく扱えるようにするために何百万エンジニア年も費やされており、ポインタのほうが多くの面で優れているので、あちこちで
Pinを使うほうがよい」ということなのか、それとも相対参照が代替案として実際には成立しない堅い理由があるのか知りたい参照が相対的だとすると、それらの型は async 状態内で使われるかどうかによって メモリ表現を変えなければならず、相対参照から実際のポインタを復元するために一緒に渡す基準ポインタの概念も必要になる。
pinned 参照内の入れ子オブジェクトは、ルートオブジェクトが pinned でも依然として自由に移動できるので、仮想的な相対参照がすべて同じ基準ポインタに対する相対であるとも言えない。
結局は絶対ポインタが必要で、相対参照はあまり適していない。では Rust コンパイラはここにある型を全部知っているのだから、オブジェクトグラフ全体を追跡して、移動したオブジェクトを指す参照を新しい位置に修正することでオブジェクトを移動可能にしたらどうか? それは実質的に 追跡型ガベージコレクタを作ることになる。
しかも Rust コンパイラはオブジェクトグラフ内のすべての型を知っているわけではない。参照は FFI を通じて渡されることがあり、外部ライブラリがその参照を保持することもある。FFI 境界をまたぐ移動参照の修正は、事実上扱いにくい問題だ。
だから本当に難しい。オブジェクト移動そのものが比較的新しい手法だという点も重要だ。ほとんどの C/C++ プログラムでは、すべてのオブジェクトが暗黙に pinned されていると見なせる。あちらで pinning があまり議論されないのは、オブジェクトが単に移動しないか、移動してもぶら下がった参照が残らないようにする責任がプログラマにあるからだ
Pinは、Rust がメモリを不透明なビットの塊のように自由に移動できない他言語との 相互運用性にも必要になる。私の理解では C++ 相互運用の問題のひとつは、オブジェクトが自由に移動できる単なるビット列ではないことにあり、その結果かなり多くの型で pinning が必要になって使い勝手が悪くなる。
ただしこれは少なくとも 6 か月前くらいにその作業をしていた人たちと話した内容に基づいているので、その後どれだけ改善されたかはわからない
全体として、公式 Rust ドキュメントに加えて読むのに良い説明だと思う。問題への入り方が少し柔らかい。
ただし 自己参照構造体から始めるのは、むしろ省くより混乱を招くと思う。特に導入部の「したがって、そのような自己参照が作られたあとは
SelfRefの移動を防ぐ方法が必要になる」という文は、核心よりも「移動を完全に防ぐ問題」を連想させた。実際の核心は、かなり後になって出てくる「
Pinは値が物理的に移動しないようにするものではない。代わりに、そのポインタを通じて値が移動されないという型レベルの保証である」という点にある。移動そのものを防ぐことはできないので、安全な API で自己参照データを排他的参照の背後にしか公開しないために
Pinを使うのだ。私がすでにPinを理解しすぎているだけかもしれないが、説明の仕方を少し整えれば読者はもっと迷わずに済みそうだこれは pinning についての自分のノートから持ってきた文章で、最初は私もそう理解していた。「移動を防ぐ」のような問題を 型レベルの保証で解けるという点が美しいと感じていた。
もちろんそれは
Pinが実際にしていることではないので、その点が見えるように文章を直すべきだこの文章のどこかに、
!UnPinは nightly Rust でしか表現できないことを書いておいてもよさそう。それがPhantomPinnedが存在する主な理由だ「ポインタラッパー」と言うけれど、Rust でもポインタを扱う機会はほとんどない。なぜ使うのかよくわからない。
*constは Google で Rust の文書を探しにくいのだが、ちゃんと文書化されているのだろうか。「コンパイラが生成した状態機械のフィールドになる」というのも知っておくべきことなのか? それとも、ひどいコンパイラエラーが実際にそういうことが起きたと言おうとしているだけなのか?
「生成された future が自己参照になる」というのも、future を使うと暗黙に起こることなのか?
Future::pollは自分で書いたことがない気がする。「安全なコードは通常の
&mut Tを取り戻せない」と言いつつ「通常の変更は許される」とあるが、ではどうやるのか?こういうことのせいで Rust をさらに掘り下げるのをやめてしまった
ただ、低レベルに降りない限り使う機会がほとんどないのもその通り。私も C ライブラリを呼ぶ必要が出るまで知らなかった。
Future::pollは Rust 非同期コードの基礎だ。自分で直接呼ぶのではなく、executor が呼ぶ。Rust には標準 executor がないので、Tokio、smol、pollster のようなものを追加する必要があり、これらがFutureトレイトで定義されたpollのようなメソッドを使って処理を進める文書は ここ を含め、いろいろな場所にある。
他人が自分に必要だったことだけを説明すべきだと期待するのは少し求めすぎだ。
「で、どうやるの?」で何を聞いているのか、ちょっとわからない