質問の歴史: Drop Check (dropck) アルゴリズムは、早期の Rust バージョンにおいて、ジェネリックデストラクタがすでに解放されたデータにアクセスできるサウンドネスホールを閉じるために導入されました。dropck の前は、スタックに割り当てられたデータへの参照を持つ構造体を構築し、それをデレファレンスするために Drop を実装し、参照されたデータがコンテナよりも先に解放され、use-after-free につながるといった問題がありました。この問題は、借用されたデータを含む可能性のあるジェネリックコレクションにおいて重大なものとなり、デストラクタの安全性を確保するための保守的な分析が必要となりました。
問題:
ジェネリック型 Container<T> が Drop を実装するとき、コンパイラは、デストラクタが無効なメモリにアクセスするのを防ぐために T がコンテナよりも長く生存することを保証しなければなりません。生ポインタを使用する型(例:*const T)の場合、借用チェッカーによって生ポインタのライフタイム情報が追跡されないため、コンパイラはライフタイムマーカーを明示的に持たない限り、デストラクタが現在のスコープに所有されるデータへのポインタをデレファレンスする可能性を確認できません。
解決策:
PhantomData はサイズゼロのマーカーとして、型 T またはライフタイム 'a の所有権または借用をシミュレートします。生ポインタを保持する構造体に PhantomData<&'a T> を含めることで、コンパイラにその構造体がライフタイム 'a にバウンドした参照を論理的に保持していることを通知します。Drop Check アルゴリズムはこれを利用して、構造体が 'a よりも長く生存できないことを強制します。構造体が Drop を実装し、参照が存在する期間を超えて生存する可能性がある場合、コンパイルは失敗し、未定義の動作を防ぎます。
あなたは、バイトバッファをラップするゼロコピーのネットワークプロトコルパーサーを構築しています。ネットワークスタックから受け取った一時的な Vec<u8> への生ポインタ *const u8 を含む Packet<'a> を定義します。パース統計を更新するために生ポインタを通じて読み取るために Packet の Drop を実装しようとします。危険なのは、受信関数が終了すると Vec<u8> が解放されますが、Packet は後で処理するためにキューに保存される可能性があり、Drop が実行されるときに use-after-free が発生することです。
最初に、生ポインタの代わりに参照 &'a [u8] を使用することを考えます。これにより、バッファが十分に長く生存することが保証されます。しかし、これは API を大幅に制限します。なぜなら、パケットを自由に移動したり、'static バウンドが必要なコレクションに保存することができなくなり、パーサーで一般的な自己参照パターンを妨げるからです。
次に、バッファの所有権を共有するために Rc<Vec<u8>> を使用することを検討します。これにより、任意のパケットが存在する限りデータが有効であり続けます。ただし、参照カウントとヒープ割り当てのパフォーマンスコストが発生し、高スループットのネットワーク処理のゼロコピー、ゼロオーバーヘッドの要件に違反します。
第三に、生ポインタのパフォーマンスを維持しながらライフタイム依存をマークするために PhantomData<&'a ()> を追加することを検討します。しかし、これにより、コンパイラはバッファがパケットよりも長く生存する保証を提供できないため、Drop を実装することが根本的に安全でないことが明らかになります。あなたは Drop の実装を削除し、バッファが解放される前に呼び出される手動クリーンアップメソッドを使用することを選択するか、借用されたデータと所有されたデータの両方をサポートするために Cow<'a, [u8]> に切り替えることを選びます。
Cow<'a, [u8]> アプローチを選択し、生ポインタと危険な Drop ロジックの必要性を排除しました。その結果、ライフタイム保証が厳格で、パケットがその基となるバッファを超えて生存することはないことを保証するパーサーが無事にコンパイルされ、借用されたケースでのパフォーマンスを維持します。
なぜコンパイラは PhantomData<&'static T> を含む構造体の Drop を実装することを許可しますが、PhantomData<&'a T> では拒否するのですか、ここで 'a は非静的ですか?
ライフタイムが 'static の場合、参照されたデータはプログラムの実行全体にわたって生存するため、デストラクタが実行される前に解放される可能性はありません。'a がローカルライフタイムの場合、構造体が存在する間にデータが解放される可能性があるため、Drop 内でのダングリング参照アクセスが発生します。コンパイラは、デストラクタがデータが解放された後にアクセスしないことを証明できないため、ローカルライフタイムのケースを拒否しますが、'static はこの保証を本質的に提供します。
dropck の文脈で PhantomData<T> (所有セマンティクス) と PhantomData<&'a T> (借用セマンティクス) はどのように異なり、前者は構造体がそのスコープを超えるのを防がないのはなぜですか?
PhantomData<T> は、構造体が T を所有するかのように振る舞うことを示しており、構造体が T を解放する可能性があると仮定することで変異とドロップチェックに影響を与えますが、構造体のライフタイムを特定の借用ライフタイム 'a に結びつけるものではありません。したがって、コンパイラは構造体が任意のローカルデータよりも長く生存できると仮定しますが、T 自体がライフタイムを含まない限りは。対照的に、PhantomData<&'a T> は構造体をライフタイム 'a に明示的に制約し、借用よりも長く生存することはできず、したがってデストラクタにおける use-after-free を防ぎます。
may_dangle 属性 (不安定/非推奨) は dropck に関連してどのような目的があり、Vec<T> のような型にはどのように適用されましたか?
#[may_dangle] 属性は、型の Drop 実装がジェネリックパラメータ T の内容にアクセスしないことをコンパイラに告げるための安全でないコードを許可しました。たとえ T がコンテナを厳密に超えて生存していない場合でも。このことは、バッファを所有するが、ドロップ時に T 値を読み取る必要がない Vec<T> のようなコレクションにとって重要でした。候補者はしばしば、Drop Check がデフォルトで保守的であり、Drop がすべてにアクセスする可能性があると仮定していること、そして may_dangle がコレクションの柔軟性を高めるためにこの仮定からオプトアウトするためのメカニズムであったことを見逃しますが、それには危険なコードとダングリングデータへのアクセスを防ぐための厳格な不変条件が必要でした。