RustProgrammingRust デベロッパー

手動で実装された **Future** 内で **Pin**<&mut Self> のフィールドプロジェクションをどのように実装し、なぜ単純な可変借用が **Pin** コンプライアンスに違反するのか?

Hintsage AIアシスタントで面接を突破

質問への回答。

質問の歴史

Rust 1.39での async/await の安定化と、1.33バージョンで導入された Pin 型により、非同期状態機械に不可欠な自己参照構造体の安全な使用が可能になりました。これらの構造体は、しばしば構造体自体が所有するデータへの内部ポインタ(バッファやそのバッファへのアクティブビューなど)を含みます。手動のfutureや複雑な侵入型データ構造を実装する際、開発者は Pin<&mut Self> を介して個々のフィールドにアクセスする必要があり、メモリ位置の保証を維持するために安全なプロジェクションメカニズムが必要です。

問題

構造体が Pin によってピン留めされると、コンパイラはそのメモリアドレスがピンのライフタイム中に一定であることを保証します。ただし、その型が Unpin を実装しない場合に限ります。構造体が自己参照ポインタ(内部ベクタへの生ポインタなど)を保持している場合、構造体を移動させるとこれらのポインタが無効になり、ダングリング参照を引き起こします。単純に Pin<&mut Self>&mut Self にデリファレンスするという単純なプロジェクションアプローチは、フィールドを安全な Rust コードにさらし、そのフィールドに対して合法的に mem::swapmem::replace を呼び出すことができ、その結果、ピン留めされたメモリ位置からフィールドが移動され、基本的なピン留め契約が違反される可能性があります。

解決策

安全なプロジェクションには、ピン留め不変量を維持するための安全でない変換が必要です:親構造体が !Unpin の場合、フィールドプロジェクションは &mut Field ではなく Pin<&mut Field> を返すべきです。これは移動を防ぐためです。実装は、フィールドが構造的にピン留めされていることを保証しなければなりません。これは通常、ポインタ算術または Pin::map_unchecked_mut を介して実現されます。Unpin を実装したフィールドについては、これらの型はピン留めされたデータ内にネストされていても移動が許可されるため、プロジェクションは安全に &mut Field を返すことができますが、そのような移動が他の自己参照フィールドを無効にしないように注意が必要です。

use std::pin::Pin; use std::marker::PhantomPinned; struct Buffer { data: [u8; 1024], cursor: *const u8, _pin: PhantomPinned, } impl Buffer { // データフィールドへの安全なプロジェクション (Unpin) fn data_mut(self: Pin<&mut Self>) -> &mut [u8; 1024] { unsafe { &mut self.get_unchecked_mut().data } } // カーソルフィールドへのプロジェクション fn cursor(self: Pin<&mut Self>) -> *const u8 { unsafe { self.get_unchecked_mut().cursor } } }

実生活からの状況

コンテキスト

私たちは、メッセージが再利用可能な内部バッファのサブレンジを参照できる金融プロトコルの高性能、ゼロコピーのパーサを構築していました。パーサの状態は非同期I/O操作全体で維持する必要があり、構造体はバッファ内への自己参照ポインタを許可するために Pin される必要がありました。

問題の説明

Parser 構造体は Vec<u8> バッファと、そのバッファを参照する &[u8] スライスを保持しており、現在のメッセージを表しています。このパーサの Stream を実装する際、poll_next メソッドは Pin<&mut Self> を受け取ります。バッファを変更し(より多くのデータを読み込むため)、スライス参照の有効性を維持する必要があり、慎重なフィールドプロジェクションが必要です。

検討された解決策

解決策A: インデックスベースのアドレス指定 スライス &[u8] を保存する代わりに、ベクタ内の (usize, usize) インデックスを保存しました。利点: 完全に安全で、Pin の複雑さがなく、実装が簡単。欠点: 実行時の境界チェックオーバーヘッド、すべてのアクセスで手動スライシングが必要で、インデックスの非同期バグが発生する可能性があります。

解決策B: 生ポインタを使用したunsafe Pinプロジェクション メッセージを生ポインタ *const u8 と長さとして保存し、バッファにアクセスしながらポインタフィールドをピン留めするために Pin::map_unchecked_mut を使用して手動プロジェクションメソッドを実装しました。利点: ゼロコストの抽象化、自己参照性を維持、直接ポインタ算術が許可される。欠点: unsafe コードブロックが必要で、Pin 不変量が違反されると未定義の動作のリスクがあります(例: Unpin を不適切に実装すること)。

解決策C: pin-project クレートを使用 安全なプロジェクションコードを自動的に生成するために手続き型マクロを活用します。利点: エルゴノミックで、テストされた安全な不変量、ボイラープレートを減らします。欠点: 追加依存関係、マクロ生成コードのデバッグが難しい場合があり、わずかなコンパイル時間のコストがあります。

選択された解決策と結果

私たちは、組み込みシステムの文脈で外部依存関係を避け、メモリレイアウトを明示的に制御するために 解決策B を選択しました。構造体が Unpin を実装していないことを確認するために PhantomPinned を追加し、ピン留め不変量を検証するために徹底的な Miri テストを作成しました。その結果、メッセージごとに割り当てを行わず、10Gbps のスループットを維持しながらゼロコピーのセマンティクスを持つパーサが誕生しました。

候補者が見落とすことが多いポイント

自己参照ポインタを含む構造体に対して Unpin を実装することがなぜ無効であるのか?

Unpin は、タイプが Pin にラップされていても安全に移動できることを特に示します。これにより、安全なコードが Pin<&mut T> から &mut T を取得することができます。自己参照構造体の移動は、内容のメモリアドレスを変更し、これらの内容を参照する内部ポインタを無効にします。Unpin を実装すると、安全なコードがピン留めされた状態で構造体を移動できるようになり、Pin が提供する非同期ランタイムへの安全性保証を違反し、使い捨てになる可能性があります。したがって、そのような構造体は PhantomPinned を使用して明示的に Unpin から除外し、偶発的な自動実装を防ぐ必要があります。

列挙体のバリアントのプロジェクションは、構造体フィールドのプロジェクションとどのように異なるのか?

多くの候補者は、プロジェクションメカニズムが列挙体と構造体で同じであると考えますが、列挙体は識別子によってどのバリアントがアクティブかが決まるため、ユニークな課題をもたらします。 Pin<&mut Enum> を特定のバリアントにプロジェクトするには、バリアントがピン留めされたままであることを確認し、識別子が変わらないようにする必要があります。バリアントを切り替えると、基底データが移動してしまうためです。Rust には、安定したビルトインサポートがないため、安全なプロジェクションには、安全でないコードが現在のバリアントを確認し、列挙体がピン留めされている間にバリアントの交換が行われないことを保証する必要があります。

PhantomPinned は、なぜ自動トレイト実装を防ぐ役割を果たすのか?

初心者はしばしば、Rust は明示的に !Unpin フィールドを含んでいない限り、ほとんどの型に対して Unpin を自動的に実装することを見落とします。これは、含まれる型がデフォルトで !Unpin になるからです。PhantomPinned は、構造体に含まれることで !Unpin として明示的に定義されたサイズのないマーカー型として機能します。このマーカーがなければ、開発者が構造体が移動しないことを前提に安全でないプロジェクションコードを書いても、コンパイラは Unpin を自動的に実装してしまい、安全なコードが Pin::into_inner_unchecked を通じて構造体を抽出・移動できるようになり、不安全な不変量を破り、未定義の動作を引き起こす可能性があります。