Static vs Dynamic Dispatch & Vtable Architecture
Explore the binary architecture of dyn Trait, the 16-byte fat pointer layout, vtable memory anatomy, and object safety rules.
Polymorphism in Rust can be achieved in two distinct ways: at compile time through Static Dispatch (generics and impl Trait), or at runtime through Dynamic Dispatch (dyn Trait).
While static dispatch delivers optimal performance, dynamic dispatch enables heterogeneous collections and flexible plugin architectures.
A dyn Trait pointer consists of an 8-byte data address and an 8-byte pointer to the static vtable.
1. Comparing Dispatch Paradigms#
Dispatch Architecture Trade-offs#
| Dimension | Static Dispatch (impl Trait / <T: Trait>) | Dynamic Dispatch (dyn Trait) |
|---|---|---|
| Method Resolution | Compile-time via monomorphization. | Runtime via virtual method table (vtable). |
| Call Instruction | Direct hardware call (call <symbol>). | Indirect hardware jump (call qword ptr [rax + offset]). |
| Inlining & SIMD | Fully inlined and vectorized by LLVM. | Cannot be inlined through dynamic jump boundaries. |
| Memory Footprint | Scalar pointer ( bytes) or unboxed value. | 16-byte fat pointer ( machine words). |
| Collection Storage | Homogeneous only (all elements same type). | Heterogeneous (can store diverse types in one vector). |
2. The 16-Byte Fat Pointer & Vtable Anatomy#
When you create a trait object reference &dyn Trait or Box<dyn Trait>, the compiler represents it as a fat pointer:
The Vtable Contains:#
- Instance Size: The memory size in bytes of the concrete type (used for deallocation).
- Instance Alignment: The alignment requirement in bytes of the concrete type.
- Drop Glue Function Pointer: A destructor function pointer executing the concrete type's destructor.
- Method Function Pointers: Sequenced function pointers for each method declared in the trait.
3. Object Safety (Dyn Compatibility)#
Not every trait can be used as a dynamic trait object (dyn Trait). To be object safe, a trait must obey two fundamental invariants:
Invariant 1: No `Self: Sized` Bound
The trait must not require Self: Sized, because the dynamic trait object itself is dynamically sized (?Sized).
Invariant 2: No `Self` Returns
Methods must not return Self. At runtime, the caller only knows the size of the 16-byte fat pointer, not the concrete size of Self needed to allocate stack space for the return value.
Invariant 3: No Generic Type Parameters on Methods
Methods must not declare their own generic type parameters. If a method had generic parameters, the compiler would have to generate an infinite number of vtable slots for every possible type!
123456789101112// Object-safe trait: can be used as dyn Backend trait Backend { fn execute(&self, query: &str) -> bool; } // Heterogeneous collection using dynamic dispatch fn run_backends(backends: &[Box<dyn Backend>]) { for backend in backends { // Dispatches through vtable at runtime backend.execute("PING"); } }
4. Concept Verification#
Test your understanding of static vs dynamic dispatch:
What two components make up the 16-byte fat pointer of a trait object like &dyn Trait on 64-bit architectures?
Why does a trait method returning 'Self' make the trait object-unsafe?