Trait Objects and Dynamic Dispatch
Designing Extensible Interfaces
An extensible interface allows downstream users or plugins to add new implementations of a behavior and compose them into existing pipelines, registries, or middlewares without modifying the core library source code.
Designing a dyn-Compatible Plugin and Middleware Pipeline
To make a trait work cleanly in both generic and dynamic (Box<dyn Trait>) plugin registries:
- Keep core trait methods
dyn-compatible (&selfor&mut self, concrete slice/struct parameters rather than generic<T>method parameters). - Gate convenience builder or boxing methods with
where Self: Sized. - Provide a forwarding
impl<T: Trait + ?Sized> Trait for Box<T>soBox<dyn Trait>itself satisfies the trait bound.
#[derive(Debug, Clone)]
pub struct Request {
pub path: String,
pub headers: Vec<(String, String)>,
}
pub trait Middleware: Send + Sync {
fn handle(&self, req: &mut Request);
}
// Forwarding implementation so `Box<dyn Middleware>` also implements `Middleware`:
impl<M: Middleware + ?Sized> Middleware for Box<M> {
fn handle(&self, req: &mut Request) {
(**self).handle(req);
}
}
pub struct Pipeline {
stages: Vec<Box<dyn Middleware>>,
}
impl Pipeline {
pub fn new() -> Self {
Self { stages: Vec::new() }
}
pub fn add<M: Middleware + 'static>(&mut self, stage: M) {
self.stages.push(Box::new(stage));
}
pub fn execute(&self, req: &mut Request) {
for stage in &self.stages {
stage.handle(req);
}
}
}
// Downstream user implementations:
struct PathPrefixer(&'static str);
impl Middleware for PathPrefixer {
fn handle(&self, req: &mut Request) {
req.path = format!("{}{}", self.0, req.path);
}
}
struct HeaderInjector {
key: &'static str,
value: &'static str,
}
impl Middleware for HeaderInjector {
fn handle(&self, req: &mut Request) {
req.headers.push((self.key.to_owned(), self.value.to_owned()));
}
}
fn main() {
let mut pipeline = Pipeline::new();
pipeline.add(PathPrefixer("/v1"));
pipeline.add(HeaderInjector {
key: "X-Runtime",
value: "Rust-2024",
});
let mut req = Request {
path: String::from("/users"),
headers: Vec::new(),
};
pipeline.execute(&mut req);
println!("Final request: {req:?}");
}
Final request: Request { path: "/v1/users", headers: [("X-Runtime", "Rust-2024")] }
Supporting Cloning on Trait Objects (CloneBox Pattern)
Because Clone::clone(&self) -> Self returns Self by value, Clone is not dyn-compatible (Box<dyn MyTrait + Clone> does not compile). To make Box<dyn MyTrait> cloneable, add a clone_box(&self) -> Box<dyn MyTrait> helper trait implemented via a blanket impl for all T: MyTrait + Clone:
trait Action: ActionClone {
fn describe(&self) -> String;
}
trait ActionClone {
fn clone_box(&self) -> Box<dyn Action>;
}
impl<T> ActionClone for T
where
T: 'static + Action + Clone,
{
fn clone_box(&self) -> Box<dyn Action> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Action> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[derive(Clone)]
struct PingAction {
target: String,
}
impl Action for PingAction {
fn describe(&self) -> String {
format!("PING {}", self.target)
}
}
fn main() {
let original: Box<dyn Action> = Box::new(PingAction {
target: String::from("10.0.0.1"),
});
let copy = original.clone();
println!("Original: {}", original.describe());
println!("Copy: {}", copy.describe());
}
Original: PING 10.0.0.1
Copy: PING 10.0.0.1