型システムにおける分散性は、ジェネリックパラメータ間のサブタイピング関係が全体の型にどのように影響を与えるかを決定します。Rustのアプローチは、リージョンベースのメモリ管理の研究と、use-after-free脆弱性を防ぐ必要性に大きく影響されました。Rustが可変参照(&mut T)を導入したとき、設計者はそれが共変(&Tのように)、逆変、または不変であるべきかを決定しなければなりませんでした。&mut Tの不変性の選択は、実行時チェックを必要とせずにメモリの安全性を維持するために重要でした。
もし**&mut TがTに対して共変だった場合、UがVのサブタイプであれば、&mut Uを期待される場所に置き換えることができてしまいます。ライフタイムの観点から、'longは'shortのサブタイプであるため('longが'shortよりも長く生きるため)、これは&mut &'long strを&mut &'short str**に割り当てることを意味します。これは無害に見えますが、健全性の穴を生じさせます。
&mut TはTに対して不変です。これは、&mut &'a strと**&mut &'b strが常に関連のない型であることを意味します、'aが'b**と正確に等しい場合を除き、ライフタイム間のサブタイピング関係にかかわらず。コンパイラは、それらの間で強制変換を試みるコードを拒否し、短命のデータを長命の参照を期待する場所に可変間接参照を通じて割り当てることを防ぎます。
コードの例:
fn demonstrate_invariance() { let mut long_lived: &'static str = "static string"; // これは&mut Tが共変だった場合にコンパイルされます: // let short_ref: &mut &'short str = &mut long_lived; // しかし、&mut Tが不変であるため、これは失敗します: // エラー:ライフタイムの不一致 // let short_ref: &mut &'_ str = &mut long_lived; let local = String::from("temporary"); // 上記が許可されていたら、次のようにできます: // *short_ref = &local; // これでlong_livedが解放されたデータを指すことになります(UAF!) } // localはここでドロップされる
あるチームが高性能ネットワークスタックの設定マネージャを構築していました。コア構造体は、所有権を持たずにランタイムでスワップ可能なプロトコル設定への可変参照を保持する必要がありました。
問題: 初期のAPI設計は、&mut &'a Configを使用しており、ここで**'aはネットワークセッションのライフタイムでした。開発者は、&mut &'static Config**(グローバルデフォルト設定用)でこれを初期化しようとし、その後**&mut &'session Configを期待する関数に渡しました。コンパイラはこれを拒否し、immutableな参照(& &'static Config**)は問題なく機能したため、混乱を引き起こしました。
考慮された解決策:
1. 変換の強制のためのUnsafe Transmute チームは、std::mem::transmuteを使用して**&mut &'static Configを&mut &'session Config**に変換することを検討しました。これはコンパイラの分散チェックをバイパスします。しかし、これにより、短命の設定参照が現在のスコープを超えて生き残る可能性のある場所に書き込まれることを許してしまい、設定がドロップされた後にアクセスされると即座に未定義の動作を引き起こすリスクがありました。製品コードでのuse-after-freeのリスクは受け入れられませんでした。
2. 不変参照への変更 彼らはAPIを**& &'a Configを使用するように変更することを考えました。共有参照は共変であるため、& &'static Configは& &'session Config**に変換可能です。しかし、これにより、ランタイム更新中に設定を原子的にスワップする能力が失われ、接続を再起動せずに設定をホットリロードするというコア要件が満たされなくなりました。
3. 内部可変性のためのCell<&'a Config>の使用 このオプションは、共有参照を介しての変更を可能にします。しかし、Cell<T>も同様の安全上の理由からTに対して不変であるため、分散性の問題を解決しませんでした。さらに、Cellはマルチスレッドアクセスのための同期を提供せず、RefCellによる実行時の借用チェックのオーバーヘッドはホットパスにとって高すぎると見なされました。
4. 所有型と間接参照を使った設計の見直し 選択された解決策は、参照への参照パターンを完全に排除しました。&mut &'a Configを保存するのではなく、構造体は**&'a mut ConfigHolder**を保存し、ConfigHolderは所有ラッパーでした。これにより、可変性が参照レベルではなく保持者レベルに移動し、分散性の罠を避けつつ、設定のスワップ能力を維持しました。APIはより使いやすくなり、ユーザーは二重参照を管理する必要がなくなりました。
結果: 再設計により、安全なAPIが生成され、unsafeコードなしでコンパイルされました。&mut Tの不変の特性が、ライフタイムの仮定が破られる可能性があるアーキテクチャ上の欠陥を認識することをチームに強いました。最終システムは、古い設定ポインタが有効期間を超えて持続するというバグのカテゴリを防止しました。
Cell<T>はなぜTに対して不変なのか、そしてこれは&mut Tの分散性にどのように関連していますか?
Cell<T>は内部可変性を提供し、共有参照を介しての変更を許します。もしCell<T>がTに対して共変だった場合、Cell<&'short str>をCell<&'static str>にアップキャストできてしまいます。そうすると、短命の文字列参照をそこに保存し、後でCell<&'static str>型を介して読み取ることになり、一時データを静的データとして扱うことになります。これはuse-after-freeの脆弱性になります。したがって、&mut Tと同様に、Cell<T>(およびUnsafeCell<T>)もTに対して不変でなければならず、短命のデータが長命のデータを保持すると主張するスロットに書き込まれることを防ぎます。この不変性は、RefCell、Mutex、その他の内部可変性型に伝播します。
PhantomData<T>は、実際のTを含まない構造体の分散性にどのように影響し、なぜPhantomData<fn(T)>を使って逆変性を実現するために使用しますか?
PhantomData<T>は、コンパイラに対してその構造体が分散性とドロップチェックの目的でTを所有しているかのように扱うように指示します。デフォルトでは、PhantomData<T>は構造体にTと同じ分散性を与えます。しかし、関数ポインタには特別な分散性があります:fn(A) -> BはA(引数)に対して逆変であり、B(戻り値)に対して共変です。構造体がライフタイムに対して逆変である必要がある場合(つまり、Struct<'long>はStruct<'short>のサブタイプであるときに'longが**'short**よりも長く生きる場合)、**PhantomData<fn(T)>**を使用します。これは、ライフタイム間の関係を逆転させなければならない型安全なコールバックや比較器を構築する際に重要です。
unsafeコードにおいて、生ポインタを使用して自己参照構造体を実装する際、なぜその構造体はそのライフタイムパラメータに対して不変である必要がありますか?
構造体が同じ構造体内の他のデータを指す生ポインタを含む場合(自己参照)、その構造体のライフタイムがポインタの有効性を決定します。その構造体がそのライフタイム**'aに対して共変であった場合、'aをより短いライフタイム'bに縮小することで、構造体が'bのみの期間に生きると主張することができます。しかし、内部の生ポインタは構造体がより長く生きていたときに作成され、それ以降の短いスコープではもはや有効ではないデータを指している可能性があります。不変性は、構造体が短いライフタイムに強制されないようにし、自己参照が型システムにエンコードされた全ライフタイムの間有効であることを保証します。これが、unsafe自己参照実装で明示的な分散マーカーとともにPin**がよく組み合わされる理由です。