Rust exige que tous les types utilisés comme champs dans des structures ou éléments dans des tableaux implémentent le trait Sized, garantissant que le compilateur puisse calculer des décalages mémoire fixes et des agencements de trame de pile à la compilation. La construction dyn Trait représente un objet de trait dispatché dynamiquement, qui est intrinsèquement !Sized (non dimensionné) car le type concret derrière l'interface est effacé, permettant à diverses implémentations avec des empreintes mémoire variées d'occuper le même type abstrait. Pour faciliter le dispatch dynamique, Rust représente dyn Trait comme un fat pointer—une structure à deux mots contenant un pointeur de données vers l'objet et un pointeur vtable contenant des adresses de méthode et des informations sur le destructeur—pourtant, le type lui-même reste non dimensionné car la taille du pointeur est inconnue. Par conséquent, l'intégration de dyn Trait directement en ligne violerait la contrainte Sized, car le compilateur ne peut pas déterminer les limites de la structure ou le pas du tableau ; une indirection par le biais de Box, Rc, Arc ou références & est nécessaire pour envelopper le fat pointer dans un conteneur Sized.
Vous concevez une architecture de plugin pour un moteur de jeu où les moddeurs fournissent diverses implémentations d'un trait Behavior—certains stockant des indicateurs d'entier simples, d'autres maintenant de grands grilles de hachage spatiales—et le moteur doit maintenir une collection de comportements actifs dans la structure GameState.
Essayer de définir struct GameState { behaviors: Vec<dyn Behavior> } échoue immédiatement la compilation avec l'erreur que dyn Behavior n'a pas de taille constante connue à la compilation, bloquant la construction.
Une solution envisagée était d'utiliser Vec<&dyn Behavior> pour stocker des objets de trait empruntés, évitant l'allocation sur le tas pour les pointeurs eux-mêmes. Cette approche impose de sévères contraintes de durée de vie, exigeant que toutes les données de plugin vivent au moins aussi longtemps que le GameState et compliquant les scénarios de rechargement à chaud où les plugins sont déchargés dynamiquement, s'avérant finalement trop restrictif pour un moteur modifiable.
Une autre alternative évaluée était le dispatch par énumération, définissant enum BehaviorType { Ai(AiModule), Physics(PhysicsBody) } pour envelopper toutes les implémentations connues. Bien que cela fournisse un dispatch statique et une excellente localité de cache, cela crée un ensemble fermé nécessitant des modifications du moteur de base pour chaque nouveau plugin, violant le principe ouvert/fermé et empêchant les extensions binaires tierces sans recompilation du moteur.
La solution sélectionnée a utilisé Vec<Box<dyn Behavior>>, allouant dynamiquement chaque instance de comportement sur le tas et stockant les fat pointers résultants dans le vecteur. Cela a satisfait l'exigence Sized par indirection Box tout en préservant le polymorphisme d'exécution et permettant des collections hétérogènes, bien que cela ait introduit des coûts de fragmentation de tas prévisibles qui ont été atténués par un allocateur d'arène personnalisé pour de petits composants comportementaux.
Comment CoerceUnsized facilite-t-il la conversion de Box<T> en Box<dyn Trait> sans allouer une nouvelle vtable à l'exécution, et quelles contraintes d'agencement mémoire cela impose-t-il au pointeur?
CoerceUnsized est un trait marqueur implémenté par des pointeurs intelligents comme Box, Rc, et Arc qui permet des coercitions non dimensionnées. Lors de la conversion de Box<Concrete> en Box<dyn Trait>, le compilateur génère la vtable pour Concrete implémentant Trait statiquement durant la compilation, l'incorporant dans la section en lecture seule du binaire. La coercition réinterprète simplement les métadonnées du pointeur, élargissant de manière à passer d'un pointeur mince (un mot) à un fat pointer (adresse de données + adresse de vtable) sans déplacer les données sous-jacentes ni allouer de mémoire à l'exécution. Cela impose la contrainte stricte que le type concret doit posséder un agencement mémoire compatible avec la représentation attendue de l'objet de trait—spécifiquement, le pointeur de données doit s'aligner avec le début de l'objet où la vtable attend des champs, et le type doit respecter #[repr(Rust)] ou des garanties de représentation compatibles, garantissant que les décalages de méthode dans la vtable se résolvent correctement sur les fonctions de l'implémentation concrète.
Pourquoi Rust interdit-il la création d'objets de trait (dyn Trait) à partir de traits qui définissent des méthodes consommant Self par valeur (fn consume(self)), et comment cela est-il lié à l'exigence Sized pour les types de retour de fonction?
Cette interdiction découle des règles de sécurité des objets. Lorsque la méthode consomme self par valeur, le compilateur doit connaître la taille exacte du type concret pour générer la trame de pile appropriée pour déplacer la valeur et insérer l'appel de destructeur correct à l'offset mémoire précis. Dans un contexte dyn Trait, le type concret est effacé ; bien que la vtable contienne des informations sur la taille et le drop, la trame de pile de l'appelant ne peut pas être ajustée dynamiquement pour accueillir la taille inconnue de la valeur déplacée. De plus, les méthodes retournant Self exigeraient que l'appelant alloue de l'espace pour un slot de retour de taille inconnue. Pour éviter la corruption de la pile et un comportement indéfini, Rust interdit les objets de trait pour les traits ayant des méthodes en self par valeur, garantissant que toutes les interactions se produisent par indirection (&self ou &mut self) où la taille du pointeur est constante.
Quelle est la distinction entre dyn Trait implémentant automatiquement Send lorsque Trait porte Send en tant que supertrait par rapport à annoter explicitement dyn Trait + Send, et pourquoi l'absence des deux conduit-elle à un échec des vérifications de sécurité des threads malgré le fait que le type concret sous-jacent implémente Send?
Lorsque Trait déclare Send comme un supertrait (par exemple, trait Trait: Send {}), le compilateur propage cette contrainte, implémentant automatiquement Send pour dyn Trait car tout implémenteur doit nécessairement être Send. Inversement, si Trait ne possède pas ce supertrait, écrire dyn Trait + Send construit explicitement un objet de trait qui n'accepte que les types concrets implémentant à la fois Trait et Send, réduisant les types autorisés au site de coercion. Si ni le supertrait ni la contrainte explicite n'existent, dyn Trait n'implémente pas Send même si l'instance concrète derrière le pointeur est sûre pour les threads, car l'effacement de type supprime cette information—le compilateur ne peut pas garantir que tous les types possibles qui pourraient occuper cet emplacement de vtable sont Send. Cela empêche l'envoi accidentel de types non sûrs pour les threads à travers des frontières de thread en raison de l'effacement de type de l'objet de trait.