Rust mandates that all types used as fields in structs or elements in arrays implement the Sized trait, ensuring the compiler can calculate fixed memory offsets and stack frame layouts at compile time. The dyn Trait construct represents a dynamically dispatched trait object, which is inherently !Sized (unsized) because the concrete type behind the interface is erased, allowing diverse implementations with varying memory footprints to occupy the same abstract type. To facilitate dynamic dispatch, Rust represents dyn Trait as a fat pointer—a two-word structure containing a data pointer to the object and a vtable pointer holding method addresses and destructor information—yet the type itself remains unsized because the pointee’s size is unknown. Consequently, embedding dyn Trait directly inline would violate the Sized bound, as the compiler cannot determine struct boundaries or array stride; indirection through Box, Rc, Arc, or references & is required to wrap the fat pointer within a Sized container.
You are designing a plugin architecture for a game engine where modders provide diverse implementations of a Behavior trait—some storing simple integer flags, others maintaining large spatial hash grids—and the engine must maintain a collection of active behaviors in the GameState struct.
Attempting to define struct GameState { behaviors: Vec<dyn Behavior> } immediately fails compilation with the error that dyn Behavior does not have a constant size known at compile time, blocking the build.
One considered solution was utilizing Vec<&dyn Behavior> to store borrowed trait objects, avoiding heap allocation for the pointers themselves. This approach imposes severe lifetime constraints, requiring all plugin data to live at least as long as the GameState and complicating hot-reloading scenarios where plugins are dynamically unloaded, ultimately proving too restrictive for a moddable engine.
Another alternative evaluated was enum dispatch, defining enum BehaviorType { Ai(AiModule), Physics(PhysicsBody) } to wrap all known implementations. While this provides static dispatch and excellent cache locality, it creates a closed set requiring core engine modifications for every new plugin, violating the open/closed principle and preventing third-party binary extensions without recompiling the engine.
The selected solution employed Vec<Box<dyn Behavior>>, heap-allocating each behavior instance and storing the resulting fat pointers in the vector. This satisfied the Sized requirement through Box indirection while preserving runtime polymorphism and allowing heterogeneous collections, though it introduced predictable heap fragmentation costs that were mitigated by a custom arena allocator for small behavioral components.
How does CoerceUnsized facilitate the conversion from Box<T> to Box<dyn Trait> without allocating a new vtable at runtime, and what memory layout constraints does this impose on the pointee?
CoerceUnsized is a marker trait implemented by smart pointers like Box, Rc, and Arc that permits unsized coercions. When converting Box<Concrete> to Box<dyn Trait>, the compiler generates the vtable for Concrete implementing Trait statically during compilation, embedding it in the binary’s read-only section. The coercion merely reinterprets the pointer metadata, widening it from a thin pointer (single word) to a fat pointer (data address + vtable address) without relocating the underlying data or allocating memory at runtime. This imposes the strict constraint that the concrete type must possess a compatible memory layout with the trait object’s expected representation—specifically, the data pointer must align with the start of the object where the vtable expects fields, and the type must adhere to #[repr(Rust)] or compatible representation guarantees, ensuring method offsets in the vtable correctly resolve to the concrete implementation’s functions.
Why does Rust prohibit creating trait objects (dyn Trait) from traits that define methods consuming Self by value (fn consume(self)), and how does this relate to the Sized requirement for function return types?
This prohibition derives from object safety rules. When a method consumes self by value, the compiler must know the concrete type’s exact size to generate the proper stack frame for moving the value and to insert the correct destructor call at the precise memory offset. In a dyn Trait context, the concrete type is erased; while the vtable contains size and drop information, the caller’s stack frame cannot be dynamically adjusted to accommodate the unknown size of the moved value. Furthermore, methods returning Self would require the caller to allocate return slot space of unknown size. To prevent stack corruption and undefined behavior, Rust forbids trait objects for traits with by-value self methods, ensuring all interactions occur through indirection (&self or &mut self) where the pointer size is constant.
What is the distinction between dyn Trait automatically implementing Send when Trait carries Send as a supertrait versus explicitly annotating dyn Trait + Send, and why does the absence of both lead to the trait object failing thread-safety checks despite the underlying concrete type implementing Send?
When Trait declares Send as a supertrait (e.g., trait Trait: Send {}), the compiler propagates this bound, automatically implementing Send for dyn Trait because any implementor must necessarily be Send. Conversely, if Trait lacks this supertrait, writing dyn Trait + Send explicitly constructs a trait object that only accepts concrete types implementing both Trait and Send, narrowing the allowable types at the coercion site. If neither supertrait nor explicit bound exists, dyn Trait does not implement Send even if the concrete instance behind the pointer is thread-safe, because type erasure discards this information—the compiler cannot guarantee that all possible types that might occupy that vtable slot are Send. This prevents accidental transmission of non-thread-safe types across thread boundaries through trait object type erasure.