Frontend Labs
⋯

17. Trait objects: dyn Trait and dynamic dispatch

Generics need the concrete type at compile time. Sometimes you only know it at runtime, for example with a list of plugins or middleware loaded from config. For that, use a trait object.

trait Middleware {
    fn name(&self) -> &str;
    fn handle(&self, path: &str) -> Result<(), String>;
}

struct Auth { token: String }
struct RateLimit { max_path_len: usize }

impl Middleware for Auth {
    fn name(&self) -> &str { "auth" }
    fn handle(&self, path: &str) -> Result<(), String> {
        if path.starts_with("/admin") && self.token != "s3cret" { Err("forbidden".into()) } else { Ok(()) }
    }
}
impl Middleware for RateLimit {
    fn name(&self) -> &str { "limit" }
    fn handle(&self, path: &str) -> Result<(), String> {
        if path.len() > self.max_path_len { Err("uri too long".into()) } else { Ok(()) }
    }
}

fn main() {
    // A Vec of DIFFERENT concrete types, all behind the same interface.
    let chain: Vec<Box<dyn Middleware>> = vec![
        Box::new(RateLimit { max_path_len: 32 }),
        Box::new(Auth { token: "wrong".into() }),
    ];
    for path in ["/home", "/admin/users", "/a-very-long-path-that-should-be-rejected"] {
        let verdict = chain.iter().try_for_each(|m| m.handle(path).map_err(|e| format!("{}: {e}", m.name())));
        println!("{path:<45} {verdict:?}");
    }
    println!("{}", std::mem::size_of::<&dyn Middleware>()); // 16: data pointer + vtable pointer
}

17.1 What a trait object is in memory

&dyn Middleware and Box<dyn Middleware> are fat pointers: one pointer to the data and one pointer to a vtable, a static table of function pointers for that concrete type.

 Box<dyn Middleware>                         vtable for Auth (static, one per type)
 ┌──────────────┐                            ┌─────────────────────┐
 │ data ptr ────┼──▶ Auth { token }          │ drop_in_place<Auth> │
 │ vtable ptr ──┼──────────────────────────▶ │ size, align         │
 └──────────────┘                            │ Auth::name          │
                                             │ Auth::handle        │
                                             └─────────────────────┘

This is the same structure as a Go interface value (type word + data word), and the same thing the C guide's function-pointer structs build by hand. One difference from Go: the vtable pointer sits in the reference, not in the object, so a plain Auth value carries no type header at all.

17.2 Static vs dynamic dispatch: how to choose

Generics T: Trait Trait objects dyn Trait
Dispatch static, inlinable indirect call through the vtable
Mixed types in one collection no yes
Binary size one copy per type one copy
Typical use hot paths, libraries plugins, heterogeneous lists, reducing compile time

17.3 Dyn compatibility (object safety)

Not every trait can become a dyn Trait. Methods that return Self, or that have their own generic type parameters, cannot be put in a vtable, because the caller would need to know the concrete type. For example, Clone (fn clone(&self) -> Self) is not dyn-compatible. If one method breaks this, add where Self: Sized to it to exclude it from the vtable and keep the rest of the trait usable as dyn.

17.4 Downcasting

dyn Any can be downcast back to the concrete type: if let Some(a) = obj.downcast_ref::<Auth>(). This is Rust's version of Go's type switch, and you should need it about as rarely. Error types are the main exception: anyhow::Error::downcast_ref is how you check for one specific error inside an error chain.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36