From 87e946ec7f4d15b20882c5f522a70075e22852a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Nov 2025 13:58:33 +0100 Subject: [PATCH 01/24] Fixing Dyn Compatibility in Rust Article --- content/blog/dyn-compatibility/index.md | 345 ++++++++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 content/blog/dyn-compatibility/index.md diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md new file mode 100644 index 00000000..dba3ec02 --- /dev/null +++ b/content/blog/dyn-compatibility/index.md @@ -0,0 +1,345 @@ ++++ +title = "Fixing Dyn Compatibility Issues in Rust" +date = 2025-11-18 +draft = false +template = "article.html" +[extra] +series = "Idiomatic Rust" +resources = [ +"[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", +"[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", +"[RFC 255: Object Safety](https://rust-lang.github.io/rfcs/0255-object-safety.html)", +"[RFC 817: Where Self Meets Sized](https://rust-lang.github.io/rfcs/0817-dyn-compatibility.html)" +] ++++ + +In Rust, not all traits can be used with `dyn Trait`. + +That is, you can't make "trait objects" from them. +This is called *dyn compatibility*, which means "a trait is not compatible with dynamic dispatch via `dyn`." + +Let's figure out the problem and how to fix it! + +**Note**: This concept used to be called "object safety" until Rust 1.84.0. If you're reading older resources, they mean the same thing. + +## The Problem + +Here's an example with code that **won't compile**: + +```rust +trait Widget { + fn draw(&self); + fn duplicate(&self) -> Self; // Returns a copy of itself +} + +struct Button { + label: String, +} + +impl Widget for Button { + fn draw(&self) { + println!("Button: {}", self.label); + } + + fn duplicate(&self) -> Self { + Button { label: self.label.clone() } + } +} + +fn main() { + let button = Button { label: "Click me".to_string() }; + let widget: &dyn Widget = &button; // Error! +} +``` + +If you tried to compile this code, you'd get an error like this: + +``` +error[E0038]: the trait `Widget` cannot be made into an object + --> src/main.rs:18:17 + | +18 | let widget: &dyn Widget = &button; + | ^^^^^^^^^^^ `Widget` is not dyn compatible + | + = note: method `duplicate` references the `Self` type in its return type +``` + +That might sound pretty confusing in the beginning. + +- What does "cannot be made into an object" even mean? +- Shouldn't the `dyn` part take care of that? +- What does it have to do with `Self`? + +You've just encountered **dyn compatibility**. + +## What's going on? + +When you use `&dyn Trait`, Rust creates a **trait object**. +Trait objects use **dynamic dispatch** to call methods at runtime. +Dynamic dispatch means that the exact method to call is determined at runtime based on the actual type of the object. + +However, for dynamic dispatch to work, you must follow certain rules. + +1. The trait must not have any methods that return `Self`. +2. The trait must not have any static methods (methods without a `self` parameter). +3. The trait must not have any generic type parameters on its methods. + +In our example, the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". +When you use `&dyn Widget`, the compiler doesn't know what `Self` is at runtime because it could be any type that implements `Widget`. +That's a problem, because the compiler needs to know the size of the return type at compile time, and `Self` could be **any size**. + +It will become clearer once we look at some fixes. + + +## How To Fix It + +Don't worry, we won't have to refactor all our code! +All fixes use the same `Widget` trait example. +There are multiple ways to make it dyn compatible. + +### Fix #1: Use Generics Instead + +One common way to fix the problem is to use generics instead of trait objects. +Generics resolve to concrete types *at compile time*, so the compiler learns about the size of `Self`. +Basically, the compiler will generate a separate version of the function for each type that implements the trait. Then at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). +The compiler always knows which type it is dealing with, so it can pick the right method to call. + +Our trait stays the same: + +```rust +trait Widget { + fn draw(&self); + fn duplicate(&self) -> Self; +} +``` + +But now we change the function which uses the trait to use generics instead of `dyn`: + +```rust +// Instead of: fn show_widget(widget: &dyn Widget) +// Use generics: +fn show_widget(widget: &W) { + widget.draw(); + let copy = widget.duplicate(); + copy.draw(); +} +``` + +Note how we changed the function signature to use a generic type parameter `W` that implements the `Widget` trait. +Here we tell Rust: "I have some type `W` that implements `Widget`, and I want to use it." and Rust will happily generate all the necessary code for each type used. + +That is similar but slightly different from using `&dyn Widget`. +The difference is that with generics, the compiler knows the concrete type at compile time, so it can handle `Self` correctly. +For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. +Now the confusion about what `Self` means is gone! + +### Fix #2: Opt Out Problem Methods with `where Self: Sized` + +Another option is to keep using trait objects but change the problematic method to only work with concrete types. + +```rust +trait Widget { + fn draw(&self); + + // Only available when the concrete type is known + fn duplicate(&self) -> Self where Self: Sized; +} +``` + +The above says "this method can only be called when `Self` has a known size at compile time", which is true for concrete types but not for trait objects. +You won't be able to call `duplicate` on `&dyn Widget`, but you can still call it on concrete types like `Button`. + +```rust +fn main() { + let button = Button { label: "Click me".to_string() }; + + // Can use as trait object now! + let widget: &dyn Widget = &button; + widget.draw(); // ✅ Works + + // ❌ Can't call this on trait objects + // widget.duplicate(); + + // ✅ But duplicate still works on concrete types: + let button2 = button.duplicate(); +} +``` + +### Fix #3: Return Boxed Trait Objects Instead of `Self` + +In this case, we can change the return type of the problematic method to return a boxed trait object instead of `Self`. + +This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), +so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. + +```rust +trait Widget { + fn draw(&self); + fn duplicate(&self) -> Box; // Returns trait object instead of Self +} +``` + +```rust +struct Button { + label: String, +} + +impl Widget for Button { + fn draw(&self) { + println!("Button: {}", self.label); + } + + fn duplicate(&self) -> Box { + Box::new(Button { label: self.label.clone() }) + } +} + +fn main() { + // Now we can use trait objects! + let widgets: Vec> = vec![ + Box::new(Button { label: "Click me".to_string() }), + Box::new(Button { label: "Submit".to_string() }), + ]; + + for widget in &widgets { + widget.draw(); + let copy = widget.duplicate(); + copy.draw(); + } +} +``` + +### Fix #4: Split Into Two Traits + +Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. + +Maybe your code is silently trying to tell you that you are mixing up two different concepts and that they should be untangled. + +In general, prefer smaller, focused traits over large, monolithic ones. +Traits are not interfaces! + +It's a bit of a silly example, but perhaps you have a `Widget` trait for drawing and a `Duplicatable` trait for duplication. + +```rust +// This trait that can be used with dyn +trait Widget { + fn draw(&self); +} + +// Separate trait for duplication, which can't be used with dyn +trait Duplicatable: Widget { + fn duplicate(&self) -> Self where Self: Sized; +} + +struct Button { + label: String, +} + +impl Widget for Button { + fn draw(&self) { + println!("Button: {}", self.label); + } +} + +impl Duplicatable for Button { + fn duplicate(&self) -> Self { + Button { label: self.label.clone() } + } +} + +fn main() { + let button = Button { label: "Click me".to_string() }; + + // Use as trait object for drawing + let widget: &dyn Widget = &button; + widget.draw(); // ✅ Works + + // Use concrete type for duplication + let button2 = button.duplicate(); // ✅ Works +} +``` + +## Understanding the Rules + +A trait is **dyn compatible** if it follows these rules: + +- No `Self: Sized` Supertrait, i.e. the trait itself must not require `Self: Sized` +- Methods Must Have a Receiver, e.g. `&self` or `&mut self` +- No Generic Type Parameters on Methods, e.g. `fn process(&self, item: T);` + The reason is that the vtable is a static struct created at compile time. It can't have infinite entries. Generic methods are **monomorphized** at compile time (one copy per type), but trait objects work at **runtime** when the type is erased. +- No `Self` in Method Parameters (Except Receiver), e.g. no `fn compare(&self, other: &Self);` + That's because `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! +- No `Self` Return Type + The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased. +- No `impl Trait` in Return Position + +That's quite a lot of exceptions, but they all boil down to the same core issue: **the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** + +To understand why that is so important, we have to look at how trait objects work under the hood. + +### What is a Trait Object? + +When you write `&dyn Trait`, you're creating a **trait object**. +It's a special kind of value that consists of two pointers (a "fat pointer"): + +``` +┌─────────────────┐ +│ Data Pointer │ ──→ points to actual data (String, i32, etc.) +├─────────────────┤ +│ VTable Pointer │ ──→ points to virtual method table +└─────────────────┘ +``` + +As you can see, a trait object has: +1. A **data pointer** that points to the actual data (the concrete type implementing the trait) +2. A **vtable pointer** that points to a table of function pointers for the methods + +The vtable is created at compile time and contains pointers to the methods for the specific type. +When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. + +In order to create a vtable, the compiler needs to know: +- The size of the type (to allocate memory) +- The exact method signatures (to create function pointers) + +If a trait has methods that return `Self` or have generic parameters, the compiler can't create a proper vtable because it doesn't know what `Self` is or how to handle generics at runtime. + +That is the root cause of dyn compatibility issues. + +## Summary + +**Dyn compatibility** determines if a trait can be used with `dyn Trait`. The rules exist because: + +1. Trait objects use dynamic dispatch via vtables +2. Vtables are static, compile-time structures, which hold method pointers +3. Type information is erased at runtime in order to allow polymorphism +4. The compiler must guarantee type safety at all times, even if it can't see the concrete type + +Many standard library traits (Clone, Iterator, etc.) are also not dyn compatible, so don't worry. +As seen above, you can work around limitations with type erasure. + + + + +### When to use what + +- Use **generics** when you know types at compile time and want maximum performance +- Use **trait objects** when you need runtime polymorphism or heterogeneous collections + +Generics lead to larger binaries and longer compile times, but they are fast at runtime. +Trait objects lead to smaller binaries and faster compile times.They allow for maximum flexibility, but come with a small runtime cost due to dynamic dispatch. + +### A Personal Note on Terminology + +I personally don't like either of the terms "object safety" or "dyn compatibility" because they sound like some obscure technical jargon. +Certainly, "object safety" is misleading because Rust doesn't have "objects" in the traditional OOP sense because it lacks classes and inheritance. And it's not about "safety" either, because it's really about whether a trait can be used with dynamic dispatch. +"dyn compatibility" is better, but you have to know a lot of Rust jargon to understand what's going on. +But to be honest, I also can't think of a better name that is both short and accurate. + +### Historical Notes + +- RFC 255: Introduced object safety (2014, before Rust 1.0) +- RFC 546: Removed implied `Sized` bound on traits +- RFC 428: Fixed edge cases in object safety +- RFC 817: Added `where Self: Sized` for fine-grained control +- Rust 1.72: GATs can be opted out with `where Self: Sized` +- Rust 1.84.0: Renamed "object safety" to "dyn compatibility" From f6e326bcc18cb303b3f8a90c638cac12390ee931 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Nov 2025 14:02:15 +0100 Subject: [PATCH 02/24] Fix typos and wording --- content/blog/dyn-compatibility/index.md | 152 ++++++++++++++---------- 1 file changed, 90 insertions(+), 62 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index dba3ec02..80bdc0c6 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,5 +1,5 @@ +++ -title = "Fixing Dyn Compatibility Issues in Rust" +title = "Understanding Dyn Compatibility" date = 2025-11-18 draft = false template = "article.html" @@ -9,18 +9,40 @@ resources = [ "[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", "[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", "[RFC 255: Object Safety](https://rust-lang.github.io/rfcs/0255-object-safety.html)", -"[RFC 817: Where Self Meets Sized](https://rust-lang.github.io/rfcs/0817-dyn-compatibility.html)" ] +++ -In Rust, not all traits can be used with `dyn Trait`. +In Rust, not all traits can be used as trait objects with `dyn Trait`. -That is, you can't make "trait objects" from them. -This is called *dyn compatibility*, which means "a trait is not compatible with dynamic dispatch via `dyn`." +When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." -Let's figure out the problem and how to fix it! +This has an impact on how you can use these traits in your code. +Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -**Note**: This concept used to be called "object safety" until Rust 1.84.0. If you're reading older resources, they mean the same thing. +Once you understand why these rules exist, they stop feeling like compiler errors and start revealing design choices. +You'll see the tradeoffs between compile-time generics and runtime polymorphism, and +get a solid grasp of when each approach fits your problem. +Knowing your options lets you write more deliberate, flexible Rust. + +Let's figure out why this happens and how to fix it! + +{% info(title="Dyn Compatibility and Object Safety", icon="crab") %} + +This concept used to be called "object safety" until Rust 1.84.0. +If you're reading older resources, they mean the same thing. + +The name got changed because it was confusing. + +"Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. +The new term "dyn compatibility" does a better job at reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] + +[^personal_note]: I personally don't like either of the terms "object safety" or "dyn compatibility" because they sound like some obscure technical jargon. +Certainly, "object safety" is misleading because Rust doesn't have "objects" in the traditional OOP sense -- it lacks classes and inheritance. And it's not about "safety" either, since it's really about whether a trait can be used with dynamic dispatch. +"dyn compatibility" is better, but you have to know a lot of Rust jargon to understand what's going on. +But to be honest, I also can't think of a better name that is both short and accurate. + + +{% end %} ## The Problem @@ -46,9 +68,10 @@ impl Widget for Button { } } -fn main() { - let button = Button { label: "Click me".to_string() }; - let widget: &dyn Widget = &button; // Error! +fn show_widget(widget: &dyn Widget) { + widget.draw(); + let copy = widget.duplicate(); // ❌ Error here + copy.draw(); } ``` @@ -97,10 +120,20 @@ Don't worry, we won't have to refactor all our code! All fixes use the same `Widget` trait example. There are multiple ways to make it dyn compatible. +We have a bunch of options: + +1. Use Generics Instead +2. Opt Out Problem Methods with `where Self: Sized` +3. Return Boxed Trait Objects Instead of `Self` +4. Split Into Two Traits + +Let's look at each of these in detail. + + ### Fix #1: Use Generics Instead One common way to fix the problem is to use generics instead of trait objects. -Generics resolve to concrete types *at compile time*, so the compiler learns about the size of `Self`. +Generics resolve to concrete types *at compile time*, so the compiler knows the size of `Self`. Basically, the compiler will generate a separate version of the function for each type that implements the trait. Then at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). The compiler always knows which type it is dealing with, so it can pick the right method to call. @@ -133,6 +166,8 @@ The difference is that with generics, the compiler knows the concrete type at co For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! +The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. + ### Fix #2: Opt Out Problem Methods with `where Self: Sized` Another option is to keep using trait objects but change the problematic method to only work with concrete types. @@ -146,7 +181,10 @@ trait Widget { } ``` -The above says "this method can only be called when `Self` has a known size at compile time", which is true for concrete types but not for trait objects. +This means "this method can only be called when `Self` has a known size at compile time", which is true for concrete types but not for trait objects. +It is more explicit because you're in control over how the trait can be used. +The downside is that this limits the usability of trait further down the line because some methods won't be callable on all trait objects and changing the trait will cause breaking changes. + You won't be able to call `duplicate` on `&dyn Widget`, but you can still call it on concrete types like `Button`. ```rust @@ -165,9 +203,11 @@ fn main() { } ``` +This means you don't lose all the flexibility of trait objects (in contrast to generics), but you have to be aware that some methods won't be available when using `dyn Trait`. + ### Fix #3: Return Boxed Trait Objects Instead of `Self` -In this case, we can change the return type of the problematic method to return a boxed trait object instead of `Self`. +We can change the return type of the problematic method to return a boxed trait object instead of `Self`. This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. @@ -209,6 +249,9 @@ fn main() { } ``` +The downside is that `Box` is often viral in your codebase: +you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. + ### Fix #4: Split Into Two Traits Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. @@ -217,18 +260,19 @@ Maybe your code is silently trying to tell you that you are mixing up two differ In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! +Instead, we lean on composition and focus on behavior instead of mangling multiple responsibilities into a single trait. -It's a bit of a silly example, but perhaps you have a `Widget` trait for drawing and a `Duplicatable` trait for duplication. +Here's a more realistic example: separating rendering from widget creation. Factory methods are often static (no `self` parameter), which makes them incompatible with `dyn`. So we split them into separate traits. ```rust -// This trait that can be used with dyn +// This trait can be used with dyn trait Widget { fn draw(&self); } -// Separate trait for duplication, which can't be used with dyn -trait Duplicatable: Widget { - fn duplicate(&self) -> Self where Self: Sized; +// Separate trait for creating widgets - can't be used with dyn +trait WidgetFactory { + fn create(label: String) -> Self; // No self parameter! } struct Button { @@ -241,21 +285,22 @@ impl Widget for Button { } } -impl Duplicatable for Button { - fn duplicate(&self) -> Self { - Button { label: self.label.clone() } +impl WidgetFactory for Button { + fn create(label: String) -> Self { + Button { label } } } fn main() { - let button = Button { label: "Click me".to_string() }; - + // Use the factory to create widgets + let button = Button::create("Click me".to_string()); + // Use as trait object for drawing let widget: &dyn Widget = &button; widget.draw(); // ✅ Works - - // Use concrete type for duplication - let button2 = button.duplicate(); // ✅ Works + + // Can't do this: let factory: &dyn WidgetFactory = ... + // But that's fine - factories work at compile time } ``` @@ -263,30 +308,29 @@ fn main() { A trait is **dyn compatible** if it follows these rules: -- No `Self: Sized` Supertrait, i.e. the trait itself must not require `Self: Sized` -- Methods Must Have a Receiver, e.g. `&self` or `&mut self` -- No Generic Type Parameters on Methods, e.g. `fn process(&self, item: T);` - The reason is that the vtable is a static struct created at compile time. It can't have infinite entries. Generic methods are **monomorphized** at compile time (one copy per type), but trait objects work at **runtime** when the type is erased. -- No `Self` in Method Parameters (Except Receiver), e.g. no `fn compare(&self, other: &Self);` - That's because `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! -- No `Self` Return Type - The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased. -- No `impl Trait` in Return Position +| Rule | Why? | +|------|------| +| No `Self: Sized` supertrait | The trait itself must not require `Self: Sized`, otherwise it can never be used as a trait object | +| Methods must have a receiver | All methods need `&self`, `&mut self`, or similar. Static methods (no receiver) can't be called through a vtable | +| No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | +| No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | +| No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | +| No `impl Trait` in return position | Similar to `Self` - the actual type needs to be known at compile time, but it's erased with trait objects | That's quite a lot of exceptions, but they all boil down to the same core issue: **the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** To understand why that is so important, we have to look at how trait objects work under the hood. -### What is a Trait Object? +{% info(title="Side Quest: What are Trait Objects anyway?", icon="crab") %} When you write `&dyn Trait`, you're creating a **trait object**. It's a special kind of value that consists of two pointers (a "fat pointer"): ``` ┌─────────────────┐ -│ Data Pointer │ ──→ points to actual data (String, i32, etc.) +│ Data Pointer │ --> points to actual data (String, i32, etc.) ├─────────────────┤ -│ VTable Pointer │ ──→ points to virtual method table +│ VTable Pointer │ --> points to virtual method table └─────────────────┘ ``` @@ -305,6 +349,8 @@ If a trait has methods that return `Self` or have generic parameters, the compil That is the root cause of dyn compatibility issues. +{% end %} + ## Summary **Dyn compatibility** determines if a trait can be used with `dyn Trait`. The rules exist because: @@ -315,31 +361,13 @@ That is the root cause of dyn compatibility issues. 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type Many standard library traits (Clone, Iterator, etc.) are also not dyn compatible, so don't worry. -As seen above, you can work around limitations with type erasure. - - - - -### When to use what +As we've seen, you can work around these limitations with type erasure and other techniques. -- Use **generics** when you know types at compile time and want maximum performance -- Use **trait objects** when you need runtime polymorphism or heterogeneous collections - -Generics lead to larger binaries and longer compile times, but they are fast at runtime. -Trait objects lead to smaller binaries and faster compile times.They allow for maximum flexibility, but come with a small runtime cost due to dynamic dispatch. - -### A Personal Note on Terminology - -I personally don't like either of the terms "object safety" or "dyn compatibility" because they sound like some obscure technical jargon. -Certainly, "object safety" is misleading because Rust doesn't have "objects" in the traditional OOP sense because it lacks classes and inheritance. And it's not about "safety" either, because it's really about whether a trait can be used with dynamic dispatch. -"dyn compatibility" is better, but you have to know a lot of Rust jargon to understand what's going on. -But to be honest, I also can't think of a better name that is both short and accurate. ### Historical Notes -- RFC 255: Introduced object safety (2014, before Rust 1.0) -- RFC 546: Removed implied `Sized` bound on traits -- RFC 428: Fixed edge cases in object safety -- RFC 817: Added `where Self: Sized` for fine-grained control -- Rust 1.72: GATs can be opted out with `where Self: Sized` -- Rust 1.84.0: Renamed "object safety" to "dyn compatibility" +- [RFC 255](https://rust-lang.github.io/rfcs/0255-object-safety.html): Introduced object safety (2014, before Rust 1.0) +- [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html): Removed implied `Sized` bound on traits +- [Issue #428](https://github.com/rust-lang/rfcs/issues/428): Object-safety and static methods +- [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html): GATs can be opted out with `where Self: Sized` +- [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/): Renamed "object safety" to "dyn compatibility" (not mentioned in the release notes) From 6462e0a91ff1495cf71f1ee7a556de874a9f2b53 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Nov 2025 19:21:23 +0100 Subject: [PATCH 03/24] Wording; integrate Theo's feedback --- content/blog/dyn-compatibility/index.md | 62 ++++++++++++------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 80bdc0c6..2273102d 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,14 +1,16 @@ +++ title = "Understanding Dyn Compatibility" -date = 2025-11-18 +date = 2025-11-20 draft = false template = "article.html" [extra] series = "Idiomatic Rust" +reviews = [ + { name = "Theodor-Alexandru Irimia", url = "https://github.com/tirimia" }, +] resources = [ "[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", "[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", -"[RFC 255: Object Safety](https://rust-lang.github.io/rfcs/0255-object-safety.html)", ] +++ @@ -304,24 +306,7 @@ fn main() { } ``` -## Understanding the Rules - -A trait is **dyn compatible** if it follows these rules: - -| Rule | Why? | -|------|------| -| No `Self: Sized` supertrait | The trait itself must not require `Self: Sized`, otherwise it can never be used as a trait object | -| Methods must have a receiver | All methods need `&self`, `&mut self`, or similar. Static methods (no receiver) can't be called through a vtable | -| No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | -| No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | -| No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | -| No `impl Trait` in return position | Similar to `Self` - the actual type needs to be known at compile time, but it's erased with trait objects | - -That's quite a lot of exceptions, but they all boil down to the same core issue: **the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** - -To understand why that is so important, we have to look at how trait objects work under the hood. - -{% info(title="Side Quest: What are Trait Objects anyway?", icon="crab") %} +## What's Going On Under the Hood? When you write `&dyn Trait`, you're creating a **trait object**. It's a special kind of value that consists of two pointers (a "fat pointer"): @@ -338,10 +323,11 @@ As you can see, a trait object has: 1. A **data pointer** that points to the actual data (the concrete type implementing the trait) 2. A **vtable pointer** that points to a table of function pointers for the methods -The vtable is created at compile time and contains pointers to the methods for the specific type. +The [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) is created at compile time and contains pointers to the methods for the specific type. +It is a concept that is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. -In order to create a vtable, the compiler needs to know: +But in order to create a vtable, the compiler needs to know: - The size of the type (to allocate memory) - The exact method signatures (to create function pointers) @@ -349,7 +335,19 @@ If a trait has methods that return `Self` or have generic parameters, the compil That is the root cause of dyn compatibility issues. -{% end %} +In summary, a trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): + +| Rule | Why? | +|------|------| +| No `Self: Sized` supertrait | The trait itself must not require `Self: Sized`, otherwise it can never be used as a trait object | +| Methods must have a receiver | All methods need `&self`, `&mut self`, or similar. Static methods (no receiver) can't be called through a vtable | +| No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | +| No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | +| No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | +| No `impl Trait` in return position | Similar to `Self` - the actual type needs to be known at compile time, but it's erased with trait objects | + +That's quite a lot of rules, but they all boil down to the same core issue: +**the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** ## Summary @@ -360,14 +358,16 @@ That is the root cause of dyn compatibility issues. 3. Type information is erased at runtime in order to allow polymorphism 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type -Many standard library traits (Clone, Iterator, etc.) are also not dyn compatible, so don't worry. -As we've seen, you can work around these limitations with type erasure and other techniques. - +If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Iterator`, etc.) are also not dyn compatible. +As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. ### Historical Notes -- [RFC 255](https://rust-lang.github.io/rfcs/0255-object-safety.html): Introduced object safety (2014, before Rust 1.0) -- [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html): Removed implied `Sized` bound on traits -- [Issue #428](https://github.com/rust-lang/rfcs/issues/428): Object-safety and static methods -- [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html): GATs can be opted out with `where Self: Sized` -- [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/): Renamed "object safety" to "dyn compatibility" (not mentioned in the release notes) +I find it interesting to see how dyn compatibility evolved over time in Rust. +If you do, too, here are some resources to dig deeper: + +- 2014-09-22: [RFC 255](https://rust-lang.github.io/rfcs/0255-object-safety.html) - Introduced object safety (2014, before Rust 1.0) +- 2014-11-03: [Issue #428](https://github.com/rust-lang/rfcs/issues/428) - Object-safety and static methods +- 2015-01-03: [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html) - Removed implied `Sized` bound on traits +- 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` +- 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - Silently renamed "object safety" to "dyn compatibility" (tragically, not mentioned in the release notes!) From 33a45aae450d443ef8140ab6ddd2ee78bc1cd207 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Nov 2025 19:26:48 +0100 Subject: [PATCH 04/24] Add big honking footnote --- content/blog/dyn-compatibility/index.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 2273102d..279267ce 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -168,7 +168,18 @@ The difference is that with generics, the compiler knows the concrete type at co For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! -The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. +The downside is that you can't fully lean on dynamic dispatch anymore [^why-dynamic-dispatch] and that you might have to refactor a lot of code if you were using trait objects extensively before. + +[^why-dynamic-dispatch]: **"What's the benefit of fully leaning on dynamic dispatch"**, you ask? Fair question! + + Dynamic dispatch has a bunch of really nice properties: + + - It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. + - It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. + You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. + - It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. + For exmaple, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. + Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. ### Fix #2: Opt Out Problem Methods with `where Self: Sized` From 02f9d06731b33e187ec9baebdb9c31f21d3be325 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Nov 2025 12:29:58 +0100 Subject: [PATCH 05/24] Add resource --- content/blog/dyn-compatibility/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 279267ce..f2fb38b6 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,6 +1,6 @@ +++ title = "Understanding Dyn Compatibility" -date = 2025-11-20 +date = 2025-11-24 draft = false template = "article.html" [extra] @@ -11,6 +11,7 @@ reviews = [ resources = [ "[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", "[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", +"[Two Ways To Do Dynamic Dispatch](https://www.youtube.com/watch?v=wU8hQvU8aKM) - Video by Logan Smith, which explains dyn dispatch from first principles" ] +++ From 4e354227a28f1d56876a9c23e73a11107acf66df Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Dec 2025 19:41:25 +0100 Subject: [PATCH 06/24] wording --- content/blog/dyn-compatibility/index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index f2fb38b6..755b53f3 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -132,7 +132,6 @@ We have a bunch of options: Let's look at each of these in detail. - ### Fix #1: Use Generics Instead One common way to fix the problem is to use generics instead of trait objects. @@ -182,7 +181,7 @@ The downside is that you can't fully lean on dynamic dispatch anymore [^why-dyna For exmaple, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. -### Fix #2: Opt Out Problem Methods with `where Self: Sized` +### Fix #2: Opt Out Problematic Methods with `where Self: Sized` Another option is to keep using trait objects but change the problematic method to only work with concrete types. From b2a004c34d4897672c327514450ca10cce8363a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Dec 2025 19:43:49 +0100 Subject: [PATCH 07/24] formtting --- content/blog/dyn-compatibility/index.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 755b53f3..83223104 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -222,8 +222,7 @@ This means you don't lose all the flexibility of trait objects (in contrast to g We can change the return type of the problematic method to return a boxed trait object instead of `Self`. -This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), -so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. +This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. ```rust trait Widget { @@ -262,8 +261,7 @@ fn main() { } ``` -The downside is that `Box` is often viral in your codebase: -you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. +The downside is that `Box` is often viral in your codebase: you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. ### Fix #4: Split Into Two Traits From 51e5c41238fc14c0c847ae6fa16afcce8d6648c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Dec 2025 20:00:35 +0100 Subject: [PATCH 08/24] add note --- content/blog/dyn-compatibility/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 83223104..12cfeada 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -116,7 +116,6 @@ That's a problem, because the compiler needs to know the size of the return type It will become clearer once we look at some fixes. - ## How To Fix It Don't worry, we won't have to refactor all our code! @@ -131,6 +130,8 @@ We have a bunch of options: 4. Split Into Two Traits Let's look at each of these in detail. +Each approach comes with different tradeoffs. +Depending on the type of dyn-compatibility issue, one might be more suitable than the others or you might even need to use a combination of them. ### Fix #1: Use Generics Instead @@ -263,6 +264,9 @@ fn main() { The downside is that `Box` is often viral in your codebase: you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. +On top of that, this fix only works for methods that return `Self`. +If your trait also has static methods or generic methods, you'll need to combine this approach with one of the other fixes. + ### Fix #4: Split Into Two Traits Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. From 508d7f65292977d9065e617bee65febf454d9032 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 00:55:51 +0200 Subject: [PATCH 09/24] Fix inconsistent fix #2 wording in options list --- content/blog/dyn-compatibility/index.md | 51 ++++++++++++++----------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 12cfeada..bb7a14b8 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,6 +1,6 @@ +++ title = "Understanding Dyn Compatibility" -date = 2025-11-24 +date = 2026-06-17 draft = false template = "article.html" [extra] @@ -22,12 +22,10 @@ When a trait can't be used with dynamic dispatch, we say it's "not dyn compatibl This has an impact on how you can use these traits in your code. Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -Once you understand why these rules exist, they stop feeling like compiler errors and start revealing design choices. +Once you understand why these rules exist, they become tell-tale sign of your design choices. +Knowing your options lets you write more deliberate, flexible Rust. You'll see the tradeoffs between compile-time generics and runtime polymorphism, and get a solid grasp of when each approach fits your problem. -Knowing your options lets you write more deliberate, flexible Rust. - -Let's figure out why this happens and how to fix it! {% info(title="Dyn Compatibility and Object Safety", icon="crab") %} @@ -37,13 +35,9 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job at reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] - -[^personal_note]: I personally don't like either of the terms "object safety" or "dyn compatibility" because they sound like some obscure technical jargon. -Certainly, "object safety" is misleading because Rust doesn't have "objects" in the traditional OOP sense -- it lacks classes and inheritance. And it's not about "safety" either, since it's really about whether a trait can be used with dynamic dispatch. -"dyn compatibility" is better, but you have to know a lot of Rust jargon to understand what's going on. -But to be honest, I also can't think of a better name that is both short and accurate. +The new term "dyn compatibility" does a better job at reflecting that it's about whether a trait can be used with `dyn Trait` in the context of dynamic dispatch. [^personal_note] +[^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. {% end %} @@ -72,8 +66,8 @@ impl Widget for Button { } fn show_widget(widget: &dyn Widget) { - widget.draw(); - let copy = widget.duplicate(); // ❌ Error here + widget.draw(); // Works + let copy = widget.duplicate(); // Error copy.draw(); } ``` @@ -81,19 +75,30 @@ fn show_widget(widget: &dyn Widget) { If you tried to compile this code, you'd get an error like this: ``` -error[E0038]: the trait `Widget` cannot be made into an object - --> src/main.rs:18:17 +error[E0038]: the trait `Widget` is not dyn compatible + --> src/main.rs:20:25 + | +20 | fn show_widget(widget: &dyn Widget) { + | ^^^^^^^^^^ `Widget` is not dyn compatible | -18 | let widget: &dyn Widget = &button; - | ^^^^^^^^^^^ `Widget` is not dyn compatible +note: for a trait to be dyn compatible it needs to allow building a vtable + for more information, visit + --> src/main.rs:3:28 | - = note: method `duplicate` references the `Self` type in its return type + 1 | trait Widget { + | ------ this trait is not dyn compatible... + 2 | fn draw(&self); + 3 | fn duplicate(&self) -> Self; // Returns a copy of itself + | ^^^^ ...because method `duplicate` references the `Self` type in its return type + = help: consider moving `duplicate` to another trait + = help: only type `Button` implements `Widget`; consider using it directly instead. ``` That might sound pretty confusing in the beginning. -- What does "cannot be made into an object" even mean? +- What does "not dyn compatible" even mean? - Shouldn't the `dyn` part take care of that? +- What's a vtable, and why does the trait need to "allow building" one? - What does it have to do with `Self`? You've just encountered **dyn compatibility**. @@ -106,9 +111,9 @@ Dynamic dispatch means that the exact method to call is determined at runtime ba However, for dynamic dispatch to work, you must follow certain rules. -1. The trait must not have any methods that return `Self`. -2. The trait must not have any static methods (methods without a `self` parameter). -3. The trait must not have any generic type parameters on its methods. +1. The trait **must not** have any methods that return `Self`. +2. The trait **must not** have any static methods (methods without a `self` parameter). +3. The trait **must not** have any generic type parameters on its methods. In our example, the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". When you use `&dyn Widget`, the compiler doesn't know what `Self` is at runtime because it could be any type that implements `Widget`. @@ -125,7 +130,7 @@ There are multiple ways to make it dyn compatible. We have a bunch of options: 1. Use Generics Instead -2. Opt Out Problem Methods with `where Self: Sized` +2. Opt Out Problematic Methods with `where Self: Sized` 3. Return Boxed Trait Objects Instead of `Self` 4. Split Into Two Traits From 33186a7b1cae251b5e45cca368e964499ab98a3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 01:17:21 +0200 Subject: [PATCH 10/24] fix typos --- content/blog/dyn-compatibility/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index bb7a14b8..7fb0cbc7 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -22,7 +22,7 @@ When a trait can't be used with dynamic dispatch, we say it's "not dyn compatibl This has an impact on how you can use these traits in your code. Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -Once you understand why these rules exist, they become tell-tale sign of your design choices. +Once you understand why these rules exist, they become a tell-tale sign of your design choices. Knowing your options lets you write more deliberate, flexible Rust. You'll see the tradeoffs between compile-time generics and runtime polymorphism, and get a solid grasp of when each approach fits your problem. @@ -184,7 +184,7 @@ The downside is that you can't fully lean on dynamic dispatch anymore [^why-dyna - It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. - It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. - For exmaple, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. + For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. ### Fix #2: Opt Out Problematic Methods with `where Self: Sized` @@ -202,7 +202,7 @@ trait Widget { This means "this method can only be called when `Self` has a known size at compile time", which is true for concrete types but not for trait objects. It is more explicit because you're in control over how the trait can be used. -The downside is that this limits the usability of trait further down the line because some methods won't be callable on all trait objects and changing the trait will cause breaking changes. +The downside is that this limits the usability of the trait further down the line because some methods won't be callable on all trait objects and changing the trait will cause breaking changes. You won't be able to call `duplicate` on `&dyn Widget`, but you can still call it on concrete types like `Button`. @@ -228,7 +228,7 @@ This means you don't lose all the flexibility of trait objects (in contrast to g We can change the return type of the problematic method to return a boxed trait object instead of `Self`. -This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. +This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. ```rust trait Widget { @@ -267,7 +267,7 @@ fn main() { } ``` -The downside is that `Box` is often viral in your codebase: you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. +The downside is that `Box` is often viral in your codebase: you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. On top of that, this fix only works for methods that return `Self`. If your trait also has static methods or generic methods, you'll need to combine this approach with one of the other fixes. @@ -280,7 +280,7 @@ Maybe your code is silently trying to tell you that you are mixing up two differ In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! -Instead, we lean on composition and focus on behavior instead of mangling multiple responsibilities into a single trait. +Instead, we lean on composition and focus on behavior rather than mangling multiple responsibilities into a single trait. Here's a more realistic example: separating rendering from widget creation. Factory methods are often static (no `self` parameter), which makes them incompatible with `dyn`. So we split them into separate traits. From a9dd9b4cf808ef73aa4ec34a7c396f2bf6d5420d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 02:03:34 +0200 Subject: [PATCH 11/24] corrections --- content/blog/dyn-compatibility/index.md | 61 +++++++++++++++++++------ 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 7fb0cbc7..b3938a6c 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -41,23 +41,29 @@ The new term "dyn compatibility" does a better job at reflecting that it's about {% end %} -## The Problem +## The Error Message -Here's an example with code that **won't compile**: +Here's an example with code that **won't compile**. + +Say you have a trait `Widget` which has a method that returns a copy of itself: ```rust trait Widget { fn draw(&self); fn duplicate(&self) -> Self; // Returns a copy of itself } +``` +...and there's a button, which implements `Widget`: + +```rust struct Button { label: String, } impl Widget for Button { fn draw(&self) { - println!("Button: {}", self.label); + // ... } fn duplicate(&self) -> Self { @@ -66,8 +72,11 @@ impl Widget for Button { } fn show_widget(widget: &dyn Widget) { - widget.draw(); // Works - let copy = widget.duplicate(); // Error + // This works + widget.draw(); + + // This produces an error because duplicate returns `Self` + let copy = widget.duplicate(); copy.draw(); } ``` @@ -94,20 +103,20 @@ note: for a trait to be dyn compatible it needs to allow building a vtable = help: only type `Button` implements `Widget`; consider using it directly instead. ``` -That might sound pretty confusing in the beginning. +That's a really good error message, but it might still sound pretty confusing in the beginning. -- What does "not dyn compatible" even mean? -- Shouldn't the `dyn` part take care of that? -- What's a vtable, and why does the trait need to "allow building" one? +- What does "not dyn compatible" mean? +- Shouldn't the `dyn` part take care of it? +- What's a "vtable", and why does the trait need to "allow building" one? - What does it have to do with `Self`? -You've just encountered **dyn compatibility**. +You've just unlocked **dyn compatibility** problems. ## What's going on? When you use `&dyn Trait`, Rust creates a **trait object**. Trait objects use **dynamic dispatch** to call methods at runtime. -Dynamic dispatch means that the exact method to call is determined at runtime based on the actual type of the object. +Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. However, for dynamic dispatch to work, you must follow certain rules. @@ -115,9 +124,10 @@ However, for dynamic dispatch to work, you must follow certain rules. 2. The trait **must not** have any static methods (methods without a `self` parameter). 3. The trait **must not** have any generic type parameters on its methods. -In our example, the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". +In our example, we violate the first rule: the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". When you use `&dyn Widget`, the compiler doesn't know what `Self` is at runtime because it could be any type that implements `Widget`. That's a problem, because the compiler needs to know the size of the return type at compile time, and `Self` could be **any size**. +It needs to know the size, because the returned value has to live *somewhere*: the caller sets aside exactly the right amount of space (usually on the stack) before the call even happens. With a `&dyn Widget`, the concrete type is erased, so there's no single size the compiler could reserve for it. It will become clearer once we look at some fixes. @@ -228,7 +238,7 @@ This means you don't lose all the flexibility of trait objects (in contrast to g We can change the return type of the problematic method to return a boxed trait object instead of `Self`. -This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap! Pointers have a known size (usually 8 bytes on 64-bit systems), so the compiler knows how much space to allocate for it, unlike `Self` which varies based on the concrete type. +This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap. (It's actually a *fat pointer*: two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable -- more on that later.) The key point is that this size is fixed and known at compile time, unlike `Self`, which varies based on the concrete type. ```rust trait Widget { @@ -358,7 +368,7 @@ In summary, a trait is **dyn compatible** if it follows [these rules](https://do | Rule | Why? | |------|------| | No `Self: Sized` supertrait | The trait itself must not require `Self: Sized`, otherwise it can never be used as a trait object | -| Methods must have a receiver | All methods need `&self`, `&mut self`, or similar. Static methods (no receiver) can't be called through a vtable | +| Methods must have a receiver | Methods need a receiver: `&self`, `&mut self`, or a pointer type like `Box`, `Rc`, `Arc`, or `Pin<&Self>`. Static methods (no receiver) can't be called through a vtable | | No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | | No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | | No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | @@ -367,6 +377,25 @@ In summary, a trait is **dyn compatible** if it follows [these rules](https://do That's quite a lot of rules, but they all boil down to the same core issue: **the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** +If you ever need the gory details -- the exact, normative list of what makes a trait dyn compatible -- the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. + +{% info(title="A Modern Gotcha: `async fn` in Traits", icon="crab") %} + +Since [Rust 1.75](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/), you can write `async fn` directly in a trait. +But there's a catch: a trait with an `async fn` is **not dyn compatible**. + +The reason fits right into what we've seen. +An `async fn` desugars to a regular method that returns `impl Future<...>` -- a hidden, return-position `impl Trait`. +Opaque return types aren't dispatchable, so the trait can't be used behind `dyn`. + +If you need dynamic dispatch with async methods today, you have a few options: + +- Box the future yourself and return `Pin>>`. +- Use the [`async-trait`](https://crates.io/crates/async-trait) crate, which does that boxing for you. +- Use the [`dynosaur`](https://crates.io/crates/dynosaur) crate, which generates a dyn-compatible wrapper for traits with `async fn`. + +{% end %} + ## Summary **Dyn compatibility** determines if a trait can be used with `dyn Trait`. The rules exist because: @@ -376,7 +405,7 @@ That's quite a lot of rules, but they all boil down to the same core issue: 3. Type information is erased at runtime in order to allow polymorphism 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type -If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Iterator`, etc.) are also not dyn compatible. +If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. ### Historical Notes @@ -388,4 +417,6 @@ If you do, too, here are some resources to dig deeper: - 2014-11-03: [Issue #428](https://github.com/rust-lang/rfcs/issues/428) - Object-safety and static methods - 2015-01-03: [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html) - Removed implied `Sized` bound on traits - 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` +- 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits -- though such traits still aren't dyn compatible - 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - Silently renamed "object safety" to "dyn compatibility" (tragically, not mentioned in the release notes!) +- Planned: the lang team wants a "practical path" to call `async fn`s through `dyn Trait` natively -- it's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time From 4a57403c9ebaf1a2e67594db2ef5016f2588f372 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 02:07:01 +0200 Subject: [PATCH 12/24] table and wording --- content/blog/dyn-compatibility/index.md | 34 ++++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index b3938a6c..3b57d112 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -124,6 +124,8 @@ However, for dynamic dispatch to work, you must follow certain rules. 2. The trait **must not** have any static methods (methods without a `self` parameter). 3. The trait **must not** have any generic type parameters on its methods. +These are simplifications: each of these is really "...unless that method opts out with `where Self: Sized`", which we'll see in a moment. But for now, the rough version is enough to build intuition. + In our example, we violate the first rule: the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". When you use `&dyn Widget`, the compiler doesn't know what `Self` is at runtime because it could be any type that implements `Widget`. That's a problem, because the compiler needs to know the size of the return type at compile time, and `Self` could be **any size**. @@ -184,18 +186,20 @@ The difference is that with generics, the compiler knows the concrete type at co For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! -The downside is that you can't fully lean on dynamic dispatch anymore [^why-dynamic-dispatch] and that you might have to refactor a lot of code if you were using trait objects extensively before. +The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. + +{% info(title="What's the benefit of fully leaning on dynamic dispatch?", icon="crab") %} -[^why-dynamic-dispatch]: **"What's the benefit of fully leaning on dynamic dispatch"**, you ask? Fair question! +Fair question! Dynamic dispatch has a bunch of really nice properties: - Dynamic dispatch has a bunch of really nice properties: +- It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. +- It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. + You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. +- It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. + For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. + Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. - - It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. - - It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. - You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. - - It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. - For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. - Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. +{% end %} ### Fix #2: Opt Out Problematic Methods with `where Self: Sized` @@ -408,6 +412,18 @@ If you need dynamic dispatch with async methods today, you have a few options: If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. +Which fix to reach for depends on what your trait needs and what you're willing to give up: + +| Fix | Reach for it when... | The tradeoff | +|-----|----------------------|--------------| +| **#1 Generics** (``) | You don't actually need trait objects -- the concrete type is known at each call site, and you don't need to mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | +| **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | +| **#3 Return `Box`** | The method returns `Self` and you genuinely need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | +| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of -- though that separation is often a feature, not a cost | +| **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Boxed futures (an allocation per call) until native `dyn` async lands | + +In practice you'll often combine these -- for example, splitting a trait *and* boxing a return value. + ### Historical Notes I find it interesting to see how dyn compatibility evolved over time in Rust. From 50d8ccbeab2397716154b9eca0af830f5c6e01fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 02:13:58 +0200 Subject: [PATCH 13/24] formatting and wording --- content/blog/dyn-compatibility/index.md | 47 ++++++++++++------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 3b57d112..e7d97e84 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -22,10 +22,9 @@ When a trait can't be used with dynamic dispatch, we say it's "not dyn compatibl This has an impact on how you can use these traits in your code. Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -Once you understand why these rules exist, they become a tell-tale sign of your design choices. +Once you understand why these rules exist, they stop looking like arbitrary compiler errors and start pointing at real design choices. +You'll see the tradeoffs between compile-time generics and runtime polymorphism, and learn when each one fits. Knowing your options lets you write more deliberate, flexible Rust. -You'll see the tradeoffs between compile-time generics and runtime polymorphism, and -get a solid grasp of when each approach fits your problem. {% info(title="Dyn Compatibility and Object Safety", icon="crab") %} @@ -35,7 +34,7 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job at reflecting that it's about whether a trait can be used with `dyn Trait` in the context of dynamic dispatch. [^personal_note] +The new term "dyn compatibility" does a better job of reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] [^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. @@ -83,7 +82,7 @@ fn show_widget(widget: &dyn Widget) { If you tried to compile this code, you'd get an error like this: -``` +```rust error[E0038]: the trait `Widget` is not dyn compatible --> src/main.rs:20:25 | @@ -110,7 +109,7 @@ That's a really good error message, but it might still sound pretty confusing in - What's a "vtable", and why does the trait need to "allow building" one? - What does it have to do with `Self`? -You've just unlocked **dyn compatibility** problems. +You've just run into a **dyn compatibility** problem. ## What's going on? @@ -148,7 +147,7 @@ We have a bunch of options: Let's look at each of these in detail. Each approach comes with different tradeoffs. -Depending on the type of dyn-compatibility issue, one might be more suitable than the others or you might even need to use a combination of them. +Depending on the kind of dyn-compatibility issue, one might fit better than the others, or you might combine a few. ### Fix #1: Use Generics Instead @@ -181,7 +180,7 @@ fn show_widget(widget: &W) { Note how we changed the function signature to use a generic type parameter `W` that implements the `Widget` trait. Here we tell Rust: "I have some type `W` that implements `Widget`, and I want to use it." and Rust will happily generate all the necessary code for each type used. -That is similar but slightly different from using `&dyn Widget`. +That's close to using `&dyn Widget`, but not quite the same. The difference is that with generics, the compiler knows the concrete type at compile time, so it can handle `Self` correctly. For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! @@ -215,8 +214,8 @@ trait Widget { ``` This means "this method can only be called when `Self` has a known size at compile time", which is true for concrete types but not for trait objects. -It is more explicit because you're in control over how the trait can be used. -The downside is that this limits the usability of the trait further down the line because some methods won't be callable on all trait objects and changing the trait will cause breaking changes. +It's more explicit, since you control how the trait can be used. +The catch is that it limits the trait further down the line: some methods won't be callable on every trait object, and changing the trait later becomes a breaking change. You won't be able to call `duplicate` on `&dyn Widget`, but you can still call it on concrete types like `Button`. @@ -236,13 +235,13 @@ fn main() { } ``` -This means you don't lose all the flexibility of trait objects (in contrast to generics), but you have to be aware that some methods won't be available when using `dyn Trait`. +So you keep most of the flexibility of trait objects (unlike with generics), as long as you remember that some methods won't be available through `dyn Trait`. ### Fix #3: Return Boxed Trait Objects Instead of `Self` We can change the return type of the problematic method to return a boxed trait object instead of `Self`. -This works because `Box` has a known size at compile time -- it's a pointer to an object on the heap. (It's actually a *fat pointer*: two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable -- more on that later.) The key point is that this size is fixed and known at compile time, unlike `Self`, which varies based on the concrete type. +This works because `Box` has a known size at compile time. It's a pointer to an object on the heap. (It's actually a *fat pointer*: two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable; more on that later.) What matters is that this size is fixed and known at compile time, unlike `Self`, which varies based on the concrete type. ```rust trait Widget { @@ -281,7 +280,7 @@ fn main() { } ``` -The downside is that `Box` is often viral in your codebase: you'll end up writing out the concrete type as `Box` more often than you'd like, which can lead to noisy code. +One cost: `Box` tends to be viral in your codebase. You'll end up writing `Box` more often than you'd like, which gets noisy. On top of that, this fix only works for methods that return `Self`. If your trait also has static methods or generic methods, you'll need to combine this approach with one of the other fixes. @@ -359,7 +358,7 @@ The [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) is created at c It is a concept that is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. -But in order to create a vtable, the compiler needs to know: +But to create a vtable, the compiler needs to know: - The size of the type (to allocate memory) - The exact method signatures (to create function pointers) @@ -376,12 +375,12 @@ In summary, a trait is **dyn compatible** if it follows [these rules](https://do | No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | | No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | | No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | -| No `impl Trait` in return position | Similar to `Self` - the actual type needs to be known at compile time, but it's erased with trait objects | +| No `impl Trait` in return position | Similar to `Self`: the actual type needs to be known at compile time, but it's erased with trait objects | That's quite a lot of rules, but they all boil down to the same core issue: **the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** -If you ever need the gory details -- the exact, normative list of what makes a trait dyn compatible -- the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. +If you ever need the gory details (the exact, normative list of what makes a trait dyn compatible), the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. {% info(title="A Modern Gotcha: `async fn` in Traits", icon="crab") %} @@ -389,7 +388,7 @@ Since [Rust 1.75](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/), you can w But there's a catch: a trait with an `async fn` is **not dyn compatible**. The reason fits right into what we've seen. -An `async fn` desugars to a regular method that returns `impl Future<...>` -- a hidden, return-position `impl Trait`. +An `async fn` desugars to a regular method that returns `impl Future<...>`, a hidden return-position `impl Trait`. Opaque return types aren't dispatchable, so the trait can't be used behind `dyn`. If you need dynamic dispatch with async methods today, you have a few options: @@ -406,7 +405,7 @@ If you need dynamic dispatch with async methods today, you have a few options: 1. Trait objects use dynamic dispatch via vtables 2. Vtables are static, compile-time structures, which hold method pointers -3. Type information is erased at runtime in order to allow polymorphism +3. Type information is erased at runtime to allow polymorphism 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. @@ -416,13 +415,13 @@ Which fix to reach for depends on what your trait needs and what you're willing | Fix | Reach for it when... | The tradeoff | |-----|----------------------|--------------| -| **#1 Generics** (``) | You don't actually need trait objects -- the concrete type is known at each call site, and you don't need to mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | +| **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | -| **#3 Return `Box`** | The method returns `Self` and you genuinely need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | -| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of -- though that separation is often a feature, not a cost | +| **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | +| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of (though that separation is often a feature, not a cost) | | **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Boxed futures (an allocation per call) until native `dyn` async lands | -In practice you'll often combine these -- for example, splitting a trait *and* boxing a return value. +In practice you'll often combine these. For example, splitting a trait and boxing a return value. ### Historical Notes @@ -433,6 +432,6 @@ If you do, too, here are some resources to dig deeper: - 2014-11-03: [Issue #428](https://github.com/rust-lang/rfcs/issues/428) - Object-safety and static methods - 2015-01-03: [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html) - Removed implied `Sized` bound on traits - 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` -- 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits -- though such traits still aren't dyn compatible +- 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits (though such traits still aren't dyn compatible) - 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - Silently renamed "object safety" to "dyn compatibility" (tragically, not mentioned in the release notes!) -- Planned: the lang team wants a "practical path" to call `async fn`s through `dyn Trait` natively -- it's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time +- Planned: the lang team wants a "practical path" to call `async fn`s through `dyn Trait` natively. It's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time From 88b814cca64fe7b524e94696f6273075e0869810 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 13:01:06 +0200 Subject: [PATCH 14/24] add short intro --- content/blog/dyn-compatibility/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index e7d97e84..896ea46f 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -15,6 +15,19 @@ resources = [ ] +++ +{% info(title="In a hurry? The short version", icon="crab") %} + +If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. + +To fix it, pick one: + +- use generics instead of `&dyn Trait` +- add `where Self: Sized` to the offending method +- return `Box` instead of `Self`, or +- split the trait in two + +{% end %} + In Rust, not all traits can be used as trait objects with `dyn Trait`. When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." From e3a0972d1b4ef7e3db5b4d92a6f317db997a9a37 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2026 14:10:40 +0200 Subject: [PATCH 15/24] wording --- content/blog/dyn-compatibility/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 896ea46f..2c45e104 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -15,7 +15,7 @@ resources = [ ] +++ -{% info(title="In a hurry? The short version", icon="crab") %} +{% info(title="Here's the gist", icon="crab") %} If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. From 295c96c99ef4709ba2e687d95e0046ea3cdba7cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:25:55 +0200 Subject: [PATCH 16/24] Quarantine flaky external links --- lychee.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lychee.toml b/lychee.toml index 554f84a0..9b194871 100644 --- a/lychee.toml +++ b/lychee.toml @@ -41,6 +41,7 @@ exclude = [ '^https?://t\.co/', '^https?://(www\.)?reddit\.com', '^https?://web\.archive\.org', + '^https?://news\.ycombinator\.com', '^https?://([a-z0-9-]+\.)?amazon\.[a-z.]+', # Hosts that block automated checkers or answer with non-2xx status codes @@ -72,6 +73,8 @@ exclude = [ '^https://fusion\.engineering/?$', # intermittent 500 '^https://manpages\.ubuntu\.com/manpages/noble/en/man8/sudo\.8\.html', # intermittent timeout '^https://greenlab\.di\.uminho\.pt/wp-content/uploads/2017/10/sleFinal\.pdf$', # intermittent timeout + '^https://lib\.rs/stats$', # intermittent 502 + '^https://soller\.dev/?$', # expired TLS cert # Dead pages we can't repoint to a sensible replacement. '^https://kerkour\.com/bugs-rust-compiler-helps-prevent', # post removed in site migration From debc3fedac1e51772ce695e77278954009352811 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:37:59 +0200 Subject: [PATCH 17/24] Tighten dyn compatibility article --- content/blog/dyn-compatibility/index.md | 49 ++++++++++++++----------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 2c45e104..2cd1b1d0 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -11,6 +11,7 @@ reviews = [ resources = [ "[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", "[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", +"[Rust error E0038: dyn compatibility](https://doc.rust-lang.org/error_codes/E0038.html)", "[Two Ways To Do Dynamic Dispatch](https://www.youtube.com/watch?v=wU8hQvU8aKM) - Video by Logan Smith, which explains dyn dispatch from first principles" ] +++ @@ -130,18 +131,19 @@ When you use `&dyn Trait`, Rust creates a **trait object**. Trait objects use **dynamic dispatch** to call methods at runtime. Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. -However, for dynamic dispatch to work, you must follow certain rules. +However, for dynamic dispatch to work, the trait's dispatchable API must follow certain rules. -1. The trait **must not** have any methods that return `Self`. -2. The trait **must not** have any static methods (methods without a `self` parameter). -3. The trait **must not** have any generic type parameters on its methods. +1. Dispatchable methods **must not** return `Self`. +2. Dispatchable methods **must have an allowed receiver** (`&self`, `&mut self`, `Box`, and a few related pointer forms). Plain static methods don't have one. +3. Dispatchable methods **must not** have generic type parameters. -These are simplifications: each of these is really "...unless that method opts out with `where Self: Sized`", which we'll see in a moment. But for now, the rough version is enough to build intuition. +These are simplifications: each method-level rule is really "...unless that method opts out with `where Self: Sized`", which we'll see in a moment. Traits also have a few item-level restrictions, such as no associated constants; we'll summarize the fuller list later. For now, the rough version is enough to build intuition. In our example, we violate the first rule: the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". -When you use `&dyn Widget`, the compiler doesn't know what `Self` is at runtime because it could be any type that implements `Widget`. -That's a problem, because the compiler needs to know the size of the return type at compile time, and `Self` could be **any size**. -It needs to know the size, because the returned value has to live *somewhere*: the caller sets aside exactly the right amount of space (usually on the stack) before the call even happens. With a `&dyn Widget`, the concrete type is erased, so there's no single size the compiler could reserve for it. +When you use `&dyn Widget`, the concrete implementor is hidden behind the trait-object interface. +The vtable still points to the right concrete implementation, but the call site has no single concrete return type it can name for `duplicate`. +That's a problem, because the compiler needs to know the size of the return value at compile time, and `Self` could be **any size**. +It needs to know the size, because the returned value has to live *somewhere*: the caller sets aside exactly the right amount of space (usually on the stack) before the call even happens. With a `&dyn Widget`, the concrete type is erased from the caller's static type, so there's no single size the compiler could reserve for it. It will become clearer once we look at some fixes. @@ -371,11 +373,12 @@ The [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) is created at c It is a concept that is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. -But to create a vtable, the compiler needs to know: -- The size of the type (to allocate memory) -- The exact method signatures (to create function pointers) +For dynamic dispatch to be sound, the vtable-facing methods need stable, concrete function signatures: +- every dispatchable method needs a receiver that leads to the object and its vtable +- argument and return types must be expressible without knowing the hidden concrete `Self` +- the vtable must contain a finite set of function pointers, known at compile time -If a trait has methods that return `Self` or have generic parameters, the compiler can't create a proper vtable because it doesn't know what `Self` is or how to handle generics at runtime. +If a trait has dispatchable methods that return `Self` or have generic parameters, there is no single vtable entry with one concrete signature that can represent all possible calls. That is the root cause of dyn compatibility issues. @@ -383,15 +386,19 @@ In summary, a trait is **dyn compatible** if it follows [these rules](https://do | Rule | Why? | |------|------| -| No `Self: Sized` supertrait | The trait itself must not require `Self: Sized`, otherwise it can never be used as a trait object | -| Methods must have a receiver | Methods need a receiver: `&self`, `&mut self`, or a pointer type like `Box`, `Rc`, `Arc`, or `Pin<&Self>`. Static methods (no receiver) can't be called through a vtable | -| No generic type parameters on methods | The vtable is a static struct created at compile time and can't have infinite entries. Generic methods are monomorphized at compile time (one copy per type), but trait objects work at runtime when the type is erased | -| No `Self` in method parameters (except receiver) | `other: &Self` means "the same type as `self`", but with trait objects, we only know both are "`dyn Comparable`". They could be different underlying types! | -| No `Self` return type | The compiler needs to know the size of the return value, but `Self` could be any size. With trait objects, the type is erased | -| No `impl Trait` in return position | Similar to `Self`: the actual type needs to be known at compile time, but it's erased with trait objects | +| All supertraits must also be dyn compatible | A `dyn Subtrait` also exposes the supertrait API, so those inherited methods must be dispatchable too | +| No `Self: Sized` supertrait | The trait object type `dyn Trait` is unsized, so the trait itself must not require `Self: Sized` | +| No associated constants | Associated constants are not entries in the method vtable | +| No generic associated types | The Reference currently forbids associated types with generics on dyn-compatible traits | +| Dispatchable methods must have an allowed receiver | Methods need a receiver: `&self`, `&mut self`, or pointer receivers like `Box`, `Rc`, `Arc`, or `Pin

` where `P` is one of those pointer forms. Static methods (no receiver) can't be dispatched through a trait object | +| No generic type parameters on dispatchable methods | The vtable is a finite structure created at compile time. Generic methods are monomorphized at compile time (one copy per concrete instantiation), but a trait object erases the concrete receiver type | +| No `Self` in dispatchable method parameters except the receiver | `other: &Self` means "the same concrete type as `self`", but with trait objects we only know both are `dyn Comparable`; they could hide different underlying types | +| No `Self` return type on dispatchable methods | The caller needs to know the return value's size and type, but `Self` could be any implementor | +| No opaque return type on dispatchable methods | `async fn` and return-position `impl Trait` hide a concrete return type that must be known statically | +| Non-dispatchable methods must opt out | A method that violates the dispatch rules can still live on the trait if it has `where Self: Sized`, making it unavailable through `dyn Trait` | That's quite a lot of rules, but they all boil down to the same core issue: -**the compiler needs to know sizes and types at compile time, but with trait objects, that information is erased at runtime.** +**the `dyn Trait` interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden.** If you ever need the gory details (the exact, normative list of what makes a trait dyn compatible), the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. @@ -432,7 +439,7 @@ Which fix to reach for depends on what your trait needs and what you're willing | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | | **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | | **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of (though that separation is often a feature, not a cost) | -| **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Boxed futures (an allocation per call) until native `dyn` async lands | +| **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Wrapper types and usually boxed futures/extra indirection until native `dyn` async improves | In practice you'll often combine these. For example, splitting a trait and boxing a return value. @@ -446,5 +453,5 @@ If you do, too, here are some resources to dig deeper: - 2015-01-03: [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html) - Removed implied `Sized` bound on traits - 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` - 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits (though such traits still aren't dyn compatible) -- 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - Silently renamed "object safety" to "dyn compatibility" (tragically, not mentioned in the release notes!) +- 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - The docs had moved from "object safety" to "dyn compatibility" around this release cycle; the [tracking issue](https://github.com/rust-lang/rust/issues/130852) notes that the rename missed the release notes. - Planned: the lang team wants a "practical path" to call `async fn`s through `dyn Trait` natively. It's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time From f36fa0ed8d50236abfd5454999f5e47720ea6ba1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:53:33 +0200 Subject: [PATCH 18/24] wip Signed-off-by: Matthias --- content/blog/dyn-compatibility/index.md | 27 +++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 2cd1b1d0..9b0dd6ce 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,6 +1,6 @@ +++ title = "Understanding Dyn Compatibility" -date = 2026-06-17 +date = 2026-07-28 draft = false template = "article.html" [extra] @@ -16,18 +16,6 @@ resources = [ ] +++ -{% info(title="Here's the gist", icon="crab") %} - -If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. - -To fix it, pick one: - -- use generics instead of `&dyn Trait` -- add `where Self: Sized` to the offending method -- return `Box` instead of `Self`, or -- split the trait in two - -{% end %} In Rust, not all traits can be used as trait objects with `dyn Trait`. @@ -40,6 +28,19 @@ Once you understand why these rules exist, they stop looking like arbitrary comp You'll see the tradeoffs between compile-time generics and runtime polymorphism, and learn when each one fits. Knowing your options lets you write more deliberate, flexible Rust. +{% info(title="Quick Summary", icon="crab") %} + +If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. + +To fix it, pick one: + +- use generics instead of `&dyn Trait` +- add `where Self: Sized` to the offending method +- return `Box` instead of `Self`, or +- split the trait in two + +{% end %} + {% info(title="Dyn Compatibility and Object Safety", icon="crab") %} This concept used to be called "object safety" until Rust 1.84.0. From 701545ee09555b38ab91da0537e04ca7c20d2565 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:54:57 +0200 Subject: [PATCH 19/24] Deslop dyn compatibility article --- content/blog/dyn-compatibility/index.md | 48 +++++++++++-------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 9b0dd6ce..39dbb9be 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -17,7 +17,7 @@ resources = [ +++ -In Rust, not all traits can be used as trait objects with `dyn Trait`. +Some Rust traits can be used as trait objects with `dyn Trait`. Others can't. When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." @@ -49,7 +49,7 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job of reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] +The new term "dyn compatibility" says the quiet part out loud: can this trait be used as `dyn Trait` for dynamic dispatch? [^personal_note] [^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. @@ -117,7 +117,7 @@ note: for a trait to be dyn compatible it needs to allow building a vtable = help: only type `Button` implements `Widget`; consider using it directly instead. ``` -That's a really good error message, but it might still sound pretty confusing in the beginning. +That's a good error message, but it can still be confusing the first time you see it. - What does "not dyn compatible" mean? - Shouldn't the `dyn` part take care of it? @@ -129,16 +129,15 @@ You've just run into a **dyn compatibility** problem. ## What's going on? When you use `&dyn Trait`, Rust creates a **trait object**. -Trait objects use **dynamic dispatch** to call methods at runtime. -Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. +Trait objects use **dynamic dispatch**: Rust chooses the method implementation at runtime based on the concrete type behind the object. -However, for dynamic dispatch to work, the trait's dispatchable API must follow certain rules. +For that to work, the trait's dispatchable API must follow certain rules. 1. Dispatchable methods **must not** return `Self`. 2. Dispatchable methods **must have an allowed receiver** (`&self`, `&mut self`, `Box`, and a few related pointer forms). Plain static methods don't have one. 3. Dispatchable methods **must not** have generic type parameters. -These are simplifications: each method-level rule is really "...unless that method opts out with `where Self: Sized`", which we'll see in a moment. Traits also have a few item-level restrictions, such as no associated constants; we'll summarize the fuller list later. For now, the rough version is enough to build intuition. +These are simplifications. Each method-level rule really means "unless that method opts out with `where Self: Sized`", which we'll see in a moment. Traits also have item-level restrictions, such as no associated constants. We'll come back to the full list later. For now, the rough version is enough. In our example, we violate the first rule: the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". When you use `&dyn Widget`, the concrete implementor is hidden behind the trait-object interface. @@ -150,11 +149,10 @@ It will become clearer once we look at some fixes. ## How To Fix It -Don't worry, we won't have to refactor all our code! -All fixes use the same `Widget` trait example. -There are multiple ways to make it dyn compatible. +We don't have to rewrite everything. +All fixes use the same `Widget` example, and each fix keeps a different tradeoff. -We have a bunch of options: +The main options are: 1. Use Generics Instead 2. Opt Out Problematic Methods with `where Self: Sized` @@ -169,8 +167,7 @@ Depending on the kind of dyn-compatibility issue, one might fit better than the One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types *at compile time*, so the compiler knows the size of `Self`. -Basically, the compiler will generate a separate version of the function for each type that implements the trait. Then at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). -The compiler always knows which type it is dealing with, so it can pick the right method to call. +The compiler generates a separate version of the function for each concrete type that implements the trait. Since the concrete type is known, the call no longer needs dynamic dispatch. Our trait stays the same: @@ -205,14 +202,11 @@ The downside is that you can't fully lean on dynamic dispatch anymore and that y {% info(title="What's the benefit of fully leaning on dynamic dispatch?", icon="crab") %} -Fair question! Dynamic dispatch has a bunch of really nice properties: +Fair question. Dynamic dispatch buys you flexibility. You can swap implementations at runtime, which is handy for plugins or for behavior you want to change without recompiling. -- It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. -- It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. - You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. -- It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. - For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. - Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. +It also gives you polymorphism without monomorphizing every call site. You treat different implementors the same way through one trait object. Generics can model many of the same ideas, but they create separate code for each concrete type, which can hurt compile times and binary size. + +A rendering engine is a classic example. Different shapes can all implement a `Drawable` trait. With dynamic dispatch, you can put them in one collection and call `draw()` on each item. With only generics, you'd need another layer of code to handle each concrete shape type. {% end %} @@ -383,7 +377,7 @@ If a trait has dispatchable methods that return `Self` or have generic parameter That is the root cause of dyn compatibility issues. -In summary, a trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): +A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): | Rule | Why? | |------|------| @@ -398,10 +392,10 @@ In summary, a trait is **dyn compatible** if it follows [these rules](https://do | No opaque return type on dispatchable methods | `async fn` and return-position `impl Trait` hide a concrete return type that must be known statically | | Non-dispatchable methods must opt out | A method that violates the dispatch rules can still live on the trait if it has `where Self: Sized`, making it unavailable through `dyn Trait` | -That's quite a lot of rules, but they all boil down to the same core issue: +The details get long, but the idea is short: **the `dyn Trait` interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden.** -If you ever need the gory details (the exact, normative list of what makes a trait dyn compatible), the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. +For the exact normative list, use the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility). The rules above are the working version; the Reference is the spec. {% info(title="A Modern Gotcha: `async fn` in Traits", icon="crab") %} @@ -429,8 +423,8 @@ If you need dynamic dispatch with async methods today, you have a few options: 3. Type information is erased at runtime to allow polymorphism 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type -If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. -As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. +If your trait is not dyn compatible, you're in good company. Many standard library traits (`Clone`, `Default`, etc.) aren't dyn compatible either. +You can usually work around the limitation with type erasure, generics, or smaller traits. Which fix to reach for depends on what your trait needs and what you're willing to give up: @@ -439,10 +433,10 @@ Which fix to reach for depends on what your trait needs and what you're willing | **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | | **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | -| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of (though that separation is often a feature, not a cost) | +| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of, but clearer separation | | **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Wrapper types and usually boxed futures/extra indirection until native `dyn` async improves | -In practice you'll often combine these. For example, splitting a trait and boxing a return value. +You'll often combine these. For example, you might split a trait and box a return value. ### Historical Notes From 8a6e288d42edab1283ae425202236c13f264db3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:58:19 +0200 Subject: [PATCH 20/24] Revert "Deslop dyn compatibility article" This reverts commit 701545ee09555b38ab91da0537e04ca7c20d2565. --- content/blog/dyn-compatibility/index.md | 48 ++++++++++++++----------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 39dbb9be..9b0dd6ce 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -17,7 +17,7 @@ resources = [ +++ -Some Rust traits can be used as trait objects with `dyn Trait`. Others can't. +In Rust, not all traits can be used as trait objects with `dyn Trait`. When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." @@ -49,7 +49,7 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" says the quiet part out loud: can this trait be used as `dyn Trait` for dynamic dispatch? [^personal_note] +The new term "dyn compatibility" does a better job of reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] [^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. @@ -117,7 +117,7 @@ note: for a trait to be dyn compatible it needs to allow building a vtable = help: only type `Button` implements `Widget`; consider using it directly instead. ``` -That's a good error message, but it can still be confusing the first time you see it. +That's a really good error message, but it might still sound pretty confusing in the beginning. - What does "not dyn compatible" mean? - Shouldn't the `dyn` part take care of it? @@ -129,15 +129,16 @@ You've just run into a **dyn compatibility** problem. ## What's going on? When you use `&dyn Trait`, Rust creates a **trait object**. -Trait objects use **dynamic dispatch**: Rust chooses the method implementation at runtime based on the concrete type behind the object. +Trait objects use **dynamic dispatch** to call methods at runtime. +Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. -For that to work, the trait's dispatchable API must follow certain rules. +However, for dynamic dispatch to work, the trait's dispatchable API must follow certain rules. 1. Dispatchable methods **must not** return `Self`. 2. Dispatchable methods **must have an allowed receiver** (`&self`, `&mut self`, `Box`, and a few related pointer forms). Plain static methods don't have one. 3. Dispatchable methods **must not** have generic type parameters. -These are simplifications. Each method-level rule really means "unless that method opts out with `where Self: Sized`", which we'll see in a moment. Traits also have item-level restrictions, such as no associated constants. We'll come back to the full list later. For now, the rough version is enough. +These are simplifications: each method-level rule is really "...unless that method opts out with `where Self: Sized`", which we'll see in a moment. Traits also have a few item-level restrictions, such as no associated constants; we'll summarize the fuller list later. For now, the rough version is enough to build intuition. In our example, we violate the first rule: the `duplicate` method returns `Self`, which means "the same type as the implementor of the trait". When you use `&dyn Widget`, the concrete implementor is hidden behind the trait-object interface. @@ -149,10 +150,11 @@ It will become clearer once we look at some fixes. ## How To Fix It -We don't have to rewrite everything. -All fixes use the same `Widget` example, and each fix keeps a different tradeoff. +Don't worry, we won't have to refactor all our code! +All fixes use the same `Widget` trait example. +There are multiple ways to make it dyn compatible. -The main options are: +We have a bunch of options: 1. Use Generics Instead 2. Opt Out Problematic Methods with `where Self: Sized` @@ -167,7 +169,8 @@ Depending on the kind of dyn-compatibility issue, one might fit better than the One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types *at compile time*, so the compiler knows the size of `Self`. -The compiler generates a separate version of the function for each concrete type that implements the trait. Since the concrete type is known, the call no longer needs dynamic dispatch. +Basically, the compiler will generate a separate version of the function for each type that implements the trait. Then at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). +The compiler always knows which type it is dealing with, so it can pick the right method to call. Our trait stays the same: @@ -202,11 +205,14 @@ The downside is that you can't fully lean on dynamic dispatch anymore and that y {% info(title="What's the benefit of fully leaning on dynamic dispatch?", icon="crab") %} -Fair question. Dynamic dispatch buys you flexibility. You can swap implementations at runtime, which is handy for plugins or for behavior you want to change without recompiling. +Fair question! Dynamic dispatch has a bunch of really nice properties: -It also gives you polymorphism without monomorphizing every call site. You treat different implementors the same way through one trait object. Generics can model many of the same ideas, but they create separate code for each concrete type, which can hurt compile times and binary size. - -A rendering engine is a classic example. Different shapes can all implement a `Drawable` trait. With dynamic dispatch, you can put them in one collection and call `draw()` on each item. With only generics, you'd need another layer of code to handle each concrete shape type. +- It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. +- It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. + You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. +- It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. + For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. + Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. {% end %} @@ -377,7 +383,7 @@ If a trait has dispatchable methods that return `Self` or have generic parameter That is the root cause of dyn compatibility issues. -A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): +In summary, a trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): | Rule | Why? | |------|------| @@ -392,10 +398,10 @@ A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang. | No opaque return type on dispatchable methods | `async fn` and return-position `impl Trait` hide a concrete return type that must be known statically | | Non-dispatchable methods must opt out | A method that violates the dispatch rules can still live on the trait if it has `where Self: Sized`, making it unavailable through `dyn Trait` | -The details get long, but the idea is short: +That's quite a lot of rules, but they all boil down to the same core issue: **the `dyn Trait` interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden.** -For the exact normative list, use the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility). The rules above are the working version; the Reference is the spec. +If you ever need the gory details (the exact, normative list of what makes a trait dyn compatible), the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. {% info(title="A Modern Gotcha: `async fn` in Traits", icon="crab") %} @@ -423,8 +429,8 @@ If you need dynamic dispatch with async methods today, you have a few options: 3. Type information is erased at runtime to allow polymorphism 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type -If your trait is not dyn compatible, you're in good company. Many standard library traits (`Clone`, `Default`, etc.) aren't dyn compatible either. -You can usually work around the limitation with type erasure, generics, or smaller traits. +If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. +As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. Which fix to reach for depends on what your trait needs and what you're willing to give up: @@ -433,10 +439,10 @@ Which fix to reach for depends on what your trait needs and what you're willing | **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | | **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | -| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of, but clearer separation | +| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of (though that separation is often a feature, not a cost) | | **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Wrapper types and usually boxed futures/extra indirection until native `dyn` async improves | -You'll often combine these. For example, you might split a trait and box a return value. +In practice you'll often combine these. For example, splitting a trait and boxing a return value. ### Historical Notes From be91251bfff28ec332281ffd3f34e82be5c740bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2026 00:59:16 +0200 Subject: [PATCH 21/24] Lightly deslop dyn compatibility article --- content/blog/dyn-compatibility/index.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 9b0dd6ce..3e32f676 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -17,7 +17,7 @@ resources = [ +++ -In Rust, not all traits can be used as trait objects with `dyn Trait`. +In Rust, some traits can't be used as trait objects with `dyn Trait`. When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." @@ -49,7 +49,7 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job of reflecting that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] +The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] [^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. @@ -132,7 +132,7 @@ When you use `&dyn Trait`, Rust creates a **trait object**. Trait objects use **dynamic dispatch** to call methods at runtime. Dynamic dispatch just means that the exact method to call is determined at runtime based on the actual type of the object. -However, for dynamic dispatch to work, the trait's dispatchable API must follow certain rules. +For dynamic dispatch to work, the trait's dispatchable API must follow certain rules. 1. Dispatchable methods **must not** return `Self`. 2. Dispatchable methods **must have an allowed receiver** (`&self`, `&mut self`, `Box`, and a few related pointer forms). Plain static methods don't have one. @@ -209,7 +209,7 @@ Fair question! Dynamic dispatch has a bunch of really nice properties: - It's very flexible. You can swap out implementations at runtime, which is great for plugins or when you want to change behavior without recompiling. - It allows for polymorphism. You can treat different types that implement the same trait uniformly, which can simplify code that needs to work with various types. - You could technically do the same with generics, but as I mentioned sometimes you can't afford the increase in code size or compile times that come with monomorphization. + You could technically do the same with generics, but sometimes you can't afford the increase in code size or compile times that come with monomorphization. - It can lead to cleaner and more maintainable code in certain scenarios, especially when dealing with complex hierarchies of types and behaviors. For example, take a graphics rendering engine where you have different shapes (circles, squares, triangles) that all implement a `Drawable` trait. Using dynamic dispatch, you can store them all in a single collection and call `draw()`. If you were to try the same with generics, you'd end up with a lot of boilerplate code to handle each shape type separately. @@ -383,7 +383,7 @@ If a trait has dispatchable methods that return `Self` or have generic parameter That is the root cause of dyn compatibility issues. -In summary, a trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): +A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): | Rule | Why? | |------|------| @@ -439,7 +439,7 @@ Which fix to reach for depends on what your trait needs and what you're willing | **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | | **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | -| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of (though that separation is often a feature, not a cost) | +| **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of, though that separation often helps | | **`async-trait` / `dynosaur`** | Your trait has `async fn`s and you need to call them through `dyn` | Wrapper types and usually boxed futures/extra indirection until native `dyn` async improves | In practice you'll often combine these. For example, splitting a trait and boxing a return value. @@ -455,4 +455,5 @@ If you do, too, here are some resources to dig deeper: - 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` - 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits (though such traits still aren't dyn compatible) - 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - The docs had moved from "object safety" to "dyn compatibility" around this release cycle; the [tracking issue](https://github.com/rust-lang/rust/issues/130852) notes that the rename missed the release notes. -- Planned: the lang team wants a "practical path" to call `async fn`s through `dyn Trait` natively. It's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time + +The lang team also wants a "practical path" to call `async fn`s through `dyn Trait` natively. It's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time. From b0405444fa4a126439453b69660c1e4c23c6d3a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Jul 2026 00:36:53 +0200 Subject: [PATCH 22/24] update post --- content/blog/dyn-compatibility/index.md | 43 ++++++++++++------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index 3e32f676..c36eaca0 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -24,9 +24,9 @@ When a trait can't be used with dynamic dispatch, we say it's "not dyn compatibl This has an impact on how you can use these traits in your code. Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -Once you understand why these rules exist, they stop looking like arbitrary compiler errors and start pointing at real design choices. -You'll see the tradeoffs between compile-time generics and runtime polymorphism, and learn when each one fits. -Knowing your options lets you write more deliberate, flexible Rust. +Once you understand why these rules exist, you'll know how to get around them by choosing a better design for your trait. +Fixing the issue is mostly about tradeoffs between compile-time generics and runtime polymorphism and learning when each one fits. +This lets you write more deliberate, flexible Rust. {% info(title="Quick Summary", icon="crab") %} @@ -39,6 +39,8 @@ To fix it, pick one: - return `Box` instead of `Self`, or - split the trait in two +Continue reading to understand the tradeoffs between each approach. + {% end %} {% info(title="Dyn Compatibility and Object Safety", icon="crab") %} @@ -49,9 +51,7 @@ If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. [^personal_note] - -[^personal_note]: I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. +The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch, although I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. {% end %} @@ -124,8 +124,6 @@ That's a really good error message, but it might still sound pretty confusing in - What's a "vtable", and why does the trait need to "allow building" one? - What does it have to do with `Self`? -You've just run into a **dyn compatibility** problem. - ## What's going on? When you use `&dyn Trait`, Rust creates a **trait object**. @@ -156,20 +154,21 @@ There are multiple ways to make it dyn compatible. We have a bunch of options: -1. Use Generics Instead -2. Opt Out Problematic Methods with `where Self: Sized` -3. Return Boxed Trait Objects Instead of `Self` -4. Split Into Two Traits +1. Use generics instead +2. Opt out problematic methods with `where Self: Sized` +3. Return boxed trait objects instead of `Self` +4. Split into two traits -Let's look at each of these in detail. Each approach comes with different tradeoffs. Depending on the kind of dyn-compatibility issue, one might fit better than the others, or you might combine a few. +Let's look at each of these in detail. ### Fix #1: Use Generics Instead One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types *at compile time*, so the compiler knows the size of `Self`. -Basically, the compiler will generate a separate version of the function for each type that implements the trait. Then at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). +Basically, the compiler will generate a separate copy of the function for each concrete type that implements the trait. +Then, at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). The compiler always knows which type it is dealing with, so it can pick the right method to call. Our trait stays the same: @@ -185,7 +184,7 @@ But now we change the function which uses the trait to use generics instead of ` ```rust // Instead of: fn show_widget(widget: &dyn Widget) -// Use generics: +// use generics: fn show_widget(widget: &W) { widget.draw(); let copy = widget.duplicate(); @@ -201,7 +200,7 @@ The difference is that with generics, the compiler knows the concrete type at co For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! -The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. +The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. Your binary size might also grow because of all the copies of the function that the compiler generates for each concrete type. {% info(title="What's the benefit of fully leaning on dynamic dispatch?", icon="crab") %} @@ -257,7 +256,7 @@ So you keep most of the flexibility of trait objects (unlike with generics), as We can change the return type of the problematic method to return a boxed trait object instead of `Self`. -This works because `Box` has a known size at compile time. It's a pointer to an object on the heap. (It's actually a *fat pointer*: two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable; more on that later.) What matters is that this size is fixed and known at compile time, unlike `Self`, which varies based on the concrete type. +This works because `Box` has a known size at compile time. It's a pointer to an object on the heap. It's actually a *fat pointer*: two words wide, or 16 bytes on a 64-bit system, because it also stores a pointer to the vtable; more on that later. What matters is that this size is fixed and known at compile time, unlike `Self`, which varies based on the concrete type. ```rust trait Widget { @@ -296,7 +295,7 @@ fn main() { } ``` -One cost: `Box` tends to be viral in your codebase. You'll end up writing `Box` more often than you'd like, which gets noisy. +The downside is that `Box` tends to be viral in your codebase. You'll end up writing `Box` more often than you'd like, which gets noisy. On top of that, this fix only works for methods that return `Self`. If your trait also has static methods or generic methods, you'll need to combine this approach with one of the other fixes. @@ -305,13 +304,13 @@ If your trait also has static methods or generic methods, you'll need to combine Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. -Maybe your code is silently trying to tell you that you are mixing up two different concepts and that they should be untangled. +Maybe your code is silently trying to tell you that you are mixing up two different responsibilities and that they should be untangled. In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! -Instead, we lean on composition and focus on behavior rather than mangling multiple responsibilities into a single trait. +Instead, we lean on composition and focus on behavior rather than mangling multiple ideas into a single trait. -Here's a more realistic example: separating rendering from widget creation. Factory methods are often static (no `self` parameter), which makes them incompatible with `dyn`. So we split them into separate traits. +Here's a more realistic example: separating rendering from widget creation. Factory methods are often static (no `self` parameter), which makes them incompatible with `dyn`. So we split them off into a separate trait. ```rust // This trait can be used with dyn @@ -319,7 +318,7 @@ trait Widget { fn draw(&self); } -// Separate trait for creating widgets - can't be used with dyn +// Separate trait for creating widgets. Can't be used with `dyn` trait WidgetFactory { fn create(label: String) -> Self; // No self parameter! } From fe43afb8262594328691b7aeff2619d7bcc6531e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Jul 2026 17:13:59 +0200 Subject: [PATCH 23/24] update --- content/blog/dyn-compatibility/index.md | 52 ++++++++++++------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index c36eaca0..b07b0f81 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -1,6 +1,6 @@ +++ title = "Understanding Dyn Compatibility" -date = 2026-07-28 +date = 2026-07-29 draft = false template = "article.html" [extra] @@ -19,41 +19,35 @@ resources = [ In Rust, some traits can't be used as trait objects with `dyn Trait`. -When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." - +When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." [^object-safety] This has an impact on how you can use these traits in your code. -Dyn compatibility is based on a set of rules that determine whether a trait can be turned into a trait object. -Once you understand why these rules exist, you'll know how to get around them by choosing a better design for your trait. +[^object-safety]: The concept used to be called "object safety" until Rust 1.84.0. If you're reading older resources, they mean the same thing. The name got changed because it was confusing. + + "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. + The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch, although I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. + +I think that's one are where the Rust compiler could print a more helpful error message. + Fixing the issue is mostly about tradeoffs between compile-time generics and runtime polymorphism and learning when each one fits. -This lets you write more deliberate, flexible Rust. +Once you understand the concept, you'll know how to get around the issues by choosing a better design for your trait. -{% info(title="Quick Summary", icon="crab") %} +{% info(title="Quick Help", icon="crab") %} If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. To fix it, pick one: -- use generics instead of `&dyn Trait` - add `where Self: Sized` to the offending method - return `Box` instead of `Self`, or +- use generics instead of `&dyn Trait` - split the trait in two Continue reading to understand the tradeoffs between each approach. {% end %} -{% info(title="Dyn Compatibility and Object Safety", icon="crab") %} - -This concept used to be called "object safety" until Rust 1.84.0. -If you're reading older resources, they mean the same thing. -The name got changed because it was confusing. - -"Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. -The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch, although I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. - -{% end %} ## The Error Message @@ -117,7 +111,7 @@ note: for a trait to be dyn compatible it needs to allow building a vtable = help: only type `Button` implements `Widget`; consider using it directly instead. ``` -That's a really good error message, but it might still sound pretty confusing in the beginning. +That all sounds sounds pretty confusing. - What does "not dyn compatible" mean? - Shouldn't the `dyn` part take care of it? @@ -382,7 +376,12 @@ If a trait has dispatchable methods that return `Self` or have generic parameter That is the root cause of dyn compatibility issues. -A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility): +A trait is **dyn compatible** if it follows [a list of rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility). + +

+ +Click here for the full list. + | Rule | Why? | |------|------| @@ -396,20 +395,19 @@ A trait is **dyn compatible** if it follows [these rules](https://doc.rust-lang. | No `Self` return type on dispatchable methods | The caller needs to know the return value's size and type, but `Self` could be any implementor | | No opaque return type on dispatchable methods | `async fn` and return-position `impl Trait` hide a concrete return type that must be known statically | | Non-dispatchable methods must opt out | A method that violates the dispatch rules can still live on the trait if it has `where Self: Sized`, making it unavailable through `dyn Trait` | +
-That's quite a lot of rules, but they all boil down to the same core issue: +The rules boil down to the same core issue: **the `dyn Trait` interface must have a finite, statically-known shape even though the concrete implementor behind it is hidden.** -If you ever need the gory details (the exact, normative list of what makes a trait dyn compatible), the [dyn compatibility section of the Rust Reference](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility) is the source of truth. The rules above are the gist; the spec is the fine print. - {% info(title="A Modern Gotcha: `async fn` in Traits", icon="crab") %} Since [Rust 1.75](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/), you can write `async fn` directly in a trait. But there's a catch: a trait with an `async fn` is **not dyn compatible**. -The reason fits right into what we've seen. An `async fn` desugars to a regular method that returns `impl Future<...>`, a hidden return-position `impl Trait`. -Opaque return types aren't dispatchable, so the trait can't be used behind `dyn`. +The type is called "opaque", because we don't know what it is, and the compiler doesn't expose it to us. +Opaque return types aren't dispatchable (which means we can't put them in a vtable of functions), so the trait can't be used behind `dyn`. If you need dynamic dispatch with async methods today, you have a few options: @@ -435,7 +433,7 @@ Which fix to reach for depends on what your trait needs and what you're willing | Fix | Reach for it when... | The tradeoff | |-----|----------------------|--------------| -| **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times | +| **#1 Generics** (``) | You don't actually need trait objects (the concrete type is known at each call site) and you won't mix different types in one collection | Static dispatch only; monomorphization can grow code size and compile times and generic parameters need to be passed around in your API | | **#2 `where Self: Sized`** | You want to keep using `dyn Widget`, and the problematic method only ever needs to be called on concrete types | That method isn't callable through `dyn`; tightening the bound later is a breaking change | | **#3 Return `Box`** | The method returns `Self` and you really need it through a trait object (e.g. a heterogeneous `Vec>`) | A heap allocation per call, and `Box` tends to spread through your API | | **#4 Split into two traits** | The trait mixes dispatchable behavior with non-dispatchable bits, like static factory methods | More traits to keep track of, though that separation often helps | @@ -453,6 +451,6 @@ If you do, too, here are some resources to dig deeper: - 2015-01-03: [RFC 546](https://rust-lang.github.io/rfcs/0546-Self-not-sized-by-default.html) - Removed implied `Sized` bound on traits - 2023-08-24: [Rust 1.72](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) - GATs can be opted out with `where Self: Sized` - 2023-12-28: [Rust 1.75.0](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0/) - Stabilized `async fn` and return-position `impl Trait` in traits (though such traits still aren't dyn compatible) -- 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - The docs had moved from "object safety" to "dyn compatibility" around this release cycle; the [tracking issue](https://github.com/rust-lang/rust/issues/130852) notes that the rename missed the release notes. +- 2025-01-09: [Rust 1.84.0](https://blog.rust-lang.org/2025/01/09/Rust-1.84.0/) - The docs had moved from "object safety" to "dyn compatibility" around this release cycle; the [tracking issue](https://github.com/rust-lang/rust/issues/130852#issuecomment-2947417189) notes that the rename unfortunately missed the release notes. The lang team also wants a "practical path" to call `async fn`s through `dyn Trait` natively. It's on the [2026 project goals](https://github.com/rust-lang/rfcs/blob/master/text/3935-Project-Goals-2026.md), so the async gotcha above should ease over time. From 1a8c06e47b6de9b95d1e9e4306d25785a579b8ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Jul 2026 17:17:33 +0200 Subject: [PATCH 24/24] Clean up dyn compatibility article --- content/blog/dyn-compatibility/index.md | 54 +++++++++++++------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/content/blog/dyn-compatibility/index.md b/content/blog/dyn-compatibility/index.md index b07b0f81..0860957e 100644 --- a/content/blog/dyn-compatibility/index.md +++ b/content/blog/dyn-compatibility/index.md @@ -12,7 +12,7 @@ resources = [ "[The Rust Reference: Dyn Compatibility](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility)", "[The Rust Reference: Trait Objects](https://doc.rust-lang.org/reference/types/trait-object.html)", "[Rust error E0038: dyn compatibility](https://doc.rust-lang.org/error_codes/E0038.html)", -"[Two Ways To Do Dynamic Dispatch](https://www.youtube.com/watch?v=wU8hQvU8aKM) - Video by Logan Smith, which explains dyn dispatch from first principles" +"[Two Ways To Do Dynamic Dispatch](https://www.youtube.com/watch?v=wU8hQvU8aKM) - Video by Logan Smith, which explains dyn dispatch from first principles", ] +++ @@ -20,26 +20,26 @@ resources = [ In Rust, some traits can't be used as trait objects with `dyn Trait`. When a trait can't be used with dynamic dispatch, we say it's "not dyn compatible." [^object-safety] -This has an impact on how you can use these traits in your code. +This has an impact on how you can use these traits in your code. [^object-safety]: The concept used to be called "object safety" until Rust 1.84.0. If you're reading older resources, they mean the same thing. The name got changed because it was confusing. "Object safety" suggests that Rust has "objects" in the traditional OOP sense and that the term is about "safety", which is misleading. - The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch, although I personally don't like either of the terms, but to be honest, I also can't think of a better name that is both short and accurate. - -I think that's one are where the Rust compiler could print a more helpful error message. - + The new term "dyn compatibility" does a better job of saying that it's about whether a trait can be used with `dyn Trait` for dynamic dispatch. I still don't love either term, but I also can't think of a better name that is both short and accurate. + +I think that's one area where the Rust compiler could print a more helpful error message. + Fixing the issue is mostly about tradeoffs between compile-time generics and runtime polymorphism and learning when each one fits. Once you understand the concept, you'll know how to get around the issues by choosing a better design for your trait. {% info(title="Quick Help", icon="crab") %} -If the compiler told you a trait is **"not dyn compatible"** your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. +If the compiler told you a trait is **"not dyn compatible"**, your trait can't be used as `dyn Trait` because it has a method that can't go through dynamic dispatch, usually one that returns `Self`, takes no `self`, or is generic. To fix it, pick one: - add `where Self: Sized` to the offending method -- return `Box` instead of `Self`, or +- return `Box` instead of `Self` - use generics instead of `&dyn Trait` - split the trait in two @@ -53,7 +53,7 @@ Continue reading to understand the tradeoffs between each approach. Here's an example with code that **won't compile**. -Say you have a trait `Widget` which has a method that returns a copy of itself: +Say you have a trait `Widget` that has a method returning a copy of itself: ```rust trait Widget { @@ -73,7 +73,7 @@ impl Widget for Button { fn draw(&self) { // ... } - + fn duplicate(&self) -> Self { Button { label: self.label.clone() } } @@ -111,7 +111,7 @@ note: for a trait to be dyn compatible it needs to allow building a vtable = help: only type `Button` implements `Widget`; consider using it directly instead. ``` -That all sounds sounds pretty confusing. +That all sounds pretty confusing. - What does "not dyn compatible" mean? - Shouldn't the `dyn` part take care of it? @@ -161,7 +161,7 @@ Let's look at each of these in detail. One common way to fix the problem is to use generics instead of trait objects. Generics resolve to concrete types *at compile time*, so the compiler knows the size of `Self`. -Basically, the compiler will generate a separate copy of the function for each concrete type that implements the trait. +The compiler generates a separate copy of the function for each concrete type that implements the trait. Then, at runtime, you no longer need to worry about any dynamic dispatch (which means "figuring out the type at runtime"). The compiler always knows which type it is dealing with, so it can pick the right method to call. @@ -187,14 +187,14 @@ fn show_widget(widget: &W) { ``` Note how we changed the function signature to use a generic type parameter `W` that implements the `Widget` trait. -Here we tell Rust: "I have some type `W` that implements `Widget`, and I want to use it." and Rust will happily generate all the necessary code for each type used. +Here we tell Rust: "I have some type `W` that implements `Widget`, and I want to use it." Rust then generates the necessary code for each type used. That's close to using `&dyn Widget`, but not quite the same. The difference is that with generics, the compiler knows the concrete type at compile time, so it can handle `Self` correctly. For instance, we might know that `W` is `Button` in this case, so `duplicate` returns a `Button`. Now the confusion about what `Self` means is gone! -The downside is that you can't fully lean on dynamic dispatch anymore and that you might have to refactor a lot of code if you were using trait objects extensively before. Your binary size might also grow because of all the copies of the function that the compiler generates for each concrete type. +The downside is that you can't fully lean on dynamic dispatch anymore, and you might have to refactor a lot of code if you were using trait objects extensively before. Your binary size might also grow because of all the copies of the function that the compiler generates for each concrete type. {% info(title="What's the benefit of fully leaning on dynamic dispatch?", icon="crab") %} @@ -216,9 +216,11 @@ Another option is to keep using trait objects but change the problematic method ```rust trait Widget { fn draw(&self); - + // Only available when the concrete type is known - fn duplicate(&self) -> Self where Self: Sized; + fn duplicate(&self) -> Self + where + Self: Sized; } ``` @@ -231,14 +233,14 @@ You won't be able to call `duplicate` on `&dyn Widget`, but you can still call i ```rust fn main() { let button = Button { label: "Click me".to_string() }; - + // Can use as trait object now! let widget: &dyn Widget = &button; widget.draw(); // ✅ Works - + // ❌ Can't call this on trait objects - // widget.duplicate(); - + // widget.duplicate(); + // ✅ But duplicate still works on concrete types: let button2 = button.duplicate(); } @@ -268,7 +270,7 @@ impl Widget for Button { fn draw(&self) { println!("Button: {}", self.label); } - + fn duplicate(&self) -> Box { Box::new(Button { label: self.label.clone() }) } @@ -280,7 +282,7 @@ fn main() { Box::new(Button { label: "Click me".to_string() }), Box::new(Button { label: "Submit".to_string() }), ]; - + for widget in &widgets { widget.draw(); let copy = widget.duplicate(); @@ -298,7 +300,7 @@ If your trait also has static methods or generic methods, you'll need to combine Sometimes the best solution is to separate the dyn-compatible methods from the problematic ones into different traits. -Maybe your code is silently trying to tell you that you are mixing up two different responsibilities and that they should be untangled. +Maybe your code is silently trying to tell you that you are mixing up two different responsibilities and that they should be untangled. In general, prefer smaller, focused traits over large, monolithic ones. Traits are not interfaces! @@ -364,7 +366,7 @@ As you can see, a trait object has: 2. A **vtable pointer** that points to a table of function pointers for the methods The [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) is created at compile time and contains pointers to the methods for the specific type. -It is a concept that is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. +It is common in many programming languages that support dynamic dispatch, such as C++, C#, or D. When you call a method on a trait object, Rust uses the vtable to look up the correct function to call based on the actual type of the data. For dynamic dispatch to be sound, the vtable-facing methods need stable, concrete function signatures: @@ -376,7 +378,7 @@ If a trait has dispatchable methods that return `Self` or have generic parameter That is the root cause of dyn compatibility issues. -A trait is **dyn compatible** if it follows [a list of rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility). +A trait is **dyn compatible** if it follows [a list of rules](https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility).
@@ -427,7 +429,7 @@ If you need dynamic dispatch with async methods today, you have a few options: 4. The compiler must guarantee type safety at all times, even if it can't see the concrete type If your trait is not dyn compatible, don't worry! Many standard library traits (`Clone`, `Default`, etc.) are also not dyn compatible. -As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. +As we've seen, there are ways to work around these limitations with type erasure, generics, or more fine-grained traits. Which fix to reach for depends on what your trait needs and what you're willing to give up: