RustArena

Step 4 of 7

Trait objects in structs: plugins

Trait objects shine in registries: a struct that holds a list of "things that can handle X" and runs them all, without knowing their types. Logging backends, middleware in a web server, game systems, editor plugins: all built this way. Anyone can add a new kind of handler by implementing the trait, without changing the registry.

trait Plugin {
    fn name(&self) -> String;
    fn handle(&mut self, event: &str);
    fn report(&self) -> String;
}

struct Counter {
    seen: u32,
}

struct ErrorWatch {
    errors: Vec<String>,
}

impl Plugin for Counter {
    fn name(&self) -> String {
        String::from("counter")
    }
    fn handle(&mut self, _event: &str) {
        self.seen += 1;
    }
    fn report(&self) -> String {
        format!("{} events", self.seen)
    }
}

impl Plugin for ErrorWatch {
    fn name(&self) -> String {
        String::from("errors")
    }
    fn handle(&mut self, event: &str) {
        if let Some(msg) = event.strip_prefix("error ") {
            self.errors.push(msg.to_string());
        }
    }
    fn report(&self) -> String {
        self.errors.join(", ")
    }
}

struct Pipeline {
    plugins: Vec<Box<dyn Plugin>>,
}

impl Pipeline {
    fn new() -> Self {
        Pipeline { plugins: Vec::new() }
    }

    fn add(&mut self, plugin: impl Plugin + 'static) {
        self.plugins.push(Box::new(plugin));
    }

    fn run(&mut self, event: &str) {
        for p in &mut self.plugins {
            p.handle(event);
        }
    }

    fn print_reports(&self) {
        for p in &self.plugins {
            println!("{}: {}", p.name(), p.report());
        }
    }
}

fn main() {
    let mut pipe = Pipeline::new();
    pipe.add(Counter { seen: 0 });
    pipe.add(ErrorWatch { errors: Vec::new() });
    for e in ["login ana", "error disk full", "logout ana", "error timeout"] {
        pipe.run(e);
    }
    pipe.print_reports();
}
counter: 4 events
errors: disk full, timeout

Piece by piece:

Why + 'static?

A Box<dyn Plugin> in a struct field is short for Box<dyn Plugin + 'static>: "a plugin that doesn't hold borrowed references that could expire". Without the bound, add doesn't compile:

trait Notifier {
    fn send(&self, msg: &str) -> String;
}

struct Hub {
    list: Vec<Box<dyn Notifier>>,
}

impl Hub {
    fn add(&mut self, n: impl Notifier) {
        self.list.push(Box::new(n));
    }
}

fn main() {}
error[E0310]: the parameter type `impl Notifier` may not live long enough

impl Notifier could be a type holding a &'a str, and the box might outlive it. 'static here doesn't mean "lives forever"; it means "owns its data" (the T: 'static bound from the lifetimes module). Every type with owned fields (String, Vec, numbers) qualifies. The compiler's help suggests exactly impl Notifier + 'static.

Your task

Build an alert registry. Alerts holds a Vec<Box<dyn Notifier>>. Write new (empty), register (boxes and stores a notifier), channels (each notifier's channel(), in order) and broadcast (each notifier's send(msg), in order).

Challenge 2

Plugins with state. Counter is done. Write a second plugin Longest that remembers the longest event it has seen (its summary is longest: <event>, or longest: if none). Then write Pipeline::run, which sends every event to every plugin, and Pipeline::summaries, which collects every plugin's summary() in order.

Challenge 3

Write a calculator that dispatches commands by name. Implement the trait Command for three types and store them in a HashMap<String, Box<dyn Command>>:

Each line of stdin is a command name and numbers, like add 2 3 4. Print the result, or error: <message> for an error. An unknown name prints error: unknown command <name>, and a word that isn't a number prints error: bad number <word>.

Previous: Static vs dynamic dispatch Next: Dyn compatibility (object safety)