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.

03. Advanced Abstractions, Traits & Smart Pointers7 min read

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.

Trait Object SizeLayout
16 Bytes

A dyn Trait pointer consists of an 8-byte data address and an 8-byte pointer to the static vtable.

1. Comparing Dispatch Paradigms#

Architecture Diagram
Rendering diagram...

Dispatch Architecture Trade-offs#

DimensionStatic Dispatch (impl Trait / <T: Trait>)Dynamic Dispatch (dyn Trait)
Method ResolutionCompile-time via monomorphization.Runtime via virtual method table (vtable).
Call InstructionDirect hardware call (call <symbol>).Indirect hardware jump (call qword ptr [rax + offset]).
Inlining & SIMDFully inlined and vectorized by LLVM.Cannot be inlined through dynamic jump boundaries.
Memory FootprintScalar pointer (88 bytes) or unboxed value.16-byte fat pointer (22 machine words).
Collection StorageHomogeneous 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:

FatPointer(dyn Trait)=(Ptrdata, Ptrvtable)\text{FatPointer}(\text{dyn Trait}) = \bigl(\text{Ptr}_{\text{data}}, \, \text{Ptr}_{\text{vtable}}\bigr)
Architecture Diagram
Rendering diagram...

The Vtable Contains:#

  1. Instance Size: The memory size in bytes of the concrete type (used for deallocation).
  2. Instance Alignment: The alignment requirement in bytes of the concrete type.
  3. Drop Glue Function Pointer: A destructor function pointer executing the concrete type's destructor.
  4. 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:

1

Invariant 1: No `Self: Sized` Bound

The trait must not require Self: Sized, because the dynamic trait object itself is dynamically sized (?Sized).

2

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.

3

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!

1
2
3
4
5
6
7
8
9
10
11
12
// 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:

Concept check

What two components make up the 16-byte fat pointer of a trait object like &dyn Trait on 64-bit architectures?

Concept check

Why does a trait method returning 'Self' make the trait object-unsafe?