GC and Exceptions in Wasmtime
The Wasm GC and exceptions proposals are both enabled by default in today’s Wasmtime 47 release! We are excited to help bring more languages to WebAssembly and everywhere that Wasmtime runs. Getting to this point involved large Wasmtime changes and represents the culmination of years of engineering effort.
Wasmtime
Wasmtime is a WebAssembly runtime that is fast, safe, and portable. It is standalone, lightweight, and easy to embed. Wasmtime maintainers are committed to open standards and actively participate in Wasm standardization.
Wasm GC
Originally, in the first versions of WebAssembly, high-level languages with an
objects-and-references data model, as opposed to a raw-pointers-and-memory data
model, had to embed their own garbage collector inside their .wasm
binaries. This led to bloated .wasm binaries and many techniques often used
when implementing collectors in native code, such as using stack maps and
stack walking to identify GC roots, were unavailable. And, unfortunately, many
languages fell into this bucket.
The Wasm GC proposal improves the situation, adding efficient support
for these high-level languages to WebAssembly.1 It extends the
WebAssembly language, allowing Wasm programs to define their own struct and
array types and subtyping relationships. The Wasm program needn’t worry about
managing these types’ instances’ lifetimes or manually deallocating them; the
runtime handles all of that. Therefore, embedding their own garbage collector is
unnecessary, and these toolchains can instead take advantage of the WebAssembly
runtime’s collector. This opens the door for many more languages to easily and
more efficiently target WebAssembly.
As an example, here is how a Wasm program might define a node type for a binary tree:
(rec
(type $node (struct
(field $key (mut f64))
(field $left (mut (ref null $node)))
(field $right (mut (ref null $node)))
(field $value (mut (ref null $payload)))
))
)
New instances are created by struct.new
$node
and fields accessed via, e.g., struct.get $node
$key
or struct.set $node
$left
instructions.
Wasm Exceptions
Wasm’s exceptions proposal has goals for exception-using languages similar to
the Wasm GC proposal’s goals for objects-and-references languages: it aims to
enable efficient support for exceptions on WebAssembly, making WebAssembly a
better compilation target for languages with exceptions.2 Without
this proposal, toolchains would need to implement custom calling conventions
that return not just function results, but also whether the function returned
normally or threw an exception. Every call site must test this condition and
branch appropriately, adding both bloat to the .wasm binary and runtime
overhead to the common, normal-return path. However, with the exceptions
proposal, all that goes away, and is replaced by throw and try/catch-style
constructs. The WebAssembly runtime is then free to implement these with the
classic unwinding approach that imposes zero overhead to the common,
normal-return call paths, resulting in faster execution and smaller .wasm
binaries.
Wasmtime’s GC Implementation
Wasmtime has a simple Cheney-style semi-space copying collector. The GC heap is divided into two halves: the “active” semi-space where new objects are allocated, and the “idle” semi-space. During collection, live objects are copied from the idle space (which was the previous active space) to the new active space, and all GC roots (e.g. active references inside Wasm stack frames) are updated to point to the new locations. Allocation is a simple bump pointer within the active semi-space and the collector does not require any read or write barriers.
We reuse WebAssembly linear memories under the covers to implement and sandbox the GC heap. A reference to a GC object is not a native pointer, it is a 32-bit index into the GC heap’s underlying linear memory. WebAssembly promises to be fast, safe, and portable, but it is the runtime that must actually shoulder that responsibility in its implementation, and reusing linear memories for the GC heap has benefits in all three dimensions. Perhaps most obvious is the defense-in-depth safety implication: even in the face of collector bugs that corrupt the GC heap, a malicious Wasm program can’t escape the sandbox to access host memory. As far as being fast goes, it lets us use virtual-memory guard pages to elide explicit bounds checks, just like we do for linear memories; we get tight integration with our pooling instance allocator, ensuring we preserve our 5-microsecond instantiation times; and, on 64-bit machines, 32-bit GC references are more compact than 64-bit pointers, more efficiently utilizing CPU caches. Finally, allocating, deallocating, and resetting large regions of memory quickly across many platforms (including bare metal!), each of which have subtly different capabilities, involves a lot of special-casing. Our existing implementation of linear memories is already portable across these platforms and already does that special-casing, so, by building our GC heap on top of a linear memory, we get a portable GC heap “for free” as well.
To further ratchet up our confidence in the collector’s correctness, we extended
our fuzzing infrastructure to hammer on Wasm GC. First, we extended
wasm-smith to support the GC proposal. It can, in theory, generate roughly
any GC-using Wasm program, given enough time.3
But it might take a looong time to do that, which is why we supplemented it
with two additional fuzzers:
- One designed to exercise interesting and arbitrary object graphs, type references, and subtyping relationships
- Another designed to detect heap corruption due to collector bugs or misoptimizations in our compiler
Lastly, a small note regarding performance to set expectations: we’ve mainly focused our engineering efforts on the correctness of our collector thus far, and less so on its performance. It’s brand new, and hasn’t benefited from decades of performance engineering, unlike collectors in, for example, V8 and SpiderMonkey. Our collector’s throughput and latency won’t match theirs today. Additionally, we’ve been primarily designing the collector and its trade offs for the use cases where Wasmtime is used most in production: creating many small, disposable Wasm instances, each of which is processing a small handful of tasks before it, and its GC heap, are thrown away. The system is designed first to scale horizontally across many instances rather than focusing on single-instance performance above all else. This scenario is different from, say, a single long-lived server process with indefinite lifetime; tuning a collector for it will also be different.
What’s Next
There’s always lots of performance work still to be done. We are, for example, currently in the process of extending our compiler’s alias-analysis optimizations, like store-to-load forwarding and redundant-load elimination, with GC-type information. When we know that two types can never alias, that is, they cannot occupy the same memory location, we can be more aggressive with these optimizations.
The next big milestone, functionality-wise, is to prototype GC integration with the component model on top of lazy value lowering. This effort will promote garbage-collected languages to first-class citizens in the component ecosystem, since they will no longer need otherwise-unused linear memories just to pass data across components.
Conclusion
We are excited to have reached this milestone! Give Wasmtime’s newly enabled GC and exceptions support a test run and let us know how it goes.
-
Note that the GC proposal has merged into the main WebAssembly specification, so the proposal page is now an archived snapshot of a particular moment in time. ↩
-
Like the GC proposal, the exceptions proposal has also merged into the main WebAssembly specification, and the proposal repo is now an archived historical snapshot. ↩
-
wasm-smith’s only blind spot with regards to Wasm GC at the time of writing is that it will never generate non-nullable references. ↩