Pinの概念は、メモリの安全性を犠牲にすることなく非同期プログラミングをサポートするRustのニーズから生まれました。歴史的に、**C++**のようなシステム言語は自己参照構造体を許可しましたが、オブジェクトがメモリ内で再配置された際に使用後移動バグが発生しました。核心的な問題は、構造体が自身のフィールドへのポインタを含んでいるときに発生します。構造体がビット単位で新しいアドレスにコピーされると、内部ポインタは解放されたスタック領域へのダングリング参照になります。Pinは、ポインタ型(Box、Rc、参照)をラップし、基礎となる値が再配置されることは決してないことを保証することでこれを解決します。これは、自己参照構造体が安定したアドレスに依存できる契約を作り出し、async/await状態マシンがサスペンションポイントをまたいで参照を保持できるようにします。
私たちは、毎秒何百万ものパケットを処理するasync Rustサービスにおいてゼロコピーのネットワークプロトコルパーサーを実装する必要がありました。Parser構造体は、Vec<u8>バッファと、そのバッファを参照するバイトスライスを含む解析されたHeader構造体を保持していました。async関数がawaitポイントで制御を譲渡すると、エグゼキュータは未来をワーカースレッド間で移動させることができ、その結果スライスポインタが無効になり、再開時に即座に未定義動作を引き起こしました。
考慮された1つのアプローチは、スライスの代わりにバイトインデックスを使用し、バッファ内のusizeオフセットを保存することでした。このアプローチは、整数がトリビアルにコピー可能で再配置可能であるため、Pinの複雑さなしで完全な安全性を提供しました。しかし、これにより常に境界チェックとポインタ算術が必要になり、私たちの厳密なパースループのパフォーマンスが約15%低下しました。
別の代替案は、Box::pinを使用してバッファを別々にヒープに割り当て、パーサー内に生ポインタ(*const u8)を保存することでした。この方法はポインタの無効化を防ぎましたが、ポインタのデリファレンスのためにunsafeコードブロックを導入しました。また、手動のメモリ管理が必要になり、バグの発生面積が増え、Rustコンパイラが私たちのライフタイム保証が正当であることを確認できなくなりました。
私たちはPinアプローチを選択し、pin_project_liteを使用して全体のParser未来をピン留めし、内部フィールドへのピンを安全に投影しました。この解決策は、ヒープ割り当てのオーバーヘッドなしでゼロコストのスライス参照を維持し、構造体がasync実行中に移動しないことを保証しました。サービスは、ポインタ追跡によるクラッシュや測定可能な遅延なしにawait境界をまたいで直接メモリ参照を介してパケットを処理します。
Unpinを実装する型が、Pinにラップされていても移動できるのはなぜですか?
UnpinはRustにおける自動トレイトであり、ピン留めの意味についての否定的マーカーとして働きます。型がUnpinを実装するとき、それは明示的に安定したメモリアドレスに依存しないことを宣言し、Pinは基礎となる値の安全な抽出を許可します。開発者はしばしば、Pinが絶対的な不動性の保証を提供すると誤解します。しかし、Pin<Ptr<T>は、T: !Unpinの場合にのみ移動を制限します。なぜなら、Unpin型はPin::into_innerを使用して抽出したり、アンピン後に安全に移動したりできるからです。この区別は、使い捨ての要件を実際に施行するためにPhantomDataあるいは明示的な境界で型を制約しなければならない汎用のasyncコードを書く際に重要です。
Dropトレイトは、ピン留めされたリソースとどのように相互作用し、安全要件は何ですか?
ピン留めされた値が破棄されるとき、Dropが呼び出され、値はそのピン留めされたメモリ位置のままであるため、自己参照ポインタは破棄中も有効です。安定したRustでは、ピン留めされた構造体のためのカスタムDrop実装を書くことは、pin_utilsやpin-projectのようなクレートを使用して慎重に投影する必要があります。なぜなら、Drop::drop(&mut self)内のselfは、値がピン留めされていてもアンピンされた参照を受け取るからです。これにより、デストラクタがPin保証の下で維持されていた自己参照フィールドにアクセスしようとすると安全上の危険が生じ、デストラクタが暗黙的にデータを移動すると使用後解放が発生する可能性があります。候補者は、ピン留めされた値を削除するには、Unpinを実装する(ピン留め保証を放棄する)か、破棄中にピン留めされたフィールドにアクセスするためにunsafe投影を使用する必要があることを理解しなければなりません。
**Pin<Box<T>>をスタック上の値をピン留めすることと区別し、ヒープピン留めが必要な場合は何ですか?
Pin<Box<T>>は、値をヒープ上に割り当て、それをピン留めし、オブジェクトのプログラム全体のライフタイムのための安定したアドレスを提供します。これは、現在のスタックフレームよりも長く生存しなければならない自己参照構造体にとって重要です。pin_utils::pin_mut!やpin-projectクレートを使用したスタックピン留めは、一時的なPinを作成し、スタックフレームが戻ると期限が切れます。これは、1つの関数スコープ内にとどまるasyncブロックに適しています。候補者はしばしばこれらのアプローチを混同し、スタックピン留めされた値を関数から返そうとしたり、すべてのPin操作にBoxが必要であると仮定したりします。Pinがポインタの動作に関する契約であり、ストレージの持続時間ではないことを理解することは、asyncタスクのスパーニングやFutureの構成におけるライフタイムエラーを防ぎます。