tech, developers, and the code underneath

issue 026· news·

Rust 1.86 lands trait upcasting after eight years

You can finally coerce dyn Sub to dyn Super. Also: HashMap::get_disjoint_mut, and a soundness fix worth reading.

Rust 1.86 stabilizes trait upcasting, a feature that has been open since 2017 and that everyone who has written a plugin system in Rust has stubbed their toe on.

the problem it fixes#

Given a supertrait relationship, you could not coerce a trait object of the subtrait to a trait object of the supertrait:

rust
trait Animal { fn name(&self) -> String; }
trait Dog: Animal { fn bark(&self); }

fn describe(a: &dyn Animal) { println!("{}", a.name()); }

fn use_dog(d: &dyn Dog) {
    describe(d);   // ERROR before 1.86
}

The workaround was a manual as_animal(&self) -> &dyn Animal method on every trait, implemented identically in every impl, existing purely to launder a pointer. Anyone building an ECS, a plugin registry, or any kind of heterogeneous object graph has written that boilerplate.

As of 1.86 the coercion just works. The vtable layout was changed so a subtrait's vtable embeds a pointer to the supertrait's, which is why this took eight years — it is a change to the ABI of trait objects, and getting it right without regressing size or dispatch cost required several redesigns.

the soundness fix#

Also in this release: a long-standing hole around Box<T> in the presence of Deref implementations that could produce aliasing violations under certain generic code. The details are in the release notes and are worth reading if you write unsafe. If you do not write unsafe, this fixed a bug you never knew you could have had.

the small ergonomics#

HashMap::get_disjoint_mut — get mutable references to multiple distinct keys at once, checked at runtime for disjointness:

rust
let [a, b] = map.get_disjoint_mut(["alice", "bob"]);

Previously this required either two lookups with an intermediate remove, or RefCell, or an unsafe block, all to express something that is obviously fine.

{float}::next_down and next_up — the adjacent representable float. Niche, and if you need it you really need it.

Target tier changes — several targets moved up and down the support tiers, as always. Check if you cross-compile to anything unusual.

the pattern#

Rust's stabilization pace on "obvious" features looks glacial from the outside and the reason is almost always the same: the obvious feature interacts with three other features in a way that produces unsoundness, and the team will not ship unsound.

Trait upcasting is a good example. The naive implementation is easy. The implementation that does not break dyn compatibility rules, does not bloat vtables, does not break existing unsafe code that assumed a layout, and composes correctly with associated types took most of a decade.

You can find that frustrating and still prefer it to the alternative.

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →