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:
plugins: Vec<Box<dyn Plugin>>: the registry owns its plugins, each in aBox.fn add(&mut self, plugin: impl Plugin + 'static)lets callers pass a plainCounter { .. };adddoes the boxing. (Takingplugin: Box<dyn Plugin>works too; then callers writeBox::new(...).)for p in &mut self.plugins { p.handle(event) }: trait objects can have&mut selfmethods, so each plugin keeps its own state.
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>>:
add: the sum of all the numbers (0if there are none)mul: the product of all the numbers (1if there are none)div: exactly two numbers, the first divided by the second (integer division). Wrong count:div needs 2 numbers. Dividing by zero:division by zero.
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)