1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • std::pin::Pinは、ポインタが指す値がそのポインタを通じて移動されないという型レベルの保証を表し、自分自身の内部を参照する型のように、アドレスが安定している必要がある値のために必要になる
  • async/awaitでは、.awaitをまたいで生き残るローカル変数や参照が、コンパイラが生成するステートマシンのフィールドになり得るため、ポーリング後にfutureが移動されるのを防ぐためにFuture::pollPinを要求する
  • 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ポインタラッパーであり、ポインタが指す値がそのポインタを通じて移動されないという保証を表す
  • 核心的な問題は自己参照型で発生する
    • 例の構造体SelfRefdata: i32ptr: *const i32を持ち、ptrself.dataを指す
    • 構造体インスタンスを別の変数へ移動したり、関数から返したりすると、メモリアドレスが変わる可能性がある
    • 生ポインタptrは以前のメモリ位置を指し続け、ダングリングポインタになる
  • 自己参照が設定された後は、その値が再び移動されないようにする仕組みが必要になる

async/awaitFutureで生じる問題

  • async/awaitFutureは、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メソッドを使う必要があり、その参照の外へ値が移動されないという約束をコードが守らなければならない

UnpinPhantomPinned

  • Unpinを実装する型は、メモリ安全性のためにpinningへ依存しない
// std::marker
pub auto trait Unpin {}
  • Rustのほとんどの型は、移動されても問題がないためデフォルトでUnpinである
    • 例: i32StringVec
  • 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

    • 最も単純な生成方法は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::pinBoxが所有するPinを返す
    • ヒープ割り当て自体は移動しないため、pointeeはBoxの生存期間中、安定したメモリ位置を持つ
    • Box自体を移動しても所有する値は移動せず、Box内のポインタだけが移動する
    • ヒープ割り当ては同じアドレスに残る
  • Pin::new_unchecked

    • 安全なコンストラクタでは値がその場に残ることを証明できないとき、unsafeコードでPinを直接作成できる
    let pinned = unsafe { Pin::new_unchecked(ptr) };
    
    • Pin::new_uncheckedの呼び出し側は、返されたPinの生存期間中、pointeeがどのポインタを通じても再び移動されないと約束する
    • この約束が破られると、pinningの保証に依存するコードで未定義動作が発生する可能性がある
    • したがって通常は、この不変条件を守れる低レベル抽象化を実装するときにだけ使われる

実際に気にする必要がある場合

  • ほとんどのRust開発者にとって、PinUnpinは裏側で静かに動作する
  • 直接気にする必要がある場合は、主に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件のコメント

 
GN⁺ 4 시간 전
Lobste.rs の意見
  • std::pin::Pin は Rust 世界の Monad みたいなもの。いったん理解するとブログ記事を書かずにはいられなくなる

    • そういう記事はたいてい monad tutorial fallacy に陥りがちだ
    • Monad のときと同じく、そうしたブログ記事は実際には何もきちんと説明できていない、ということか?
  • Pin を理解しようとするときに、私やほかの人がつまずいた点をいくつか扱うとよさそう。
    Unpin という名前はあまりよくない。より正確だがやはりいまひとつな名前としては MovableWhenPinnedPinIsNoOp がありえただろう。
    nightly の !Unpin という二重否定は奇妙に見えるが、既存の型を 99% のデフォルトケースのままにするには、型が抜け出せる自動トレイト Unpin を追加する必要があったのでそうなっている。!MovableWhenPinned だと思えばもう少し納得できる。
    stable 版の代替である PhantomPinned も名前はよくない。pinned 状態は pinned 参照があることで生じる一時的な状態であって、型の性質ではないからだ。代わりの名前は PhantomNotMovableWhenPinned くらいだっただろう。
    こういうふうに頭の中で訳し始めたら、かなり理解しやすくなった。もちろん今でも混乱はするが、単に運がよかっただけかもしれない

    • 完全に同意。以前は !Unpin が頭痛の種だったが、UnpinSafeToUnpin と読むようにしたら少し楽になった
  • 以前この質問をしたことがあり、誰かが丁寧に答えてくれた気がするのだが思い出せない。私の理解では Pin は async から出てきたもので、ローカル変数への参照が、特定の関数の状態機械を表すデータの塊の中で 自己参照になる問題だった。
    async 状態が移動すると、そのローカル変数参照は以前の無効な場所を指すことになる。
    でもそれは、参照が完全な絶対アドレスを持つ実際のポインタだからそうなるだけではないのか? なぜ解決策は参照を相対アドレスにすることではなく、移動能力を取り除くことだったのかが気になる。
    答えがだいたい「コンパイラ、CPU、OS がポインタを非常にうまく扱えるようにするために何百万エンジニア年も費やされており、ポインタのほうが多くの面で優れているので、あちこちで Pin を使うほうがよい」ということなのか、それとも相対参照が代替案として実際には成立しない堅い理由があるのか知りたい

    • async 状態内のローカル変数が、同じ状態内の別のローカル変数を直接参照することだけが問題なのではない。その場合ならコンパイラはすべてのローカル変数を把握しているので、アクセスを相対的にすることはできる。だが、ある型の深いところにある参照が別の型の深いところにある値を指す場合はずっと厄介になる。
      参照が相対的だとすると、それらの型は 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 をさらに掘り下げるのをやめてしまった

    • 生ポインタは Rust の プリミティブ型のひとつだ。文書は ここここ にある。
      ただ、低レベルに降りない限り使う機会がほとんどないのもその通り。私も C ライブラリを呼ぶ必要が出るまで知らなかった。
      Future::poll は Rust 非同期コードの基礎だ。自分で直接呼ぶのではなく、executor が呼ぶ。Rust には標準 executor がないので、Tokio、smol、pollster のようなものを追加する必要があり、これらが Future トレイトで定義された poll のようなメソッドを使って処理を進める
    • 原文の筆者ではないし、これらだけが理由でもないが、Rust でポインタを扱う必要があった理由は FFI と、グラフのような 自己参照データ構造だった。
      文書は ここ を含め、いろいろな場所にある。
      他人が自分に必要だったことだけを説明すべきだと期待するのは少し求めすぎだ。
      「で、どうやるの?」で何を聞いているのか、ちょっとわからない