August 13, 2025•3 min
Making Traits Object-Safe for Rust's dyn Trait in Plugin Systems
m
mayoTable of Contents
Rust requires traits to be object-safe to use with dyn Trait
for dynamic dispatch, as this ensures a consistent vtable (virtual table) for runtime method calls. Non-object-safe traits, such as those with generic methods or static requirements, cannot be used with dyn Trait
, but they can be refactored for plugin systems needing runtime polymorphism. I’ll explain why object safety is necessary and demonstrate how to refactor a non-object-safe trait for a plugin system.
Why Object Safety Matters
A trait is object-safe if:
- All methods have a receiver (
&self
,&mut self
) or no receiver, but not static. - Methods do not use
Self
as a return type or generic parameter (except inwhere
clauses). - Methods are not generic (no
<T>
parameters).
dyn Trait
uses a fat pointer (data pointer + vtable pointer) to call methods at runtime. Non-object-safe traits prevent vtable construction because:
- Generic Methods: Different type parameters create varying method signatures, making a single vtable impossible.
- Self Returns: The size and type of
Self
differ per implementor, breaking vtable uniformity. - Static Methods: These lack an instance to dispatch on, so they don’t fit in a vtable.
Example: Non-Object-Safe Trait
Consider a plugin system for data transformers:
trait Transformer {
fn transform<T: Into<f64>>(&self, value: T) -> f64; // Generic method
fn new() -> Self; // Static, returns Self
}
struct SquareTransformer;
impl Transformer for SquareTransformer {
fn transform<T: Into<f64>>(&self, value: T) -> f64 {
let v = value.into();
v * v
}
fn new() -> Self { SquareTransformer }
}
// Fails: Trait isn’t object-safe
// let transformer: Box<dyn Transformer> = Box::new(SquareTransformer);
Problems:
transform<T>
: Generic, requiring a unique vtable entry perT
.new()
: Static withSelf
return, varying by implementor and lacking a receiver.
Refactored: Object-Safe Version
To enable dyn Trait
for a plugin system:
trait Transformer {
fn transform(&self, value: f64) -> f64; // No generics, fixed type
}
struct SquareTransformer;
impl Transformer for SquareTransformer {
fn transform(&self, value: f64) -> f64 {
value * value
}
}
// Factory function for instantiation
fn create_square_transformer() -> Box<dyn Transformer> {
Box::new(SquareTransformer)
}
// Usage in plugin system
fn main() {
let transformer: Box<dyn Transformer> = create_square_transformer();
let result = transformer.transform(3.0); // 9.0
}
Changes Made
- Removed Generics: Changed
transform<T: Into<f64>>
totransform(&self, value: f64)
. The vtable now has a single, fixed entry:fn(&self, f64) -> f64
.- Trade-off: Less flexible (only
f64
, noti32
orf32
), but plugins can convert inputs externally.
- Trade-off: Less flexible (only
- Dropped Static Method: Removed
new() -> Self
. Static methods don’t belong in vtables.- Solution: Added a factory function (
create_square_transformer
) for instantiation. A plugin loader could use a registry:use std::collections::HashMap; let mut plugins: HashMap<String, fn() -> Box<dyn Transformer>> = HashMap::new(); plugins.insert("square".to_string(), create_square_transformer);
- Solution: Added a factory function (
How It Enables dyn Trait
- Vtable Construction: The refactored
Transformer
has one method with a fixed signature, enabling a vtable like:
A// Conceptual vtable struct TransformerVtable { transform: fn(*const (), f64) -> f64, // Pointer to SquareTransformer::transform }
Box<dyn Transformer>
pairs this vtable with the instance for runtime calls. - Safety: No generics or
Self
ensure the vtable is type-agnostic, safe for any implementor. - Efficiency: Dynamic dispatch adds a vtable lookup (1-2 cycles), but enables
Back to Blog
Share::