TL;DR

A developer working on the Nova JavaScript engine describes mapping garbage-collected handles onto Rust lifetimes and discovers that the model’s surprising behavior comes from contravariant lifetimes. The mapping forces unsafe lifetime coercions to allow rooting local handles in heap slots, exposing tensions between Rust’s borrow checker and garbage-collection semantics.

What happened

The author, who has helped build a JavaScript engine implemented in more than 100,000 lines of Rust, revisited how garbage-collected handles are modeled with Rust lifetimes. Heap-stored handles are naturally long-lived (conveniently treated as 'static), while unrooted stack handles are only safe until the next collection. Attempting to write a local (short-lived) handle into a heap slot (which expects a long-lived handle) is logically valid for garbage collection but rejected by Rust’s covariant lifetimes because it would appear to allow use-after-free. To work around the type system the author had been using unsafe lifetime transmutation and an explanatory safety comment. That pattern led to a realization: the behavior being exploited amounts to contravariance of lifetimes. The post walks through how contravariant types act like sinks and why that property explains the surprising lifetime coercions the engine needs.

Why it matters

  • Mapping GC handles to Rust lifetimes exposes tensions between automatic memory management semantics and Rust’s variance rules.
  • Rust’s covariance prevents directly storing short-lived (stack) handles in long-lived (heap) slots, forcing unsafe code in the engine.
  • The use of unsafe lifetime coercions complicates reasoning and maintenance, contributing to boilerplate like bind/unbind calls.
  • Understanding contravariance reframes the problem and may influence safer design choices or alternative models for GC in Rust engines.

Key facts

  • The Nova JavaScript engine referenced is implemented in over 100,000 lines of Rust.
  • Heap handles can be treated with an effectively long lifetime (conveniently 'static in the example).
  • Unrooted handles on the stack are only guaranteed valid until the next garbage collection run.
  • Covariant lifetimes (like Rust references) prevent assigning a short-lived reference into a slot typed for a longer lifetime.
  • To perform the logical 'rooting' operation (copying a local handle into the heap), the author used unsafe transmute to coerce lifetime types.
  • The author framed the underlying phenomenon as contravariance: some generic types reverse the subtype ordering on their parameters.
  • Contravariant types can be thought of as sinks: places you can put values and not get them back, which helps explain the lifetime behavior.
  • A colleague described the resulting code style—heavy on manual bind/unbind handling—as worse than C++ (paraphrased).
  • The author wrote a safety comment documenting why shortening a heap handle’s lifetime in this context is safe.

What to watch next

  • Whether the Nova project changes its lifetime model or introduces safer abstractions to avoid unsafe transmute (not confirmed in the source).
  • Community discussion or language-level proposals on representing GC semantics with Rust lifetimes (not confirmed in the source).
  • Potential refactors that reduce manual bind/unbind boilerplate in the engine (not confirmed in the source).

Quick glossary

  • Garbage collection: Automatic reclamation of memory that is no longer reachable from program roots.
  • Rust borrow checker: A component of the Rust compiler that enforces rules about references and lifetimes to prevent data races and use-after-free.
  • Lifetime: A compile-time annotation describing how long a reference or handle is valid.
  • Covariance and contravariance: Variance describes how subtyping between complex types relates to subtyping of their component types; covariance preserves order, contravariance reverses it.
  • Rooting: Making a handle reachable from collector-observed roots so the garbage collector treats the referenced object as live.

Reader FAQ

What problem did the author encounter?
Assigning a short-lived (stack) handle into a heap slot typed for a long-lived handle is rejected by Rust’s covariance rules even though it’s logically the correct GC operation.

Is the workaround safe?
The author used unsafe transmute and documented a safety comment arguing the coercion is safe in this GC model; the post does not provide external verification.

Why does contravariance matter here?
Contravariance explains why a complex type parameterized by a shorter lifetime can be used where a longer-lifetime type is expected, which underlies the observed coercions.

Will the engine’s design change to avoid these issues?
not confirmed in the source

Garbage collection is contrarian Published 2026-01-09 by Aapo Alasuutari Previously on this blog I've written about how Nova JavaScript engine models garbage collection using the Rust borrow checker and how…

Sources

Related posts

By

Leave a Reply

Your email address will not be published. Required fields are marked *