Skip to content

docs: object safety → dyn compatibility, and fix the rule list in rust-patterns-book ch02 - #120

Open
Abinoam P. Marques Jr. (abinoam) wants to merge 9 commits into
microsoft:mainfrom
abinoam:fix/dyn-compatibility-terminology
Open

docs: object safety → dyn compatibility, and fix the rule list in rust-patterns-book ch02#120
Abinoam P. Marques Jr. (abinoam) wants to merge 9 commits into
microsoft:mainfrom
abinoam:fix/dyn-compatibility-terminology

Conversation

@abinoam

@abinoam Abinoam P. Marques Jr. (abinoam) commented Aug 21, 2026

Copy link
Copy Markdown

Some background, since this is my first contribution here. I've been programming for a long time, though in other stacks - Rust is the new one for me. I want to say first that this material is excellent: ch02 goes deeper than most trait material I've found.

While reading, I asked an AI assistant to sanity-check a few sections against the current Reference. The intent was just to confirm the object safety → dyn compatibility rename. It came back with more than that, so rather than take it at face value I put every rule through rustc 1.95.0 in a scratch project - a compiling example and a failing example per rule. Two of the chapter's four rules turned out to be genuinely wrong, and the failing cases produce the error messages quoted throughout.

I kept the commits atomic - drop whichever ones don't hold up and the rest still stand on their own.


Rust renamed object safety to dyn compatibility in 1.84. The rename missed the release notes, but it's settled everywhere that matters - the Reference, rustdoc, and the compiler itself, which now says the trait 'X' is not dyn compatible for E0038.

While updating the terminology in rust-patterns-book ch02, I checked the chapter's rule list against the Reference and found it's not only outdated wording: the list has 4 rules where the Reference has 6, and two of the 4 are factually wrong.

Every rule touched here was verified against rustc 1.95.0 in a scratch project before being written - one example per rule, each with the compiling case and the failing case. The compiler messages quoted in the commits and in the prose are literal output, not paraphrase.

Commits

Each commit is one self-contained fix, so any of them can be dropped without disturbing the others.

# Commit What
1 docs: rename "object safety" to "dyn compatibility" Pure terminology. No technical content touched. Also fixes an internal inconsistency in async-book ch10, where line 80 already said "not dyn-compatible" while line 143 still said "not object-safe".
2 fix: correct the Self-in-method rule The rule said "No use of Self in return position (except via indirection like Box<Self>)". Both halves are wrong - see below.
3 fix: correct the associated-function rule and list valid receivers The rule said "No associated functions (methods must have &self, &mut self, or self)". Both halves are wrong - see below.
4 fix: fix contradictory Sized advice in the rule of thumb The tip says "no Sized bounds" three lines after the Workarounds block recommends exactly where Self: Sized.
5 docs: note that lifetime parameters are allowed Rule 2 was correct but incomplete: type parameters are barred, lifetime parameters are not.
6 docs: add the supertrait rules Missing rule: all supertraits must themselves be dyn compatible.
7 docs: add associated const and GAT restrictions Missing rules: no associated constants, no generic associated types.
8 docs: add the opaque return type restriction Missing rule: no async fn, no return-position impl Trait.
9 docs: note the rename Short historical note so readers recognise "object safe" in older material.

The two factual errors

Rule 3 - Self is not only about return position. The Reference says a dispatchable method must "not use Self except in the type of the receiver", which covers arguments too. The old wording could not explain why PartialEq (fn eq(&self, other: &Self)) isn't dyn compatible. rustc emits two distinct messages here:

...because method `clone_self` references the `Self` type in its return type
...because method `equals` references the `Self` type in this parameter

Rule 3 - Box<Self> is not an escape hatch. Box<Self> is a valid receiver (self: Box<Self>), not a valid return type; fn spawn(&self) -> Box<Self> is still rejected. The actual "clone through a trait object" idiom is -> Box<dyn Trait>, which names a concrete type rather than Self.

Rule 4 - associated functions are allowed. They just need where Self: Sized to be opted out of the vtable. rustc suggests this fix itself in the E0038 note.

Rule 4 - self by value is not a dispatchable receiver. It implies where Self: Sized. A trait with a self-by-value method is still dyn compatible - the method is simply absent from the vtable. Calling it through the trait object fails with E0161: cannot move a value of type dyn Consume, not E0038. The rule now lists the real receiver set: &self, &mut self, self: Box<Self>, self: Rc<Self>, self: Arc<Self>, self: Pin<P> - all six verified dispatching through a trait object.

Two things worth flagging for review

The GAT and RPITIT rules land close to home. The chapter teaches GATs (LendingIterator) and RPITIT (fn items(&self) -> impl Iterator<..>, presented as a strict improvement over Box<dyn Iterator>) without mentioning that both cost the trait its dyn compatibility. Commits 7 and 8 add cross-references in both directions.

Associated constants have no where Self: Sized opt-out. Readers will try it by analogy with methods. It isn't accepted syntax on stable - E0658, generic const items are experimental (rust#113521). The workaround is to turn the constant into a method, which the text now says.

Verification

  • cargo xtask build - 7/7 books build clean.
  • Rendered HTML checked: the rule list renders as a 7-item <ol>, the section anchor resolves, and the new cross-reference to async-book ch10 points at a file that exists.
  • No SUMMARY.md or internal link referenced the old #trait-object-safety-rules anchor, so renaming the heading breaks nothing in-repo.

Rust renamed the concept in 1.84. The Reference, rustdoc and the compiler
all use "dyn compatibility" now — E0038 reads "the trait `X` is not dyn
compatible".

Pure terminology change, no technical content touched. Also fixes an
internal inconsistency in async-book ch10, where line 80 already said
"not dyn-compatible" while line 143 still said "not object-safe".

Ref: https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atibility

The rule said "No use of `Self` in return position (except via indirection
like `Box<Self>`)". Both halves were wrong:

1. The restriction is not limited to return position. The Reference says a
   dispatchable method must "not use `Self` except in the type of the
   receiver" — argument position counts too. That's why `PartialEq`
   (`fn eq(&self, other: &Self)`) is not dyn compatible, which the old
   wording could not explain.

2. `Box<Self>` is not an escape hatch in return position. `Box<Self>` is a
   valid *receiver* (`self: Box<Self>`); as a return type it still names
   `Self` and is rejected. The real idiom is `-> Box<dyn Trait>`, which
   names a concrete type rather than `Self`.

Verified against rustc 1.95.0; the compiler emits two distinct messages,
"references the `Self` type in its return type" and "references the `Self`
type in this parameter", both quoted in the examples.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t valid receivers

The rule said "No associated functions (methods must have `&self`,
`&mut self`, or `self`)". Wrong on both counts:

1. Associated functions with no receiver ARE allowed — they just have to be
   opted out of the vtable with `where Self: Sized`. rustc itself suggests
   this fix in the E0038 note.

2. `self` by value is not a dispatchable receiver: it implies
   `where Self: Sized`. A trait with a `self`-by-value method is still dyn
   compatible; the method is merely absent from the vtable. Calling it
   through the trait object fails with E0161 ("cannot move a value of type
   `dyn Consume`"), not E0038 — a different failure mode worth showing.

Replaces the wrong list with the actual set of dispatchable receivers:
`&self`, `&mut self`, `self: Box<Self>`, `self: Rc<Self>`, `self: Arc<Self>`,
`self: Pin<P>`. All six verified dispatching through a trait object on
rustc 1.95.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mpatibility tip

The rule of thumb said "no `Sized` bounds", three lines after the
"Workarounds" block that recommends exactly `where Self: Sized`.

The two are different things: `Sized` as a supertrait kills dyn
compatibility outright (E0038, "...because it requires `Self: Sized`"),
whereas `where Self: Sized` on one method removes just that method from the
vtable and keeps the trait usable. Both verified on rustc 1.95.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n dispatchable methods

Rule 2 said "No generic type parameters on methods", which is correct but
incomplete. The Reference is explicit: a dispatchable method must "not have
any type parameters (although lifetime parameters are allowed)".

Readers routinely assume `fn first_token<'a>(&self, s: &'a str) -> &'a str`
breaks dyn compatibility because it has angle brackets. It doesn't —
lifetimes are erased before codegen, so one vtable slot still suffices.
Verified on rustc 1.95.0, including a higher-ranked closure argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two Reference rules were missing or loosely worded:

- "All supertraits must also be dyn compatible." Added as rule 5. It's worth
  its own entry because the failure is non-local: your trait looks fine and
  the compiler blames a method in somebody else's trait.

- Rule 1 restated as "`Sized` must not be a supertrait", matching the
  Reference, instead of "No `Self: Sized` bound on the trait itself" — which
  reads as if it also covered per-method `where Self: Sized` bounds.

Verified on rustc 1.95.0: `trait Derived: Base` where `Base` has a receiverless
associated function yields E0038 pointing at `Base::make`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more Reference rules the chapter didn't list: a dyn compatible trait
"must not have any associated constants" and "must not have any associated
types with generics".

The GAT one matters here in particular — the chapter teaches GATs a few
sections earlier with a `LendingIterator` example, without mentioning that
such a trait can never become `dyn LendingIterator`. Cross-referenced.

Also records something the Reference leaves implicit and that readers will
try by analogy with methods: there is no `where Self: Sized` opt-out for an
associated constant. `const MAX: f64 where Self: Sized;` doesn't compile at
all on stable — E0658, generic const items are experimental (rust#113521).
The workaround is to turn the constant into a method.

Plus the E0191 note: a plain associated type keeps the trait dyn compatible
but must be specified in the trait object type.

All verified on rustc 1.95.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nc fn / impl Trait)

Last of the missing Reference rules: a dispatchable method must "not have an
opaque return type", covering both `async fn` and return-position
`impl Trait`.

This one is load-bearing for this chapter specifically. The RPITIT section
below presents `fn items(&self) -> impl Iterator<Item = &str>` as a strict
improvement over `Box<dyn Iterator>` ("RPITIT removes the allocation")
without mentioning that it also removes the option of `dyn Container`. Added
a cross-reference in both directions, plus one to async-book ch10, which
already covers the `Pin<Box<dyn Future>>` workaround.

Also folds in the `AsyncFn`/`AsyncFnMut`/`AsyncFnOnce` rule, but explains it
rather than restating it: rustc blames `AsyncFnMut::CallRefFuture<'a>`, so
it's a corollary of the GAT rule, not an independent special case. The
contrast with plain `Fn` (dyn compatible) makes the point concrete.

Verified on rustc 1.95.0 — both still rejected, and both workarounds
exercised through a real trait object.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rename

Readers will meet "object safe" everywhere outside this book — older
articles, crate docs, Stack Overflow — and there is nothing in the chapter
telling them it's the same concept under a different name.

Follows the chapter's existing habit of anchoring features to versions
("Since Rust 1.65", "Since Rust 1.75"), and explains why the term changed:
Rust has no OOP objects, and the property was never about memory safety.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abinoam

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant