Rustは、構造体のフィールドや配列の要素として使用されるすべての型がSizedトレイトを実装することを要求し、コンパイラが固定メモリオフセットおよびスタックフレームレイアウトをコンパイル時に計算できるようにしています。dyn Trait構造は、動的にディスパッチされるトレイトオブジェクトを表し、インターフェースに対する具体的な型が隠蔽されているため、固有のサイズを持たない**!Sized**(サイズなし)です。このため、メモリフットプリントが異なる多様な実装が同じ抽象型を占めることが可能です。動的ディスパッチを促進するために、Rustはdyn Traitをファットポインタとして表現します。これは、データポインタとメソッドアドレスおよびデストラクタ情報を保持するvtableポインタを含む二語構造です。しかし、ポインタが指すオブジェクトのサイズは不明なため、型自体は依然としてサイズがないままです。したがって、dyn Traitを直接インラインに埋め込むと、コンパイラが構造体の境界や配列のストライドを決定できないため、Sized制約に違反します。そのため、ファットポインタをSizedコンテナ内にラップするために、Box、Rc、Arcなどの間接参照または参照**&**を通じてラップする必要があります。
ゲームエンジンのプラグインアーキテクチャを設計していると想像してください。そこでモッダーはBehaviorトレイトの多様な実装を提供します。一部は単純な整数フラグを保存し、他は大規模な空間ハッシュグリッドを維持します。そして、エンジンはGameState構造体内のアクティブなビヘイビアのコレクションを維持する必要があります。struct GameState { behaviors: Vec<dyn Behavior> }を定義しようとすると、dyn Behaviorがコンパイル時に知られている定数サイズを持たないため、コンパイルが失敗します。
考慮された解決策の1つは、借用されたトレイトオブジェクトを保存するためにVec<&dyn Behavior>を利用することであり、ポインタ自体のヒープ割り当てを回避します。このアプローチは厳しいライフタイム制約を課し、すべてのプラグインデータがGameStateと同じかそれ以上の期間存在する必要があり、プラグインが動的にアンロードされるホットリロードシナリオを複雑にします。結局、これはモダブルエンジンには制限が厳しすぎることが証明されました。
また別の代替案として、すべての既知の実装をラップするためにenum BehaviorType { Ai(AiModule), Physics(PhysicsBody) }を定義する列挙型ディスパッチが評価されました。このアプローチは静的ディスパッチと優れたキャッシュ局所性を提供しますが、新しいプラグインごとにコアエンジンの修正が必要となる閉じたセットを作成し、オープン/クローズドの原則に違反し、エンジンを再コンパイルせずにサードパーティのバイナリ拡張を防ぎます。
選択された解決策は、各ビヘイビアインスタンスをヒープに割り当て、結果のファットポインタをベクタに保存するVec<Box<dyn Behavior>>を使用しました。これは、Box間接参照を通じてSized要件を満たしながら、ランタイムポリモーフィズムを保存し、異種コレクションを許可しましたが、小さなビヘイビアコンポーネントのためのカスタムアリーナアロケータによって軽減された予測可能なヒープ断片化コストを導入しました。
CoerceUnsizedは、Box<T>からBox<dyn Trait>への変換をどのようにして新しいvtableをランタイムで割り当てずに行うことができ、ポインティーにどのようなメモリレイアウトの制約を課すのか?
CoerceUnsizedは、Box、Rc、Arcなどのスマートポインタによって実装されたマーカートレイトで、サイズのないコーハージョンを許可します。Box<Concrete>からBox<dyn Trait>に変換すると、コンパイラはコンパイル時にConcreteがTraitを実装するためのvtableを生成し、それをバイナリの読み取り専用セクションに埋め込みます。コーハージョンは単にポインタメタデータを再解釈するだけで、薄いポインタ(単一の単語)からファットポインタ(データアドレス + vtableアドレス)に広げるものであり、基盤となるデータの移動やランタイムでのメモリ割り当ては行われません。これにより、具体的な型がトレイトオブジェクトの期待される表現と互換性のあるメモリレイアウトを持つ必要があるという厳しい制約が課されます。具体的には、データポインタはvtableがフィールドを期待するオブジェクトの開始位置にアラインする必要があり、型は**#[repr(Rust)]**または互換性のある表現保証に従う必要があります。これにより、vtableのメソッドオフセットが具体的な実装の関数に正しく解決されることが保証されます。
Rustが自己を値で消費するメソッド(fn consume(self))を持つトレイトからトレイトオブジェクト(dyn Trait)を作成することを禁止する理由は何ですか?これは関数の戻り値の型に対するSized要件とどのように関連していますか?
この禁止は、オブジェクト安全性のルールから派生します。メソッドがselfを値として消費する場合、コンパイラは値を移動するための適切なスタックフレームを生成し、正しいデストラクタ呼び出しを正確なメモリアドレスに挿入するために、具体的な型の正確なサイズを知る必要があります。dyn Traitの文脈では、具体的な型が隠蔽されています。vtableにはサイズやドロップ情報が含まれていますが、呼び出し側のスタックフレームは、移動された値の不明なサイズに合わせて動的に調整することができません。さらに、Selfを返すメソッドは、呼び出し側が不明なサイズの戻り値スロットスペースを割り当てる必要があるため、スタックの破損や未定義の動作を防ぐために、Rustは値でのselfメソッドを持つトレイトに対するトレイトオブジェクトを禁止し、すべてのやり取りが間接参照(&selfまたは**&mut self**)を通じて行われることを保証します。この場合、ポインタサイズは一定です。
トレイトがSendをスーパーtraitsとして持つ場合に、dyn Traitが自動的にSendを実装することと、明示的にdyn Trait + Sendを注釈付けすることとの違いは何ですか?なぜそのどちらも欠如している場合、トレイトオブジェクトはSendを実装していないとみなされ、ポインタの背後にある具体的な型がSendを実装していてもスレッドセーフチェックに失敗するのですか?
TraitがSendをスーパーtraitとして宣言している場合(例:trait Trait: Send {})、コンパイラはこの制約を伝播し、あらゆる実装者が必然的にSendであるため、dyn Traitに自動的にSendを実装します。逆に、Traitがこのスーパーtraitを持たない場合、dyn Trait + Sendを明示的に書くと、両方のトレイト(TraitとSend)を実装する具体的な型のみを受け入れるトレイトオブジェクトが構築され、コーハージョンサイトで許可される型が絞られます。いずれのスーパーtraitも明示的な制約も存在しない場合、ポインタの背後にある具体的なインスタンスがスレッドセーフであっても、dyn TraitはSendを実装していないと見なされます。これは型の隠蔽によりこの情報が失われるためです。コンパイラは、そのvtableスロットに占める可能性のあるすべての型がSendであることを保証できません。これにより、トレイトオブジェクト型の隠蔽を通じて、スレッド境界を越えた非スレッドセーフ型の偶発的な伝送を防ぎます。