The universal adapter that refuses to work
You are building a plugin system. You have a Plugin trait that defines how plugins behave. You want to load plugins at runtime and store them in a collection. You write Vec<Box<dyn Plugin>>. The compiler rejects you with E0038: the trait cannot be made into an object.
You look at the trait. It has methods. It has self. It looks perfectly normal. You added a method fn process<T>(&self, data: T) because you wanted the plugin to handle any data type. You assumed Rust would figure it out. Rust disagrees. The error message points to the trait and tells you it is not object-safe.
This error is not a bug. It is the compiler enforcing a hard limit on how dynamic dispatch works. A trait object is a runtime abstraction. It requires the compiler to build a fixed-size lookup table for method calls. If the trait has generic parameters or methods that take self by value, the compiler cannot build that table. E0038 is the compiler saying, "I cannot create a universal adapter for this trait because the rules are too flexible."
What object safety really means
A trait object lets you treat different types as the same thing at runtime. When you write &dyn Trait, you get a fat pointer. The fat pointer contains two parts: a pointer to the actual data and a pointer to a vtable. The vtable is a lookup table generated by the compiler. It holds function pointers for every method in the trait. When you call a method on the trait object, the code jumps to the vtable, finds the right function pointer, and executes it.
For this to work, the vtable must have a fixed size. The compiler must know exactly how many function pointers to store. The compiler must also know how to call every function in the table. If the trait has generic type parameters, the size of the vtable becomes unknown. If a method takes self by value, the compiler cannot generate a call instruction because it does not know how many bytes to pass.
Object safety is the set of rules that guarantee the compiler can build a vtable. A trait is object-safe if it meets several conditions. The trait cannot have generic type parameters. Methods cannot have generic type parameters. Methods cannot return Self. Methods cannot take self by value. The trait cannot require Self: Sized by default.
If any of these rules are broken, the trait is not object-safe. You cannot use dyn Trait. You get E0038.
The minimal crash
Here is the smallest code that triggers the error. The trait has a generic method. The compiler cannot put a generic method in the vtable.
// The trait has a generic method.
// This breaks object safety.
trait Processor {
fn process<T>(&self, value: T);
}
struct MyProcessor;
impl Processor for MyProcessor {
fn process<T>(&self, _value: T) {
// Implementation doesn't matter for the error.
}
}
fn main() {
// E0038: the trait `Processor` cannot be made into an object
let obj: &dyn Processor = &MyProcessor;
}
The error occurs at the &dyn Processor line. The compiler tries to build a vtable for Processor. It sees process<T>. It stops. It cannot generate code for process<i32> and process<String> and process<f64> all at once. The vtable would need infinite space. The compiler rejects the code.
Why generics break the vtable
Generics in Rust use monomorphization. When you write a generic function, the compiler creates a separate copy of the function for every type you use. If you call process(5) and process("hello"), the compiler generates process<i32> and process<&str>. These are two different functions in the binary. They have different machine code.
Dynamic dispatch works differently. A trait object has one vtable. The vtable holds one function pointer per method. When you call obj.process(5), the code looks up the pointer in the vtable and jumps to it. There is no monomorphization at the call site. The vtable must contain the code for the method.
If the method is generic, which version goes in the vtable? The vtable cannot hold process<i32> and process<&str> because the set of types is unknown. The vtable size must be fixed at compile time. Generics require the vtable to grow based on runtime usage. These two models are incompatible.
The compiler cannot make a trait object for a trait with generic methods. You must remove the generics or move them elsewhere.
Why self by value is impossible
Another common cause of E0038 is a method that takes self by value.
trait Consumer {
fn consume(self);
}
fn main() {
// E0038: the trait `Consumer` cannot be made into an object
let obj: Box<dyn Consumer> = Box::new(MyType);
}
This fails because of how arguments are passed. When a function takes self by value, the caller must move the data into the function. The caller needs to know the size of the data to copy it or move it.
With a trait object, the size of the data is unknown at the call site. The fat pointer points to heap data of dynamic size. The vtable knows the size, but the compiler generating the call instruction does not. The call instruction needs a fixed size to pass the argument. The compiler cannot generate a call that moves a value of unknown size.
Even if the vtable contains the size, the machine code for the function call must be generated at compile time. The instruction cannot say "move unknown bytes." It must say "move 8 bytes" or "move 16 bytes." Since the size varies per type, the compiler cannot emit the instruction. Methods that take self by value are forbidden in trait objects.
Fixing the trait
You have three main ways to fix E0038. Choose based on what you are trying to do.
Remove generics and use concrete types
If the generic parameter is not essential, replace it with a concrete type. This is the simplest fix. The trait becomes object-safe because the method signature is fixed.
// The method uses a concrete type.
// The trait is now object-safe.
trait ObjectSafeProcessor {
fn process(&self, value: String);
}
struct TextProcessor;
impl ObjectSafeProcessor for TextProcessor {
fn process(&self, value: String) {
println!("Processing: {}", value);
}
}
fn main() {
// This compiles. The vtable holds one function pointer.
let obj: &dyn ObjectSafeProcessor = &TextProcessor;
obj.process("Hello".to_string());
}
This works when you only need to handle one type of data. If you need to handle multiple types, you might need a different approach.
Move generics to the function
If you need generics, keep the trait simple and make the function generic. This uses monomorphization instead of dynamic dispatch. The caller knows the concrete type, so the compiler can generate the right code.
// The trait has no generics.
// It is object-safe.
trait SimpleProcessor {
fn process(&self, value: String);
}
struct MyProcessor;
impl SimpleProcessor for MyProcessor {
fn process(&self, value: String) {
println!("Got: {}", value);
}
}
// The function is generic.
// It works with any type that implements the trait.
fn run_processor<T: SimpleProcessor>(proc: &T, data: String) {
proc.process(data);
}
fn main() {
let proc = MyProcessor;
// The compiler generates run_processor<MyProcessor>.
run_processor(&proc, "Data".to_string());
}
This approach gives you the flexibility of generics without breaking object safety. The trait remains usable as a trait object if you need it elsewhere. The generic function provides the polymorphism you wanted.
Use where Self: Sized for specific methods
Sometimes you want a trait to be object-safe, but one method needs Self or generics. You can exclude that method from the vtable by adding a where Self: Sized bound. This tells the compiler, "This method only works when the size is known. Do not put it in the vtable."
trait MixedTrait {
// This method goes in the vtable.
fn dynamic_method(&self);
// This method is excluded from the vtable.
// It can only be called on concrete types.
fn static_method(&self) where Self: Sized;
}
struct MyType;
impl MixedTrait for MyType {
fn dynamic_method(&self) {
println!("Dynamic");
}
fn static_method(&self) {
println!("Static");
}
}
fn main() {
// Trait object works for dynamic_method.
let obj: &dyn MixedTrait = &MyType;
obj.dynamic_method();
// static_method is not available on the trait object.
// obj.static_method(); // Error: method not found
// Call static_method on the concrete type.
MyType.static_method();
}
This pattern is useful for traits that need both dynamic and static dispatch. The where Self: Sized bound keeps the trait object-safe while allowing methods that require knowing the exact type.
Pitfalls and compiler errors
E0038 is the primary error, but you may see related messages. The compiler often says "the trait is not object safe" and lists the reasons. Common reasons include generic type parameters on the trait, methods with generic parameters, methods returning Self, and methods taking self by value.
A subtle pitfall is returning Self. If a method returns Self, the compiler cannot put it in the vtable because the return type changes per implementation. The vtable needs a fixed return type. You can fix this by returning a concrete type or using where Self: Sized.
Another pitfall is assuming Box<dyn Trait> works if &dyn Trait fails. Both use the same vtable. If the trait is not object-safe, neither works. The error is about the trait, not the wrapper.
Convention aside: always write dyn Trait explicitly. The dyn keyword signals dynamic dispatch. It makes the intent clear. Older Rust code sometimes omitted dyn, but modern Rust requires it. Writing dyn helps readers understand that the code uses runtime polymorphism.
Decision matrix
Use dyn Trait when you need to store heterogeneous collections of types that implement the trait, and the trait is object-safe. Use generic functions when you want monomorphization and zero-cost abstraction, and the caller knows the concrete type. Use concrete types when performance is paramount and you do not need polymorphism. Use where Self: Sized on specific methods when the trait needs both dynamic dispatch for some methods and static dispatch for others that require knowing the exact type.
The vtable is a fixed-size table. Generics require an infinite table. They do not mix. If you need generics, move them to the function. Keep the trait simple. Trust E0038. It forces you to choose between static and dynamic dispatch explicitly.