Box<dyn Trait> patterns and vtables
A trait object is Rust's mechanism for runtime polymorphism, allowing you to work with values of different concrete types through a common trait interface. Unlike generic type parameters which are resolved at compile time (static dispatch), trait objects use dynamic dispatch through a virtual table (vtable) mechanism.
Trait objects are created using thedyn Trait syntax and are always accessed through a pointer type (&dyn Trait, Box, Arc).
// Static dispatch with generics (monomorphization)
fn process_static<T: Display>(item: &T) {
println!("{}", item); // Compiler generates specialized code for each T
}
// Dynamic dispatch with trait objects
fn process_dynamic(item: &dyn Display) {
println!("{}", item); // Runtime lookup through vtable
}
// Trait object types
let boxed: Box<dyn Display> = Box::new("hello");
let referenced: &dyn Display = &42;
let shared: Arc<dyn Mutex<dyn Write>> = Arc::new(Mutex::new(File::create("log.txt")?));
Key characteristics:
Understanding the memory representation is crucial for performance-conscious systems programming:
use std::fmt::Display;
// Memory layout visualization
fn memory_layout_demo() {
let value: i32 = 42;
let trait_obj: &dyn Display = &value;
// trait_obj is a "fat pointer" with 2 components:
// +-------------------+-------------------+
// | data_ptr | vtable_ptr |
// | (8 bytes) | (8 bytes) |
// +-------------------+-------------------+
// | |
// v v
// [42: i32] [VTable for Display]
// - size/align
// - destructor
// - Display::fmt
// Size comparison
assert_eq!(std::mem::size_of::<&i32>(), 8); // thin pointer
assert_eq!(std::mem::size_of::<&dyn Display>(), 16); // fat pointer
println!("Thin pointer: {} bytes", std::mem::size_of::<&i32>());
println!("Fat pointer: {} bytes", std::mem::size_of::<&dyn Display>());
}
// The vtable structure (conceptual - compiler-generated)
struct VTable {
destructor: fn(*mut ()),
size: usize,
align: usize,
// Method pointers for each trait method
display_fmt: fn(*const (), &mut Formatter) -> Result,
// ... other trait methods
}
// What the compiler generates (simplified)
struct TraitObject {
data: *const (), // Pointer to actual data
vtable: *const VTable, // Pointer to vtable
}
VTable contents:
// Multiple types, one trait
trait Drawable {
fn draw(&self);
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Drawable for Circle {
fn draw(&self) { println!("Drawing circle"); }
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
}
impl Drawable for Rectangle {
fn draw(&self) { println!("Drawing rectangle"); }
fn area(&self) -> f64 { self.width * self.height }
}
// Compiler generates TWO vtables:
// VTable_Circle_Drawable:
// - drop_in_place: <Circle as Drop>::drop
// - size: 8, align: 8
// - draw: <Circle as Drawable>::draw
// - area: <Circle as Drawable>::area
//
// VTable_Rectangle_Drawable:
// - drop_in_place: <Rectangle as Drop>::drop
// - size: 16, align: 8
// - draw: <Rectangle as Drawable>::draw
// - area: <Rectangle as Drawable>::area
fn heterogeneous_collection() {
let shapes: Vec<Box<dyn Drawable>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 20.0 }),
];
// Each element has different data but same fat pointer layout
for shape in &shapes {
shape.draw(); // vtable lookup: shape.vtable.draw(shape.data)
println!("Area: {}", shape.area());
}
}
Not all traits can be made into trait objects. A trait is object-safe if it follows these rules:
// ❌ NOT object-safe: returns Self
trait Cloneable {
fn clone(&self) -> Self; // Compiler doesn't know size of Self
}
// ✅ Object-safe alternative: use where clause
trait CloneableBox {
fn clone_box(&self) -> Box<dyn CloneableBox>;
}
impl<T: Clone> CloneableBox for T {
fn clone_box(&self) -> Box<dyn CloneableBox> {
Box::new(self.clone())
}
}
// Real-world example: std::error::Error is object-safe
trait Error: Display + Debug {
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
// Note: No clone() method that returns Self
}
// ❌ NOT object-safe: generic method
trait Processor {
fn process<T>(&self, item: T); // Can't build vtable for infinite T types
}
// ✅ Object-safe alternatives:
// Option 1: Make the trait itself generic (but then it's not object-safe either)
trait Processor<T> {
fn process(&self, item: T);
}
// Option 2: Use trait objects in the method signature
trait Processor {
fn process(&self, item: &dyn Any);
}
// Option 3: Use associated types
trait Processor {
type Item;
fn process(&self, item: Self::Item);
}
// ❌ NOT object-safe (pre-Rust 1.73)
trait Config {
const MAX_SIZE: usize; // No place in vtable for this
}
// ✅ Object-safe alternative: associated function
trait Config {
fn max_size(&self) -> usize;
}
trait Example {
// ✅ Object-safe: no Self restriction
fn method1(&self);
// ✅ Object-safe: Sized bound makes method unavailable on trait objects
fn method2(&self) where Self: Sized {
// This method can't be called on dyn Example
}
// ❌ Would be NOT object-safe if this was required for all methods
fn method3(self) where Self: Sized; // Takes self by value
}
// The compiler provides helpful errors
trait NotObjectSafe {
fn clone(&self) -> Self;
}
fn test_object_safety() {
// ❌ Compile error: the trait `NotObjectSafe` cannot be made into an object
// let obj: Box<dyn NotObjectSafe> = Box::new(MyType);
// ^^^^^^^^^^^^^^^^
// the trait cannot be made into an object because method `clone`
// references the `Self` type in its return type
}
// You can use the `dyn_compatible` trait bound (Rust 1.83+)
fn requires_object_safe<T: ?Sized + dyn_compatible::DynCompatible>() {
// T must be object-safe
}
A plugin system where modules are loaded at runtime is a perfect use case for trait objects.
use std::collections::HashMap;
use std::error::Error;
use std::sync::{Arc, RwLock};
/// Object-safe plugin trait
pub trait Plugin: Send + Sync {
/// Plugin name for identification
fn name(&self) -> &str;
/// Initialize plugin with configuration
fn init(&mut self, config: &PluginConfig) -> Result<(), Box<dyn Error>>;
/// Handle an event
fn handle_event(&self, event: &Event) -> Result<Response, Box<dyn Error>>;
/// Shutdown hook
fn shutdown(&mut self) -> Result<(), Box<dyn Error>> {
Ok(()) // Default implementation
}
}
pub struct PluginConfig {
pub settings: HashMap<String, String>,
}
pub struct Event {
pub event_type: String,
pub payload: Vec<u8>,
}
pub struct Response {
pub status: u16,
pub data: Vec<u8>,
}
/// Plugin manager that owns trait objects
pub struct PluginManager {
plugins: RwLock<HashMap<String, Box<dyn Plugin>>>,
}
impl PluginManager {
pub fn new() -> Self {
Self {
plugins: RwLock::new(HashMap::new()),
}
}
/// Register a plugin (takes ownership of Box<dyn Plugin>)
pub fn register(&self, plugin: Box<dyn Plugin>) -> Result<(), String> {
let name = plugin.name().to_string();
let mut plugins = self.plugins.write().unwrap();
if plugins.contains_key(&name) {
return Err(format!("Plugin '{}' already registered", name));
}
plugins.insert(name, plugin);
Ok(())
}
/// Dispatch event to all plugins
pub fn dispatch_event(&self, event: &Event) -> Vec<Result<Response, Box<dyn Error>>> {
let plugins = self.plugins.read().unwrap();
plugins.values()
.map(|plugin| plugin.handle_event(event))
.collect()
}
/// Get plugin by name
pub fn get_plugin(&self, name: &str) -> Option<Arc<Box<dyn Plugin>>> {
let plugins = self.plugins.read().unwrap();
plugins.get(name).map(|p| Arc::new(p.clone()))
}
}
// Concrete plugin implementations
struct LoggingPlugin {
name: String,
log_level: String,
}
impl Plugin for LoggingPlugin {
fn name(&self) -> &str {
&self.name
}
fn init(&mut self, config: &PluginConfig) -> Result<(), Box<dyn Error>> {
self.log_level = config.settings
.get("log_level")
.cloned()
.unwrap_or_else(|| "info".to_string());
println!("[{}] Initialized with log level: {}", self.name, self.log_level);
Ok(())
}
fn handle_event(&self, event: &Event) -> Result<Response, Box<dyn Error>> {
println!("[{}] Event: {} (payload: {} bytes)",
self.name, event.event_type, event.payload.len());
Ok(Response {
status: 200,
data: b"logged".to_vec(),
})
}
fn shutdown(&mut self) -> Result<(), Box<dyn Error>> {
println!("[{}] Shutting down", self.name);
Ok(())
}
}
struct MetricsPlugin {
name: String,
event_count: std::sync::atomic::AtomicU64,
}
impl Plugin for MetricsPlugin {
fn name(&self) -> &str {
&self.name
}
fn init(&mut self, config: &PluginConfig) -> Result<(), Box<dyn Error>> {
println!("[{}] Metrics plugin initialized", self.name);
Ok(())
}
fn handle_event(&self, event: &Event) -> Result<Response, Box<dyn Error>> {
let count = self.event_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Response {
status: 200,
data: format!("event_count: {}", count + 1).into_bytes(),
})
}
}
// Usage example
fn plugin_system_example() {
let manager = PluginManager::new();
// Register plugins dynamically
manager.register(Box::new(LoggingPlugin {
name: "logger".to_string(),
log_level: String::new(),
})).unwrap();
manager.register(Box::new(MetricsPlugin {
name: "metrics".to_string(),
event_count: std::sync::atomic::AtomicU64::new(0),
})).unwrap();
// Dispatch event to all plugins
let event = Event {
event_type: "user.login".to_string(),
payload: b"user_id: 42".to_vec(),
};
let responses = manager.dispatch_event(&event);
println!("Received {} responses", responses.len());
}
Why trait objects here?
Box allows ownership transferSend + Sync bounds enable thread-safe plugin systemWeb frameworks use trait objects for flexible request routing and middleware chains.
use std::collections::HashMap;
use std::sync::Arc;
/// Object-safe handler trait for HTTP requests
pub trait Handler: Send + Sync {
fn handle(&self, req: &Request) -> Response;
}
pub struct Request {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
pub struct Response {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Vec<u8>,
}
impl Response {
fn ok(body: impl Into<Vec<u8>>) -> Self {
Self {
status: 200,
headers: HashMap::new(),
body: body.into(),
}
}
fn not_found() -> Self {
Self {
status: 404,
headers: HashMap::new(),
body: b"Not Found".to_vec(),
}
}
}
/// Router that stores trait objects
pub struct Router {
routes: HashMap<String, Arc<dyn Handler>>,
middleware: Vec<Arc<dyn Middleware>>,
}
impl Router {
pub fn new() -> Self {
Self {
routes: HashMap::new(),
middleware: Vec::new(),
}
}
/// Register a route with a handler
pub fn route(&mut self, path: impl Into<String>, handler: Arc<dyn Handler>) {
self.routes.insert(path.into(), handler);
}
/// Add middleware to the chain
pub fn middleware(&mut self, mw: Arc<dyn Middleware>) {
self.middleware.push(mw);
}
/// Handle incoming request
pub fn handle(&self, mut req: Request) -> Response {
// Run middleware chain
for mw in &self.middleware {
if let Some(response) = mw.before(&mut req) {
return response;
}
}
// Find and execute handler
let response = self.routes
.get(&req.path)
.map(|handler| handler.handle(&req))
.unwrap_or_else(|| Response::not_found());
// Run middleware post-processing
let mut response = response;
for mw in self.middleware.iter().rev() {
mw.after(&req, &mut response);
}
response
}
}
/// Middleware trait for request/response processing
pub trait Middleware: Send + Sync {
/// Called before handler (can short-circuit)
fn before(&self, req: &mut Request) -> Option<Response> {
None
}
/// Called after handler
fn after(&self, req: &Request, res: &mut Response) {}
}
// Concrete handler implementations
/// Simple function-based handler
struct FnHandler<F>
where
F: Fn(&Request) -> Response + Send + Sync,
{
func: F,
}
impl<F> Handler for FnHandler<F>
where
F: Fn(&Request) -> Response + Send + Sync,
{
fn handle(&self, req: &Request) -> Response {
(self.func)(req)
}
}
/// JSON API handler
struct JsonApiHandler {
endpoint: String,
}
impl Handler for JsonApiHandler {
fn handle(&self, req: &Request) -> Response {
// In real code: deserialize JSON, process, serialize response
let response_body = format!(
r#"{{"endpoint": "{}", "method": "{}"}}"#,
self.endpoint, req.method
);
let mut res = Response::ok(response_body);
res.headers.insert("Content-Type".to_string(), "application/json".to_string());
res
}
}
/// Static file handler
struct StaticFileHandler {
base_path: String,
}
impl Handler for StaticFileHandler {
fn handle(&self, req: &Request) -> Response {
// In real code: read file from disk
Response::ok(format!("File from {}/{}", self.base_path, req.path))
}
}
// Middleware implementations
struct LoggingMiddleware;
impl Middleware for LoggingMiddleware {
fn before(&self, req: &mut Request) -> Option<Response> {
println!("[REQUEST] {} {}", req.method, req.path);
None // Don't short-circuit
}
fn after(&self, req: &Request, res: &mut Response) {
println!("[RESPONSE] {} {} -> {}", req.method, req.path, res.status);
}
}
struct AuthMiddleware {
required_token: String,
}
impl Middleware for AuthMiddleware {
fn before(&self, req: &mut Request) -> Option<Response> {
match req.headers.get("Authorization") {
Some(token) if token == &self.required_token => None,
_ => Some(Response {
status: 401,
headers: HashMap::new(),
body: b"Unauthorized".to_vec(),
}),
}
}
}
struct CorsMiddleware;
impl Middleware for CorsMiddleware {
fn after(&self, req: &Request, res: &mut Response) {
res.headers.insert(
"Access-Control-Allow-Origin".to_string(),
"*".to_string()
);
}
}
// Usage example
fn web_router_example() {
let mut router = Router::new();
// Register middleware (trait objects in Vec)
router.middleware(Arc::new(LoggingMiddleware));
router.middleware(Arc::new(CorsMiddleware));
// Register routes (different handler types, same trait)
router.route(
"/api/users",
Arc::new(JsonApiHandler {
endpoint: "users".to_string(),
}),
);
router.route(
"/api/posts",
Arc::new(JsonApiHandler {
endpoint: "posts".to_string(),
}),
);
router.route(
"/static",
Arc::new(StaticFileHandler {
base_path: "/var/www".to_string(),
}),
);
// Function-based handler using closure
router.route(
"/health",
Arc::new(FnHandler {
func: |_req| Response::ok(b"OK"),
}),
);
// Handle requests
let req = Request {
method: "GET".to_string(),
path: "/api/users".to_string(),
headers: HashMap::new(),
body: vec![],
};
let response = router.handle(req);
println!("Status: {}", response.status);
println!("Body: {}", String::from_utf8_lossy(&response.body));
}
Why trait objects here?
Arc enables shared ownership across threadsNetwork systems need to handle multiple protocols dynamically, making trait objects essential.
use std::io::{self, Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
/// Object-safe protocol trait
pub trait Protocol: Send + Sync {
/// Protocol name (HTTP, WebSocket, gRPC, etc.)
fn name(&self) -> &str;
/// Parse incoming data and return message boundaries
fn parse(&self, buffer: &[u8]) -> Result<ParseResult, ProtocolError>;
/// Encode a message for transmission
fn encode(&self, message: &Message) -> Result<Vec<u8>, ProtocolError>;
/// Protocol-specific handshake
fn handshake(&self, stream: &mut TcpStream) -> Result<(), ProtocolError> {
Ok(()) // Default: no handshake
}
/// Protocol-specific keepalive
fn keepalive_interval(&self) -> Option<Duration> {
None
}
}
pub struct Message {
pub headers: std::collections::HashMap<String, String>,
pub body: Vec<u8>,
}
pub struct ParseResult {
pub message: Option<Message>,
pub bytes_consumed: usize,
}
#[derive(Debug)]
pub enum ProtocolError {
InvalidFormat(String),
Io(io::Error),
Unsupported(String),
}
impl From<io::Error> for ProtocolError {
fn from(err: io::Error) -> Self {
ProtocolError::Io(err)
}
}
/// HTTP/1.1 Protocol implementation
pub struct HttpProtocol;
impl Protocol for HttpProtocol {
fn name(&self) -> &str {
"HTTP/1.1"
}
fn parse(&self, buffer: &[u8]) -> Result<ParseResult, ProtocolError> {
// Simplified HTTP parsing
if let Some(pos) = buffer.windows(4).position(|w| w == b"\r\n\r\n") {
let header_end = pos + 4;
// Parse headers (simplified)
let header_str = std::str::from_utf8(&buffer[..pos])
.map_err(|e| ProtocolError::InvalidFormat(e.to_string()))?;
let mut lines = header_str.lines();
let request_line = lines.next()
.ok_or_else(|| ProtocolError::InvalidFormat("Empty request".to_string()))?;
let mut headers = std::collections::HashMap::new();
for line in lines {
if let Some(colon_pos) = line.find(':') {
let key = line[..colon_pos].trim().to_string();
let value = line[colon_pos + 1..].trim().to_string();
headers.insert(key, value);
}
}
// Check for Content-Length
let content_length: usize = headers
.get("Content-Length")
.and_then(|v| v.parse().ok())
.unwrap_or(0);
let total_length = header_end + content_length;
if buffer.len() >= total_length {
let body = buffer[header_end..total_length].to_vec();
return Ok(ParseResult {
message: Some(Message { headers, body }),
bytes_consumed: total_length,
});
}
}
// Incomplete message
Ok(ParseResult {
message: None,
bytes_consumed: 0,
})
}
fn encode(&self, message: &Message) -> Result<Vec<u8>, ProtocolError> {
let mut response = String::from("HTTP/1.1 200 OK\r\n");
for (key, value) in &message.headers {
response.push_str(&format!("{}: {}\r\n", key, value));
}
response.push_str(&format!("Content-Length: {}\r\n", message.body.len()));
response.push_str("\r\n");
let mut bytes = response.into_bytes();
bytes.extend_from_slice(&message.body);
Ok(bytes)
}
}
/// WebSocket Protocol implementation
pub struct WebSocketProtocol {
handshake_complete: std::sync::atomic::AtomicBool,
}
impl WebSocketProtocol {
pub fn new() -> Self {
Self {
handshake_complete: std::sync::atomic::AtomicBool::new(false),
}
}
}
impl Protocol for WebSocketProtocol {
fn name(&self) -> &str {
"WebSocket"
}
fn parse(&self, buffer: &[u8]) -> Result<ParseResult, ProtocolError> {
// Simplified WebSocket frame parsing
if buffer.len() < 2 {
return Ok(ParseResult {
message: None,
bytes_consumed: 0,
});
}
let fin = buffer[0] & 0x80 != 0;
let opcode = buffer[0] & 0x0F;
let masked = buffer[1] & 0x80 != 0;
let mut payload_len = (buffer[1] & 0x7F) as usize;
let mut offset = 2;
// Extended payload length
if payload_len == 126 {
if buffer.len() < 4 { return Ok(ParseResult { message: None, bytes_consumed: 0 }); }
payload_len = u16::from_be_bytes([buffer[2], buffer[3]]) as usize;
offset = 4;
} else if payload_len == 127 {
if buffer.len() < 10 { return Ok(ParseResult { message: None, bytes_consumed: 0 }); }
payload_len = u64::from_be_bytes([
buffer[2], buffer[3], buffer[4], buffer[5],
buffer[6], buffer[7], buffer[8], buffer[9],
]) as usize;
offset = 10;
}
// Masking key
let mask_offset = offset;
if masked {
offset += 4;
}
let frame_length = offset + payload_len;
if buffer.len() < frame_length {
return Ok(ParseResult {
message: None,
bytes_consumed: 0,
});
}
let mut payload = buffer[offset..frame_length].to_vec();
// Unmask payload
if masked {
let mask = &buffer[mask_offset..mask_offset + 4];
for (i, byte) in payload.iter_mut().enumerate() {
*byte ^= mask[i % 4];
}
}
Ok(ParseResult {
message: Some(Message {
headers: std::collections::HashMap::new(),
body: payload,
}),
bytes_consumed: frame_length,
})
}
fn encode(&self, message: &Message) -> Result<Vec<u8>, ProtocolError> {
// Simplified WebSocket frame encoding
let payload_len = message.body.len();
let mut frame = Vec::new();
// FIN + opcode (text frame)
frame.push(0x81);
// Payload length
if payload_len < 126 {
frame.push(payload_len as u8);
} else if payload_len < 65536 {
frame.push(126);
frame.extend_from_slice(&(payload_len as u16).to_be_bytes());
} else {
frame.push(127);
frame.extend_from_slice(&(payload_len as u64).to_be_bytes());
}
frame.extend_from_slice(&message.body);
Ok(frame)
}
fn handshake(&self, stream: &mut TcpStream) -> Result<(), ProtocolError> {
// WebSocket handshake (simplified)
let mut buffer = [0u8; 1024];
let n = stream.read(&mut buffer)?;
// In real code: parse Sec-WebSocket-Key, compute accept key
let response = b"HTTP/1.1 101 Switching Protocols\r\n\
Upgrade: websocket\r\n\
Connection: Upgrade\r\n\
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n";
stream.write_all(response)?;
self.handshake_complete.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
}
fn keepalive_interval(&self) -> Option<Duration> {
Some(Duration::from_secs(30))
}
}
/// Connection handler that uses protocol trait objects
pub struct Connection {
stream: TcpStream,
protocol: Arc<dyn Protocol>,
buffer: Vec<u8>,
}
impl Connection {
pub fn new(stream: TcpStream, protocol: Arc<dyn Protocol>) -> io::Result<Self> {
Ok(Self {
stream,
protocol,
buffer: Vec::with_capacity(8192),
})
}
pub fn handshake(&mut self) -> Result<(), ProtocolError> {
self.protocol.handshake(&mut self.stream)
}
pub fn receive(&mut self) -> Result<Option<Message>, ProtocolError> {
let mut temp_buf = [0u8; 4096];
let n = self.stream.read(&mut temp_buf)?;
if n == 0 {
return Err(ProtocolError::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"Connection closed",
)));
}
self.buffer.extend_from_slice(&temp_buf[..n]);
// Parse using protocol-specific logic
let result = self.protocol.parse(&self.buffer)?;
if let Some(message) = result.message {
self.buffer.drain(..result.bytes_consumed);
Ok(Some(message))
} else {
Ok(None)
}
}
pub fn send(&mut self, message: &Message) -> Result<(), ProtocolError> {
let encoded = self.protocol.encode(message)?;
self.stream.write_all(&encoded)?;
Ok(())
}
pub fn protocol_name(&self) -> &str {
self.protocol.name()
}
}
/// Protocol negotiation based on incoming data
pub fn negotiate_protocol(initial_data: &[u8]) -> Arc<dyn Protocol> {
// Simplified protocol detection
if initial_data.starts_with(b"GET ") || initial_data.starts_with(b"POST ") {
// Check for WebSocket upgrade
if let Ok(s) = std::str::from_utf8(initial_data) {
if s.contains("Upgrade: websocket") {
return Arc::new(WebSocketProtocol::new());
}
}
Arc::new(HttpProtocol)
} else {
// Default to HTTP
Arc::new(HttpProtocol)
}
}
// Usage example
fn protocol_handler_example() {
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
println!("Server listening on port 8080");
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
// Read initial data for protocol negotiation
let mut peek_buf = [0u8; 1024];
let n = stream.peek(&mut peek_buf).unwrap_or(0);
// Negotiate protocol based on initial data
let protocol = negotiate_protocol(&peek_buf[..n]);
println!("Negotiated protocol: {}", protocol.name());
// Create connection with appropriate protocol handler
let mut conn = Connection::new(stream, protocol).unwrap();
// Handle connection based on protocol
if let Err(e) = conn.handshake() {
eprintln!("Handshake error: {:?}", e);
continue;
}
// Receive and echo messages
loop {
match conn.receive() {
Ok(Some(message)) => {
println!("Received {} bytes via {}",
message.body.len(), conn.protocol_name());
// Echo back
if let Err(e) = conn.send(&message) {
eprintln!("Send error: {:?}", e);
break;
}
}
Ok(None) => {
// Incomplete message, wait for more data
}
Err(e) => {
eprintln!("Receive error: {:?}", e);
break;
}
}
}
}
Err(e) => {
eprintln!("Connection error: {}", e);
}
}
}
}
Why trait objects here?
Connection type works with any protocolArc enables protocol sharing across connectionsMulti-backend logging systems are a classic trait object use case.
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
/// Object-safe logger trait
pub trait Logger: Send + Sync {
fn log(&self, level: LogLevel, message: &str);
fn flush(&self) -> io::Result<()> { Ok(()) }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn as_str(&self) -> &str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
}
}
}
/// Console logger
pub struct ConsoleLogger {
min_level: LogLevel,
}
impl ConsoleLogger {
pub fn new(min_level: LogLevel) -> Self {
Self { min_level }
}
}
impl Logger for ConsoleLogger {
fn log(&self, level: LogLevel, message: &str) {
if level >= self.min_level {
let timestamp = humantime::format_rfc3339_seconds(SystemTime::now());
println!("[{}] {} - {}", timestamp, level.as_str(), message);
}
}
}
/// File logger with rotation
pub struct FileLogger {
file: Mutex<File>,
min_level: LogLevel,
}
impl FileLogger {
pub fn new(path: &str, min_level: LogLevel) -> io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
Ok(Self {
file: Mutex::new(file),
min_level,
})
}
}
impl Logger for FileLogger {
fn log(&self, level: LogLevel, message: &str) {
if level >= self.min_level {
let timestamp = humantime::format_rfc3339_seconds(SystemTime::now());
let log_line = format!("[{}] {} - {}\n", timestamp, level.as_str(), message);
if let Ok(mut file) = self.file.lock() {
let _ = file.write_all(log_line.as_bytes());
}
}
}
fn flush(&self) -> io::Result<()> {
if let Ok(mut file) = self.file.lock() {
file.flush()
} else {
Ok(())
}
}
}
/// Network logger (sends logs to remote server)
pub struct NetworkLogger {
endpoint: String,
client: reqwest::blocking::Client,
min_level: LogLevel,
}
impl NetworkLogger {
pub fn new(endpoint: String, min_level: LogLevel) -> Self {
Self {
endpoint,
client: reqwest::blocking::Client::new(),
min_level,
}
}
}
impl Logger for NetworkLogger {
fn log(&self, level: LogLevel, message: &str) {
if level >= self.min_level {
let payload = serde_json::json!({
"timestamp": SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs(),
"level": level.as_str(),
"message": message,
});
// Fire and forget (in production, use async or buffering)
let _ = self.client.post(&self.endpoint).json(&payload).send();
}
}
}
/// Multi-logger that fans out to multiple backends
pub struct MultiLogger {
loggers: Vec<Arc<dyn Logger>>,
}
impl MultiLogger {
pub fn new() -> Self {
Self {
loggers: Vec::new(),
}
}
pub fn add_logger(&mut self, logger: Arc<dyn Logger>) {
self.loggers.push(logger);
}
}
impl Logger for MultiLogger {
fn log(&self, level: LogLevel, message: &str) {
for logger in &self.loggers {
logger.log(level, message);
}
}
fn flush(&self) -> io::Result<()> {
for logger in &self.loggers {
logger.flush()?;
}
Ok(())
}
}
/// Global logger instance
pub struct GlobalLogger {
logger: Arc<dyn Logger>,
}
impl GlobalLogger {
pub fn new(logger: Arc<dyn Logger>) -> Self {
Self { logger }
}
pub fn trace(&self, message: &str) {
self.logger.log(LogLevel::Trace, message);
}
pub fn debug(&self, message: &str) {
self.logger.log(LogLevel::Debug, message);
}
pub fn info(&self, message: &str) {
self.logger.log(LogLevel::Info, message);
}
pub fn warn(&self, message: &str) {
self.logger.log(LogLevel::Warn, message);
}
pub fn error(&self, message: &str) {
self.logger.log(LogLevel::Error, message);
}
}
// Usage example
fn logging_example() {
// Create multi-backend logger
let mut multi_logger = MultiLogger::new();
// Add console logger
multi_logger.add_logger(Arc::new(ConsoleLogger::new(LogLevel::Debug)));
// Add file logger
if let Ok(file_logger) = FileLogger::new("/tmp/app.log", LogLevel::Info) {
multi_logger.add_logger(Arc::new(file_logger));
}
// Add network logger (in production)
// multi_logger.add_logger(Arc::new(NetworkLogger::new(
// "https://logs.example.com/ingest".to_string(),
// LogLevel::Error,
// )));
// Create global logger
let logger = GlobalLogger::new(Arc::new(multi_logger));
// Use logger
logger.info("Application started");
logger.debug("Processing request");
logger.warn("Cache miss");
logger.error("Database connection failed");
// Flush all loggers
logger.logger.flush().unwrap();
}
Why trait objects here?
Arc enables shared logger across threads
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 10.0, height: 20.0 }),
];
fn create_handler(handler_type: &str) -> Box<dyn Handler> {
match handler_type {
"json" => Box::new(JsonHandler::new()),
"xml" => Box::new(XmlHandler::new()),
_ => Box::new(DefaultHandler::new()),
}
}
fn load_plugin(path: &str) -> Result<Box<dyn Plugin>, Error> {
// Load from dynamic library
}
pub fn create_database(config: &Config) -> Box<dyn Database> {
// Return implementation without exposing concrete type
}
Any trait for type inspection
use std::any::Any;
trait Component: Any {
fn as_any(&self) -> &dyn Any;
}
fn get_component<T: 'static>(component: &dyn Component) -> Option<&T> {
component.as_any().downcast_ref::<T>()
}
// Prefer generic function (monomorphization)
fn process<T: Processor>(processor: &T) {
processor.process(); // Direct call, no vtable
}
// Better: exhaustive enum
enum Handler {
Json(JsonHandler),
Xml(XmlHandler),
}
// Not: trait object with 2 implementations
// let handler: Box<dyn Handler> = ...;
Self return types
trait NotObjectSafe {
fn clone(&self) -> Self; // ❌ Returns Self
fn process<T>(&self, item: T); // ❌ Generic method
}
// Can't clone Box<dyn Trait> unless trait includes clone_box() method
// Prefer const generics or zero-sized types for compile-time dispatch
// ❌ ERROR: size cannot be known at compile time
fn broken(handler: Box<Handler>) {
// Compiler error: Handler doesn't have a size
}
// ✅ CORRECT: Use dyn keyword
fn correct(handler: Box<dyn Handler>) {
// Box<dyn Handler> is always 16 bytes (fat pointer)
}
// ❌ BAD: Trait with generic method
trait Processor {
fn process<T>(&self, item: T); // Not object-safe
}
// Compiler error when trying to use as trait object:
// let processor: Box<dyn Processor> = ...;
// ^^^^^^^^^^^^^ the trait `Processor` cannot be made into an object
// ✅ GOOD: Use associated types or remove generics
trait Processor {
type Item;
fn process(&self, item: Self::Item);
}
// ❌ BAD: Boxing when static dispatch would work
fn process_all(items: Vec<Box<dyn Item>>) {
for item in items {
item.process(); // Vtable lookup for each call
}
}
// ✅ GOOD: Use generics when all items are same type
fn process_all<T: Item>(items: Vec<T>) {
for item in items {
item.process(); // Direct call, no vtable
}
}
// ✅ ALSO GOOD: Trait objects only when truly needed
fn process_mixed(items: Vec<Box<dyn Item>>) {
// Only use when items have different concrete types
}
// Different pointer types for different use cases:
// &dyn - Borrowed, no ownership
fn read_only(logger: &dyn Logger) {
logger.log(LogLevel::Info, "message");
}
// Box<dyn> - Owned, single owner
fn take_ownership(logger: Box<dyn Logger>) {
// logger is dropped when function ends
}
// Arc<dyn> - Shared ownership, thread-safe
fn share_across_threads(logger: Arc<dyn Logger>) {
std::thread::spawn(move || {
logger.log(LogLevel::Info, "from thread");
});
}
// ❌ BAD: Unnecessary cloning via Box
fn bad_sharing(logger: Box<dyn Logger>) {
// Can't clone Box<dyn Logger> without explicit support
}
// ✅ GOOD: Use Arc for sharing
fn good_sharing(logger: Arc<dyn Logger>) {
let logger2 = Arc::clone(&logger);
// Both can use logger
}
// ❌ BAD: Designing trait without thinking about object safety
trait DataProcessor {
fn new() -> Self; // ❌ Not object-safe (no Self in trait objects)
fn process(&self, data: &[u8]) -> Self; // ❌ Returns Self
fn convert<T>(&self, value: T) -> String; // ❌ Generic method
}
// Later: "Oh no, I need trait objects but my trait isn't object-safe!"
// ✅ GOOD: Design with object safety in mind from the start
trait DataProcessor {
// Use Box<dyn> for factory pattern
fn create() -> Box<dyn DataProcessor> where Self: Sized;
// Return Box instead of Self
fn process(&self, data: &[u8]) -> Box<dyn DataProcessor>;
// Use concrete types or associated types instead of generics
fn convert(&self, value: &dyn Any) -> String;
}
use std::time::Instant;
trait Operation {
fn execute(&self, x: i32) -> i32;
}
struct AddOne;
impl Operation for AddOne {
fn execute(&self, x: i32) -> i32 { x + 1 }
}
struct MultiplyTwo;
impl Operation for MultiplyTwo {
fn execute(&self, x: i32) -> i32 { x * 2 }
}
// Static dispatch (monomorphization)
fn benchmark_static<T: Operation>(op: &T, iterations: usize) -> u128 {
let start = Instant::now();
let mut result = 0;
for i in 0..iterations {
result = op.execute(i as i32);
}
start.elapsed().as_nanos()
}
// Dynamic dispatch (trait objects)
fn benchmark_dynamic(op: &dyn Operation, iterations: usize) -> u128 {
let start = Instant::now();
let mut result = 0;
for i in 0..iterations {
result = op.execute(i as i32);
}
start.elapsed().as_nanos()
}
fn performance_comparison() {
let iterations = 10_000_000;
let add_one = AddOne;
// Static dispatch
let static_time = benchmark_static(&add_one, iterations);
println!("Static dispatch: {} ns", static_time);
// Dynamic dispatch
let dynamic_time = benchmark_dynamic(&add_one, iterations);
println!("Dynamic dispatch: {} ns", dynamic_time);
let overhead = (dynamic_time as f64 / static_time as f64 - 1.0) * 100.0;
println!("Overhead: {:.2}%", overhead);
// Typical results (optimized build):
// Static dispatch: 5,000,000 ns
// Dynamic dispatch: 15,000,000 ns
// Overhead: ~200% (but this is a microbenchmark)
// In real-world code with more complex operations,
// the overhead is typically 1-5% and often negligible
}
| Aspect | Static Dispatch | Dynamic Dispatch |
|--------|----------------|------------------|
| Method call overhead | Zero (inlined) | ~1-2ns (vtable lookup) |
| Code size | Larger (monomorphization) | Smaller (one impl) |
| Compile time | Slower (more code gen) | Faster |
| Optimization potential | High (inlining, devirtualization) | Limited (no inlining across trait boundary) |
| Memory layout | Concrete type size | 16 bytes (fat pointer) |
| Cache effects | Better (direct calls) | Worse (indirect jump) |
When overhead matters:Create a plugin system for a text editor with the following requirements:
// Your task: Implement this trait and create 3 plugins
trait EditorPlugin: Send + Sync {
fn name(&self) -> &str;
fn on_save(&self, content: &str) -> Result<String, String>;
fn on_load(&self, content: &str) -> Result<String, String>;
}
// TODO: Create these plugins:
// 1. MarkdownPlugin - converts markdown to HTML on save
// 2. WhitespacePlugin - trims trailing whitespace
// 3. EncryptionPlugin - encrypts content on save, decrypts on load
// TODO: Implement PluginManager
struct PluginManager {
plugins: Vec<Box<dyn EditorPlugin>>,
}
impl PluginManager {
fn register(&mut self, plugin: Box<dyn EditorPlugin>) {
// Your code here
}
fn process_save(&self, content: &str) -> Result<String, String> {
// Apply all plugins' on_save in order
todo!()
}
fn process_load(&self, content: &str) -> Result<String, String> {
// Apply all plugins' on_load in reverse order
todo!()
}
}
Expected output:
let mut manager = PluginManager::new();
manager.register(Box::new(WhitespacePlugin));
manager.register(Box::new(MarkdownPlugin));
let content = "# Hello World \n";
let saved = manager.process_save(content).unwrap();
assert_eq!(saved, "<h1>Hello World</h1>"); // Whitespace trimmed, markdown converted
Implement a flexible middleware system for request processing:
trait Middleware: Send + Sync {
fn process(&self, request: Request, next: &dyn Fn(Request) -> Response) -> Response;
}
struct Request {
path: String,
headers: std::collections::HashMap<String, String>,
body: Vec<u8>,
}
struct Response {
status: u16,
body: Vec<u8>,
}
// TODO: Implement MiddlewareChain
struct MiddlewareChain {
middlewares: Vec<Box<dyn Middleware>>,
}
impl MiddlewareChain {
fn add(&mut self, middleware: Box<dyn Middleware>) {
// Your code here
}
fn execute(&self, request: Request, handler: impl Fn(Request) -> Response) -> Response {
// Execute middlewares in order, each calling next()
// Hint: Use recursion or fold to build the chain
todo!()
}
}
// TODO: Implement these middleware:
// 1. LoggingMiddleware - logs request/response
// 2. AuthMiddleware - checks authorization header
// 3. CompressionMiddleware - compresses response body
// 4. RateLimitMiddleware - limits requests per second
Expected behavior:
Request → RateLimit → Auth → Logging → Handler → Logging → Compression → Response
Build a connection handler that negotiates protocols and allows downcasting:
use std::any::Any;
trait Protocol: Send + Sync {
fn name(&self) -> &str;
fn parse(&self, data: &[u8]) -> Option<Message>;
fn as_any(&self) -> &dyn Any; // For downcasting
}
struct Message {
headers: std::collections::HashMap<String, String>,
body: Vec<u8>,
}
// TODO: Implement HTTP, WebSocket, and custom protocol
struct HttpProtocol {
version: String,
}
struct WebSocketProtocol {
compression_enabled: bool,
}
// TODO: Implement protocol negotiation
fn negotiate_protocol(initial_bytes: &[u8]) -> Box<dyn Protocol> {
// Detect protocol from initial bytes
todo!()
}
// TODO: Implement typed protocol extraction
fn get_http_protocol(protocol: &dyn Protocol) -> Option<&HttpProtocol> {
// Downcast to concrete HttpProtocol
protocol.as_any().downcast_ref::<HttpProtocol>()
}
// TODO: Implement connection that can upgrade protocols
struct Connection {
protocol: Box<dyn Protocol>,
}
impl Connection {
fn upgrade_protocol(&mut self, new_protocol: Box<dyn Protocol>) {
// Switch from HTTP to WebSocket, for example
todo!()
}
fn handle_message(&self, data: &[u8]) -> Option<Message> {
// Use current protocol to parse
self.protocol.parse(data)
}
}
Test cases:
// Test 1: Protocol detection
let http_data = b"GET / HTTP/1.1\r\n";
let protocol = negotiate_protocol(http_data);
assert_eq!(protocol.name(), "HTTP");
// Test 2: Downcasting
if let Some(http) = get_http_protocol(protocol.as_ref()) {
assert_eq!(http.version, "1.1");
}
// Test 3: Protocol upgrade
let mut conn = Connection::new(http_protocol);
conn.upgrade_protocol(websocket_protocol);
assert_eq!(conn.protocol.name(), "WebSocket");
use std::error::Error;
use std::fmt;
// Error trait is designed to be object-safe
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
// Note: No Clone, no generic methods, no Self returns
}
// Common pattern: Box<dyn Error> for error propagation
fn may_fail() -> Result<String, Box<dyn Error>> {
let file = std::fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&file)?;
Ok(config.setting)
}
// Works with any error type implementing Error trait
fn handle_any_error(error: &dyn Error) {
eprintln!("Error: {}", error);
// Walk the error chain
let mut source = error.source();
while let Some(err) = source {
eprintln!("Caused by: {}", err);
source = err.source();
}
}
// actix-web uses trait objects for route handlers
use actix_web::{HttpRequest, HttpResponse};
trait Handler: Send + Sync {
fn call(&self, req: HttpRequest) -> HttpResponse;
}
// Allows different handler types
struct JsonHandler;
impl Handler for JsonHandler {
fn call(&self, req: HttpRequest) -> HttpResponse {
HttpResponse::Ok().json(ResponseData { status: "ok" })
}
}
// Router stores handlers as trait objects
struct Router {
routes: HashMap<String, Box<dyn Handler>>,
}
// Serde uses trait objects for format-agnostic serialization
use serde::Serializer;
fn serialize_to_any_format<T: Serialize>(
value: &T,
serializer: &mut dyn Serializer,
) -> Result<(), Error> {
value.serialize(serializer)
}
// Works with JSON, TOML, MessagePack, etc.
let mut json_serializer = serde_json::Serializer::new(writer);
serialize_to_any_format(&data, &mut json_serializer)?;
// Before async fn in traits, trait objects with async required workarounds
use async_trait::async_trait;
#[async_trait]
trait AsyncHandler {
async fn handle(&self, req: Request) -> Response;
}
// async_trait macro transforms async fn into Box<dyn Future>
// Making it object-safe by returning a boxed future
// Modern Rust (1.75+) supports async fn in traits natively
// but still requires trait objects for dynamic dispatch
trait AsyncHandler {
async fn handle(&self, req: Request) -> Response;
}
// Use with trait objects:
let handler: Box<dyn AsyncHandler> = Box::new(MyHandler);
handler.handle(request).await;
For systems programmers who want to understand the low-level details:
// Conceptual representation of what the compiler generates
// Trait definition
trait Draw {
fn draw(&self);
fn area(&self) -> f64;
}
// Concrete type
struct Circle {
radius: f64,
}
impl Draw for Circle {
fn draw(&self) { println!("Circle"); }
fn area(&self) -> f64 { 3.14 * self.radius * self.radius }
}
// What the compiler generates (pseudo-code):
// VTable structure
struct VTable_Circle_Draw {
drop_in_place: fn(*mut Circle),
size: usize, // 8
align: usize, // 8
draw: fn(*const Circle),
area: fn(*const Circle) -> f64,
}
// Static vtable instance (one per type-trait pair)
static VTABLE_CIRCLE_DRAW: VTable_Circle_Draw = VTable_Circle_Draw {
drop_in_place: std::ptr::drop_in_place::<Circle>,
size: 8,
align: 8,
draw: <Circle as Draw>::draw,
area: <Circle as Draw>::area,
};
// Fat pointer representation
struct TraitObject_Draw {
data: *const (), // Pointer to Circle instance
vtable: *const VTable_Circle_Draw,
}
// Method call translation:
// trait_object.draw() becomes:
// (trait_object.vtable.draw)(trait_object.data)
// Concrete example:
let circle = Circle { radius: 5.0 };
let trait_obj: &dyn Draw = &circle;
// trait_obj is actually:
// TraitObject_Draw {
// data: &circle as *const () as *const Circle,
// vtable: &VTABLE_CIRCLE_DRAW,
// }
// Calling trait_obj.draw() compiles to:
// ((*trait_obj.vtable).draw)(trait_obj.data)
Memory layout visualization:
Stack: Heap:
+-----------------+ +-----------------+
| trait_obj | | Circle |
| +-------------+ | | radius: 5.0 |
| | data ------|-|---->+-----------------+
| +-------------+ |
| | vtable ----|-|----> Static VTable:
| +-------------+ | +---------------------------+
+-----------------+ | drop_in_place: 0x1000 |
| size: 8 |
| align: 8 |
| draw: 0x2000 |
| area: 0x3000 |
+---------------------------+
---
Summary: Trait objects provide runtime polymorphism through dynamic dispatch, enabling heterogeneous collections, plugin systems, and flexible abstractions. They come with a small performance cost (vtable indirection) but offer crucial flexibility when types must be determined at runtime. Understanding object safety, memory layout, and performance characteristics is essential for systems programmers building production Rust applications.Run this code in the official Rust Playground