Rust exige que todos los tipos utilizados como campos en estructuras o elementos en arreglos implementen el rasgo Sized, asegurando que el compilador pueda calcular desplazamientos de memoria fijos y disposiciones de marco de pila en tiempo de compilación. La construcción dyn Trait representa un objeto de rasgo despachado dinámicamente, que es inherentemente !Sized (no dimensionado) porque el tipo concreto detrás de la interfaz se borra, permitiendo que diversas implementaciones con diferentes huellas de memoria ocupen el mismo tipo abstracto. Para facilitar el dispatch dinámico, Rust representa dyn Trait como un puntero gordo—una estructura de dos palabras que contiene un puntero de datos al objeto y un puntero de vtable que sostiene direcciones de método e información de destructor—sin embargo, el tipo mismo permanece no dimensionado porque el tamaño del objeto apuntado es desconocido. En consecuencia, incrustar dyn Trait directamente en línea violaría la restricción Sized, ya que el compilador no puede determinar los límites de la estructura o el paso de arreglo; se requiere indirection a través de Box, Rc, Arc, o referencias & para envolver el puntero gordo dentro de un contenedor Sized.
Estás diseñando una arquitectura de plugins para un motor de juego donde los modders proporcionan diversas implementaciones de un rasgo Behavior—algunos almacenando simples banderas enteras, otros manteniendo grandes rejillas de hash espaciales—y el motor debe mantener una colección de comportamientos activos en la estructura GameState.
Intentar definir struct GameState { behaviors: Vec<dyn Behavior> } falla inmediatamente la compilación con el error de que dyn Behavior no tiene un tamaño constante conocido en tiempo de compilación, bloqueando la construcción.
Una solución considerada fue utilizar Vec<&dyn Behavior> para almacenar objetos de rasgo prestados, evitando la asignación en el heap para los punteros mismos. Este enfoque impone severas restricciones de duración, requiriendo que todos los datos del plugin vivan al menos tanto tiempo como el GameState y complicando escenarios de recarga en caliente donde los plugins se descargan dinámicamente, demostrando ser demasiado restrictivo para un motor modificable.
Otra alternativa evaluada fue el dispatch de enum, definiendo enum BehaviorType { Ai(AiModule), Physics(PhysicsBody) } para envolver todas las implementaciones conocidas. Mientras que esto proporciona un dispatch estático y una excelente localidad de caché, crea un conjunto cerrado que requiere modificaciones al motor base para cada nuevo plugin, violando el principio de abierto/cerrado y previniendo extensiones binarias de terceros sin recompilar el motor.
La solución seleccionada empleó Vec<Box<dyn Behavior>>, asignando en el heap cada instancia de comportamiento y almacenando los punteros gordos resultantes en el vector. Esto cumplió con el requisito Sized a través de la indirection de Box mientras preservaba el polimorfismo en tiempo de ejecución y permitía colecciones heterogéneas, aunque introdujo costos de fragmentación del heap predecibles que se mitigaron mediante un asignador de arena personalizado para pequeños componentes de comportamiento.
¿Cómo facilita CoerceUnsized la conversión de Box<T> a Box<dyn Trait> sin asignar una nueva vtable en tiempo de ejecución, y qué restricciones de diseño de memoria impone esto sobre el objeto apuntado?
CoerceUnsized es un rasgo marcador implementado por punteros inteligentes como Box, Rc, y Arc que permite coerciones no dimensionadas. Al convertir Box<Concrete> a Box<dyn Trait>, el compilador genera la vtable para Concrete implementando Trait estáticamente durante la compilación, incrustándola en la sección de solo lectura del binario. La coerción simplemente reinterpreta los metadatos del puntero, ampliándolo de un puntero delgado (una sola palabra) a un puntero gordo (dirección de datos + dirección de vtable) sin reubicar los datos subyacentes ni asignar memoria en tiempo de ejecución. Esto impone la estricta restricción de que el tipo concreto debe poseer un diseño de memoria compatible con la representación esperada del objeto de rasgo—específicamente, el puntero de datos debe alinearse con el inicio del objeto donde la vtable espera campos, y el tipo debe adherirse a #[repr(Rust)] o garantías de representación compatibles, asegurando que los desplazamientos de método en la vtable se resuelvan correctamente a las funciones de la implementación concreta.
¿Por qué Rust prohíbe crear objetos de rasgo (dyn Trait) a partir de rasgos que definen métodos que consumen Self por valor (fn consume(self)), y cómo se relaciona esto con el requisito Sized para tipos de retorno de funciones?
Esta prohibición deriva de las reglas de seguridad de objeto. Cuando un método consume self por valor, el compilador debe conocer el tamaño exacto del tipo concreto para generar el marco de pila adecuado para mover el valor e insertar la llamada de destructor correcta en el desplazamiento de memoria preciso. En un contexto dyn Trait, el tipo concreto se borra; mientras que la vtable contiene información de tamaño y destrucción, el marco de pila del llamador no puede ajustarse dinámicamente para acomodar el tamaño desconocido del valor movido. Además, los métodos que retornan Self requerirían que el llamador asigne espacio para el slot de retorno de tamaño desconocido. Para prevenir la corrupción de la pila y el comportamiento indefinido, Rust prohíbe objetos de rasgo para rasgos con métodos por valor self, asegurando que todas las interacciones ocurran a través de indirection (&self o &mut self) donde el tamaño del puntero es constante.
¿Cuál es la distinción entre dyn Trait que implementa automáticamente Send cuando Trait lleva Send como supertrait versus la anotación explícita de dyn Trait + Send, y por qué la ausencia de ambos lleva a que el objeto de rasgo falle en las comprobaciones de seguridad de hilo a pesar de que el tipo concreto subyacente implemente Send?
Cuando Trait declara Send como supertrait (por ejemplo, trait Trait: Send {}), el compilador propaga este límite, implementando automáticamente Send para dyn Trait porque cualquier implementador debe ser necesariamente Send. Por el contrario, si Trait carece de este supertrait, escribir dyn Trait + Send construye explícitamente un objeto de rasgo que solo acepta tipos concretos que implementen tanto Trait como Send, restringiendo los tipos permitidos en el sitio de coerción. Si no existe ni supertrait ni límite explícito, dyn Trait no implementa Send incluso si la instancia concreta detrás del puntero es segura para hilos, porque la eliminación de tipo descarta esta información—el compilador no puede garantizar que todos los posibles tipos que podrían ocupar ese slot de vtable son Send. Esto previene la transmisión accidental de tipos no seguros para hilos a través de límites de hilo mediante la eliminación de tipos de objetos de rasgo.