Skip to content
Corrode Rust Consulting

Corrode Rust Consulting

corrode is a friendly Rust consulting company based in Düsseldorf, Germany. We help you build reliable software and ship it to production. We offer consulting, training, and development services for Rust projects. Provided by Matthias Endler.

corrode.dev
Corrode Rust ConsultingCorrode Rust Consulting
SOFTWARE AND DATA ENGINEERING

Practical Rust API Design

A good metric for ergonomic systems design is how much of a program you have to keep in your head at once to know what’s going on. It’s empowering if you can understand a function by its type signature and get immediate feedback on whether you used it correctly. Other times, it feels like you’re a code archaeologist: Was this value validated before? Is it safe to retry this call? Can this panic? Bad APIs make local reasoning hard. “Local reasoning” here means being able to understand a piece of code from a limited amount of surrounding context and the contracts of the APIs it uses. The main point is that you can rely on those contracts without additional knowledge of the implementation. What makes Rust feel different from other languages is the ability to encode invariants in the type system and do so at zero cost. The combination of both properties is rare. Once you notice this “local reasoning principle”, you’ll see it everywhere in Rust’s standard library: through the use of Result and Option, borrows marked with &, enums to represent a closed set of possibilities, or explicit unsafe blocks. This information is always visible in every function signature. A simple way to apply this mindset yourself is to check if your function signatures communicate as much information as possible to the caller. Maybe show the signature to a friend or colleague and ask them to explain what it does. It can be eye-opening. Let’s look at a few examples. ## Put Relationships in the Signature Consider this function signature: fn first_line(text: &str) -> Option<&str> Before even looking at the implementation, we already know that the function takes a borrowed input and may have no output. (Or, rather, it may return None.) Actually, if you know Rust’s lifetime elision rules, you know that the real signature is: fn first_line<'text>(text: &'text str) -> Option<&'text str> The input is tied to the output. That implies that the caller can’t keep using the returned string after the borrow of text has ended. And by extension, the function can’t return a temporary string either. The function is not making any long-lived allocations. That’s a lot of useful information for just one function header! All of that is great, but equally important, there are a few “implicit” assumptions that the signature does not guarantee. Take the function name: it suggests that the function returns “the first line of something.” However, the type system does not enforce that. It could return the last line for all we know, and the signature would be identical. The function header also won’t tell us whether anything is logged, how it performs, or whether it panics. Rust is often described as an explicit language. Yet it also uses type inference, lifetime elision, automatic borrowing of method receivers, and implicit coercions. That’s because writing everything out would make many programs harder to read. What you should make explicit depends on your context. A good rule of thumb when designing an API is to look for relationships that callers would otherwise have to keep in their heads. Does your function really take any &str, or does it expect a string that has been validated in some way? For example, is an empty string valid input? What does None mean in the return value? Should it be treated as an error, or is it a valid case? Should it be a Result instead? Good function signatures tell a story about the relationships between inputs and outputs. ## Keep Consequential Choices Visible Consider these two calls, where text is a String: inspect(&text); consume(text); The first call borrows text while the second one moves it. Now let’s look at the context of the borrowed call: fn inspect(text: &str) { println!("{text}"); } let text = String::from("hello"); inspect(&text); The compiler applies a deref coercion automatically. I think that’s a good compromise: the ownership decision is still visible, but the compiler handles the bookkeeping. Some people argue that we could go one step further. Why not automatically borrow an owned argument in an ordinary function call? We could then write inspect(text) instead of inspect(&text), which seems convenient. But, as always, there’s a cost to convenience. Saving the & would remove information readers currently get from the expression itself: that the value does not move.[1] Convenience does not always mean better ergonomics. This gives us a way to judge our own conveniences, too. Implementing Deref for a wrapper makes its target’s methods available implicitly. That’s convenient. But it’s a slippery slope. It can lead to leaky abstractions, where a wrapper type is treated as if it were the underlying type, but is not quite the same. Suppose a UserId stores a String. You might consider implementing Deref<Target = str> for it, so that callers can use it as if it were a &str. Now you expose the whole string interface through Deref. You implicitly allow callers to treat the identifier as text, sidestepping all type invariants. Instead, an explicit as_str() leaves a visible point where the identifier is treated as text. It also lets your UserId newtype have an API of its own. Interaction with user IDs becomes more deliberate. Use Deref only when the wrapper transparently behaves like its target and dereferencing is cheap and unsurprising. The standard library agrees. ## Give Names a Purpose Take a look at this function signature: fn visit<F: FnMut(&str)>(callback: F) Here, we introduce a name, F, give it a bound of FnMut(&str), and then use it exactly once for callback. I think that signature would be clearer if we put the requirement right next to where it’s used: fn visit(callback: impl FnMut(&str)) Now you can read the signature from left to right. This saves you from jumping back and forth just to figure out what F means. One less thing to keep in your head while reading.[2] Now, I’m not saying that version two is always better. For example, it can be helpful to keep generics separate from the rest of the signature when the generic type is used in multiple places: fn choose<T>(first: T, second: T, take_first: bool) -> T { if take_first { first } else { second } } Here, T tells us that both arguments as well as the return value have the same type. The same thinking applies to return types: fn nonempty(lines: &[String]) -> impl Iterator<Item = &str> { lines.iter().map(String::as_str).filter(|s| !s.is_empty()) } impl Iterator<Item = &str> tells the caller what they can do with the result, namely iterate over borrowed strings. (Besides, if you tried writing out the concrete return type of that function, it would be unnecessarily long and complicated.) Ask yourself: does naming this type help the caller understand something? ## Ownership Beyond Memory We usually learn about ownership in the context of memory. But the same rules apply in other situations. Take file descriptors, for example. On Unix, a raw file descriptor is just an integer. That integer doesn’t tell you whether the descriptor is still open, or who’s responsible for closing it. Worse, once it’s closed, the operating system can reuse the number for something else. Consider these two signatures, using types from std::os::fd: fn inspect(fd: RawFd) -> std::io::Result<()> fn inspect(fd: BorrowedFd<'_>) -> std::io::Result<()> The first signature only gives us an integer. RawFd is literally just an alias for c_int. But that descriptor might already be closed. Those “time-of-check to time-of-use” bugs are a common pitfall of safe Rust. The second guarantees that the descriptor remains open for the duration of the borrow.[3] The caller keeps its owner alive, and Rust checks that relationship when we borrow from a File: use std::fs::File; use std::os::fd::AsFd; let file = File::open("notes.txt")?; inspect(file.as_fd())?; Now the compiler can help! You can’t drop file and then keep using the descriptor borrowed from it in safe Rust. You no longer need to search through the code to check whether someone closed it earlier. But what if our function should really take ownership of the file descriptor? Use OwnedFd instead. When the OwnedFd is dropped, the descriptor is closed automatically, so the caller doesn’t have to remember to do it. In a sense, memory and file descriptors share a similar set of types with different guarantees: | Memory | File descriptors | Use Case | | ----------- | ---------------- | ------------------------------------------------------------------------------------- | | Box<T> | OwnedFd | “I want to own this resource and close it when I’m done.” | | &T | BorrowedFd<'a> | “I want to borrow this resource for a limited time.” | | Raw pointer | RawFd | “I want to use this resource, but I don’t know who owns it or how long it will live.” | Lock guards are another example. { let _guard = lock.lock(); // do cool things with lock } // We no longer have access to the lock here, because `_guard` was dropped. If you can access the data, you hold the lock. You don’t have to “trace the program back” to an earlier lock() call and check every path for an unlock. In C, that’s very much the case and easy to get wrong. Of course, these types only guarantee what they encode. A BorrowedFd keeps track of one borrow, but it doesn’t guarantee exclusivity over the underlying resource. That means another process might still be writing to the same file. But in general, you can stop relying on callers to remember the provenance of a resource. That’s a much stronger guarantee than simply saying “keep this open until you’re done” in your API documentation. Raw handles are still necessary at a low-level boundary, but you don’t have to pass them through your entire application. The lesson is that you can encapsulate ownership information in your own types and provide a safe wrapper around an unsafe API. ## Explain Who Is Responsible Sometimes things are truly outside of Rust’s control. In that case, the safety responsibility shifts to the user. We use unsafe APIs to make that inversion of responsibility explicit. In Rust, there are two different responsibilities, which share the same keyword: - An unsafe fn says: “Before you call me, you MUST establish these conditions. This is your responsibility.” - An unsafe block means you’re responsible for satisfying the conditions of the unsafe operations inside the block.[4] The difference is that an unsafe fn sets safety conditions that its caller must meet, while an unsafe block marks where the person writing the code claims that each unsafe operation’s safety conditions have been met. In both cases, it is good practice to add safety comments to make readers aware of these conditions. Here’s how that could look in practice: /// # Safety /// `index` must be less than `values.len()`. unsafe fn element_unchecked(values: &[u8], index: usize) -> u8 { // SAFETY: The caller guarantees that `index` is in bounds. unsafe { *values.get_unchecked(index) } } In general, you should use get(index) instead to handle the None case, but this example illustrates how to document safety obligations. Since the type system can’t check the safety conditions, it’s your obligation to keep the documentation up to date. Suppose someone adds another unsafe operation to this function later. Does knowing that index is in bounds make that operation safe, too? Maybe. You have to check and potentially update your docs. Quick tip: when you write a safety comment, explain why the operation is safe. “This is safe” doesn’t help the next person, but “The caller guarantees that the index is in bounds” gives them something they can check. Another tip: in Rust 2024, unsafe operations inside unsafe functions warn by default unless you put them in an explicit unsafe block. You can enforce that with #![deny(unsafe_op_in_unsafe_fn)]. ## Don’t Make Callers Do Your Work You’ve probably heard the advice to panic for programmer errors and return Result for recoverable failures. That’s reasonable, but who decides what counts as a programmer error? You do, when you design the API.[5] Consider the difference between indexing and get: let item = items[index]; let item = items.get(index); If you use indexing, you need to know that the index is valid to avoid a panic. With get, you can try the lookup and handle None if it fails. Remember that both are safe Rust: an invalid index doesn’t cause undefined behavior in either case. Now suppose the index comes from user input. Is an out-of-bounds index really a bug in your program? Or is it something you should expect and handle? One escape hatch is to make every caller check the index before calling your function. A strict precondition might make your work simpler, but think about your users. Before you document another thing the caller “must” do, ask whether your API could handle it instead. For example, you could return an Option and let callers decide what to do next. That doesn’t mean you should avoid indexing altogether. If an index is indeed valid by construction, indexing can express that assumption directly. A panic then means there’s a bug in your API. It’s probably fine to panic in that case, instead of introducing undefined behavior. And often it’s possible to sidestep these issues entirely. For example, if you need to visit each element of a collection, use an iterator. This way, you don’t have to worry about indices at all. ## Decide What You Want to Promise Another mental model for building great APIs is to think about what you want to guarantee to your users. Remember Hyrum’s Law: > With a sufficient number of users of an API, > it does not matter what you promise in the contract: > all observable behaviors of your system > will be depended on by somebody. With that in mind, think about what happens when you try to change your API. For example, users can match on every variant of your public enum. They can’t forget a case, because the compiler will warn them about it. That’s local reasoning at work, which is great. But on the flip side, it also means that you can’t add a variant without breaking their code. You’ve broken a guarantee they depended on. To prevent that, mark your enum as #[non_exhaustive] so you can add variants later: #[non_exhaustive] pub enum ServiceError { Unavailable, Rejected, } In that case, users have to include a fallback when matching the enum.[6] You’ve pushed the responsibility to the call site, which is likely the better place to decide what to do with an unexpected variant. Exhaustive matching keeps working inside your own crate. Should you add #[non_exhaustive] to every public enum just in case? No. If the set of possibilities really is closed, exhaustive matching gives users a useful guarantee. Don’t take it away without a reason. Examples of closed sets include days of the week, months of the year, or the suits of a deck of cards: pub enum Suit { Hearts, Diamonds, Clubs, Spades, } If you make this enum non-exhaustive, users can’t use your crate to implement a card game without having to handle an impossible case. The awkward middle ground is a set that looks closed but really isn’t. HTTP status codes are a good example: mapping all the standard codes doesn’t mean you’re safe from a vendor inventing their own codes. This caused a real problem in http-types: a user reported that constructing a response with Cloudflare’s custom status codes panicked because the library’s StatusCode enum couldn’t represent them. #[non_exhaustive] doesn’t solve that problem by itself. It lets you add additional variants in the future, but it doesn’t allow users to represent unknown codes today. How about we add Unregistered(u16)? #[non_exhaustive] pub enum Status { Ok, NotFound, // Other known status codes we can't name yet... Unregistered(u16), } Now users can handle codes which don’t have a name yet: fn is_early_hints(status: Status) -> bool { match status { Status::Unregistered(103) => true, _ => false, } } But that introduces another compatibility trap. Suppose a later release adds an EarlyHints variant and starts returning it for code 103. The function now returns false for the same HTTP status code. The code still compiles, but its behavior has changed. I recommend reading “Pattern Matching and Backwards Compatibility” by Sean McArthur, the author of the http crate, about how even an enum with a catch-all variant can make promises you didn’t intend. The escape hatch for the http crate was to make StatusCode an opaque struct with associated constants for the known codes like StatusCode::OK. Users can then construct values from numeric codes, even when the library doesn’t have a name for them: use http::StatusCode; let status = StatusCode::from_u16(599).unwrap(); assert_eq!(status.as_u16(), 599); They can still match on familiar codes, but they have to include a fallback. And they can handle an unnamed code by inspecting its numeric value: use http::StatusCode; fn describe(status: StatusCode) -> &'static str { match status { StatusCode::OK => "success", code if code.as_u16() == 103 => "early hints", _ => "some other status", } } That’s pretty clever, because adding a new constant like StatusCode::EARLY_HINTS assigns a name to a value without changing the underlying representation, and numeric checks continue to work. So before making something public, ask yourself: am I willing to uphold this guarantee forever? This applies to your entire public API, including enums and public fields inside structs. Changing things later can break user code. From their perspective, it was part of the API all along. ## Try It on Your Own APIs I suggest you put that advice into practice. Pick a function in your codebase and look at it from the caller’s perspective. What do you have to know to use it correctly? Can you get that information from the signature, or do you have to read the implementation first? Look for instructions in the documentation that the compiler could help enforce. Instead of writing “keep this resource alive”, maybe you can return a borrowed type. “Only pass validated text” might become a newtype with some validation in the constructor. You don’t have to follow every single suggestion in this article, either. The goal is to make your API easier to understand and harder to misuse. Where to go from here Want a second pair of eyes on your Rust APIs? I can help you put these patterns to work in your codebase. - For your team. Training, code review, and architecture support to ship Rust with confidence. - For yourself. 1-on-1 mentorship for Rust design, architecture, and code review on real projects. 1. RFC 241: Deref coercions discusses why automatically borrowing arguments would make local reasoning harder. ↩ 2. RFC 1951: Expand impl Trait explains ergonomics in terms of how much you have to keep in your head, and discusses who chooses the concrete type. ↩ 3. RFC 3128: I/O safety explains the analogy between raw handles and raw pointers, and introduces owned and borrowed handle types. ↩ 4. RFC 2585: Unsafe blocks in unsafe functions separates defining safety obligations from satisfying them. The Rust 2024 edition guide covers the lint’s current default. ↩ 5. RFC 236: Error conventions recommends expressing contracts through types where possible, and using Result or Option when a stricter contract is hard to justify. The discussion of task failure predates Rust 1.0; the advice about API contracts is the relevant part here. ↩ 6. RFC 2008: Non-exhaustive enums and structs discusses exhaustive matching and the freedom to add new variants. ↩
RUST
Corrode Rust ConsultingCorrode Rust Consulting
ARTIFICIAL INTELLIGENCE

Rust Conferences 2027

Planning your Rust conference trips for 2027? Here are the events with confirmed dates so far. We’ll add more conferences, ticket prices, and call for proposals (CFP) deadlines as organizers announce them. Come say hi if you see us at any of these events! (We’ll bring Rust in Production stickers.) For this year’s events, see Rust Conferences 2026. ## Q1 2027 ### Rust Nation UK (London, UK) Rust Nation returns to London with a one-day, three-track conference at a new venue in the City of London. A separate workshop day is being planned for February 17, but is not yet confirmed. - When: February 18, 2027 - Where: 1 Basinghall Avenue, London, UK - Format: 1 day of talks across 3 tracks - Focus: Rust developers of all experience levels - Links: Website ## Q2 2027 ### TokioConf (Portland, USA) After its first edition in 2026, TokioConf returns to Portland. It’s a conference for developers working with Tokio, async Rust, and high-performance network applications. - When: April 26-27, 2027 - Where: Portland, Oregon, USA (venue to be announced) - Focus: Async Rust and Tokio - Links: Website | 2027 Announcement ### RustWeek (Utrecht, Netherlands) RustWeek brings the Rust community together in Utrecht for talks, workshops, a hackathon, and social activities. The week also includes the invitation-only Rust Project All Hands. - When: May 24-29, 2027 - Where: Utrecht, Netherlands - Format: 1 day of workshops, 2 days of talks, a hackathon, and social activities - Focus: Broad, open to Rust developers of all levels - CFP: Open — closes January 10, 2027 at 23:59 CET. Workshop proposals are handled separately via email. - Links: Website | CFP Announcement ## Q3 2027 ### RustConf (Vancouver, Canada + Online) Hosted by the Rust Foundation, RustConf heads to Vancouver in 2027, with online participation available too. More details about the program are still to come. - When: September 7-10, 2027 - Where: Vancouver, Canada, and online - Focus: Rust and its open-source ecosystem - Links: Website ## More conferences to watch We haven’t found official 2027 dates for the following conferences yet. These are events to keep an eye on, not confirmed 2027 editions: - Rustikon - Rust in Paris - Rust India - RUSTMEET - Oxidize - EuroRust - RustLab - RustForge ## Want to catch up on past events? Browse our dedicated Rust conference recordings page where you can filter by year, topic, and conference! --- Missing an event? Spot an error? Feel free to edit this list directly or let us know. Check the organizers’ websites for the latest details before booking travel. See you at the next conference! 🦀
NETHERLANDSCANADARUSTRUST FOUNDATION
Corrode Rust ConsultingCorrode Rust Consulting
SPACE AND ASTRONOMY

JetBrains

Welcome to the final episode of this season of Rust in Production. My guest is Orhun Parmaksız from JetBrains, and we talk about building developer tools with Rust. JetBrains is best known for IntelliJ IDEA, Kotlin, and a long line of IDEs for professional software teams. In the Rust world, that now includes RustRover: a commercial IDE built on the IntelliJ platform, with deep Rust support for navigation, refactoring, debugging, testing, and large codebases. This episode is about where Rust fits into that world. We talk about why JetBrains does not plan to rewrite the IntelliJ platform in Rust, why Fleet used Rust for its File System Daemon, how Air builds on parts of Fleet’s architecture, and why JetBrains prefers out-of-process Rust helpers over JNI inside the JVM. We also get into RustRover’s internals: PSI, THIR, MIR-based expression evaluation in the debugger, procedural macro sandboxing, library stubs, parser regression testing, cargo-nextest support, and the practical trade-offs between JetBrains’ indexing model and rust-analyzer’s Salsa-based approach. Proudly Supported by Svix Svix makes it easy to send webhooks reliably at any scale. It handles retries, secure signatures, monitoring, replay, and endpoint management so you can focus on your product. Svix is powered by Rust. Learn more about Svix. ## Show Notes ### Links From The Episode - Ratatui - The terminal UI library that changed Orhun’s life - ratatuefi - A demo showing Ratatui running in UEFI without an operating system kernel - Renaissance of Terminal User Interfaces with Rust - Orhun’s FrOSCon talk about Ratatui and Rust-powered TUIs - lychee - Matthias’ Rust link checker project - Orhun’s Arch Linux packages - A great way to discover Rust projects through packaging work - RustRover - JetBrains’ fully integrated Rust development suite - IdeaVim - Vim mode for JetBrains IDEs - LightEdit mode - Quick startup for simple editing tasks in JetBrains IDEs - Welcome to Fleet! - The original Fleet announcement - Fleet Below Deck, Part I: Architecture Overview - Details on Fleet’s hybrid architecture and Rust-based File System Daemon - JetBrains Air - The agentic successor to Fleet - Rust MIR - The Mid-level IR used as a hook point for RustRover’s debugger integrations - Fewer False Positives in RustRover 2026.1 - Background on JetBrains’ Crate Rover-inspired diagnostics regression work - Crater - The Rust Project’s CI tool for rebuilding public crates and catching regressions - rust-analyzer - The Rust IDE backend solving many similar problems from a different architecture - Code in Rust with RustRover - Vitaly Bragilevsky’s RustRover talk - How Rust IDEs Understand Code - Recap of the RustRover and rust-analyzer livestream with Lukas Wirth and Vlad Beskrovny - Salsa - The incremental computation framework used by rust-analyzer - RustRover licenses for open source - One way JetBrains supports open-source projects - Rust Berlin Talks at JetBrains - Berlin Rust meetup hosted at the JetBrains office - JetBrains Academy plugin - Learn Rust inside RustRover and other JetBrains IDEs - Rust in Production: Rust with Niko Matsakis - The Rust Project episode mentioned in this conversation - Rust Commercial Network - JetBrains and Ratatui are both part of the network - cargo-nextest - An alternative Rust test runner with benefits over plain cargo test - What’s New in RustRover 2026.2 - The latest RustRover release with tighter Axum and reqwest integration - PhpStorm - Matthias’ first experience with JetBrains products - Zig 2026: No-AI Policy, $670K Foundation, Left GitHub & Why Zig Isn’t 1.0 - JetBrains’ interview with Andrew Kelley - Interactive declarative macro tester - A RustRover feature for better macro debugging during development - Grindhouse - A joke turned into a flourishing community - Terminal Tuesdays - Orhun’s terminal-focused livestreams and interviews - Terminal Collective Discord - The terminal community Discord server - Ratty - A fun GPU-rendered terminal emulator with inline 3D graphics ### Official Links - JetBrains Website - RustRover - JetBrains Open Source Support - Orhun’s Website - Orhun on GitHub - Orhun on YouTube - Orhun on Bluesky - Orhun on LinkedIn
ORHUN PARMAKSIZVLAD BESKROVNY
githubgithubFerrous SystemsThe JetBrains Blog
Corrode Rust ConsultingCorrode Rust Consulting
CIVIL RIGHTS AND SOCIAL INEQUALITY

Understanding Dyn Compatibility

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.” [1] This has an impact on how you can use these traits in your code. 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. Quick Help 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<dyn Trait> instead of Self - use generics instead of &dyn Trait - split the trait in two Continue reading to understand the tradeoffs between each approach. ## The Error Message Here’s an example with code that won’t compile. Say you have a trait Widget that has a method returning a copy of itself: trait Widget { fn draw(&self); fn duplicate(&self) -> Self; // Returns a copy of itself } …and there’s a button, which implements Widget: struct Button { label: String, } impl Widget for Button { fn draw(&self) { // ... } fn duplicate(&self) -> Self { Button { label: self.label.clone() } } } fn show_widget(widget: &dyn Widget) { // This works widget.draw(); // This produces an error because duplicate returns `Self` let copy = widget.duplicate(); copy.draw(); } If you tried to compile this code, you’d get an error like this: 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 | note: for a trait to be dyn compatible it needs to allow building a vtable for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility> --> src/main.rs:3:28 | 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 all sounds pretty confusing. - 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? ## 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. 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<Self>, 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. 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. 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. ## 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 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 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. 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. Our trait stays the same: 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: // Instead of: fn show_widget(widget: &dyn Widget) // use generics: fn show_widget<W: 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.” 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 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. What’s the benefit of fully leaning on dynamic dispatch? 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 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. ### 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. trait Widget { fn draw(&self); // Only available when the concrete type is known fn duplicate(&self) -> Self where Self: Sized; } 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’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. 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(); } 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<dyn Widget> 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. trait Widget { fn draw(&self); fn duplicate(&self) -> Box<dyn Widget>; // Returns trait object instead of Self } struct Button { label: String, } impl Widget for Button { fn draw(&self) { println!("Button: {}", self.label); } fn duplicate(&self) -> Box<dyn Widget> { Box::new(Button { label: self.label.clone() }) } } fn main() { // Now we can use trait objects! let widgets: Vec<Box<dyn Widget>> = 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(); } } The downside is that Box<dyn> tends to be viral in your codebase. You’ll end up writing Box<dyn Widget> 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. ### 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 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 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 off into a separate trait. // This trait can be used with dyn trait Widget { fn draw(&self); } // Separate trait for creating widgets. Can't be used with `dyn` trait WidgetFactory { fn create(label: String) -> Self; // No self parameter! } struct Button { label: String, } impl Widget for Button { fn draw(&self) { println!("Button: {}", self.label); } } impl WidgetFactory for Button { fn create(label: String) -> Self { Button { label } } } fn main() { // 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 // Can't do this: let factory: &dyn WidgetFactory = ... // But that's fine - factories work at compile time } ## 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”): ┌─────────────────┐ │ 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. 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: - 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 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. A trait is dyn compatible if it follows a list of rules. Click here for the full list. | Rule | Why? | | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 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<Self>, Rc<Self>, Arc<Self>, or Pin<P> 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 | 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. A Modern Gotcha: async fn in Traits Since Rust 1.75, you can write async fn directly in a trait. But there’s a catch: a trait with an async fn is not dyn compatible. An async fn desugars to a regular method that returns impl Future<...>, a hidden return-position impl Trait. 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: - Box the future yourself and return Pin<Box<dyn Future<Output = ...>>>. - Use the async-trait crate, which does that boxing for you. - Use the dynosaur crate, which generates a dyn-compatible wrapper for traits with async fn. ## 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 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. 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 (<W: Widget>) | 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<dyn Widget> | The method returns Self and you really need it through a trait object (e.g. a heterogeneous Vec<Box<dyn Widget>>) | A heap allocation per call, and Box<dyn> 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 | | async-trait / dynosaur | Your trait has async fns 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. ### Historical 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 - Introduced object safety (2014, before Rust 1.0) - 2014-11-03: Issue #428 - Object-safety and static methods - 2015-01-03: RFC 546 - Removed implied Sized bound on traits - 2023-08-24: Rust 1.72 - GATs can be opted out with where Self: Sized - 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 - The docs had moved from “object safety” to “dyn compatibility” around this release cycle; the tracking issue notes that the rename unfortunately missed the release notes. The lang team also wants a “practical path” to call async fns through dyn Trait natively. It’s on the 2026 project goals, so the async gotcha above should ease over time. 1. 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. I still don’t love either term, but I also can’t think of a better name that is both short and accurate. ↩