In this lesson, you will learn how to design extensible Rust systems that allow third-party developers to inject new functionality at runtime using trait implementations.
The Plugin Pattern
When building a large application, you often want to allow others to add features without modifying your core source code. We achieve this by defining a "contract" through a trait that every plugin must implement.
To manage these, the core application maintains a collection of "trait objects." Since we don't know the exact types of the plugins at compile time, we use Box<dyn PluginTrait> to store them in a heterogeneous container.
Designing the Interface
First, define the trait that outlines exactly what a plugin must do:
trait Plugin {
fn name(&self) -> &str;
fn execute(&self);
}
The core system then holds a Vec of these implementations. Because we are using dyn Plugin, the compiler ensures that any struct added to this list provides the name and execute methods.
Registering and Running
Your host application acts as a manager that iterates through its registered plugins:
struct PluginManager {
plugins: Vec<Box<dyn Plugin>>,
}
impl PluginManager {
fn run_all(&self) {
for p in &self.plugins {
println!("Running plugin: {}", p.name());
p.execute();
}
}
}
By decoupling the PluginManager from specific implementations, you can ship your application and allow users to create new structs that implement Plugin in their own crates. As long as they provide their implementation to the PluginManager, your system will treat them as first-class citizens.
This approach relies heavily on type erasure—the ability to hide the specific concrete type behind the dyn keyword—allowing your host system to treat vastly different types as a unified interface. This is the cornerstone of extensible, modular architecture in Rust.
Key Takeaway: By defining a common trait, you can create a plugin-based architecture where the host system remains completely agnostic of the concrete types being executed.