WebAssembly has a quiet limitation that surprises people the first
time they hit it. Once you have compiled some Rust or C or Go down to
a .wasm module, the boundary of that module speaks only in numbers.
A function can take and return 32- or 64-bit integers and floats, and
that is the whole vocabulary. There are no strings at the boundary, no
lists, no structs. If you want to pass a string from one module to
another, you pass two integers — a pointer into linear memory and a
length — and trust that the code on the other side knows what to do
with them.
For a single program that compiles to a single module, this is fine.
The trouble starts when you want two modules, written in two
languages, to call each other. That is the gap the Component Model is
meant to close, and it is worth understanding what the gap actually is
before looking at the fix.
What core WebAssembly does and doesn’t give you
Core WebAssembly — the 1.0 specification that browsers have shipped for
years — defines a module: a unit of code with imports and exports,
some functions, a chunk of linear memory, maybe a table. The
Bytecode Alliance’s own documentation puts the constraint plainly:
functions are “restricted, essentially, to using integer (i32 or
i64) or floating-point (f32 or f64) types.”
So passing a string is not a single operation. You write the bytes
into memory, hand over the offset and the length, and then read the
result back out of memory afterward. Both sides have to agree on the
encoding, on who allocates, on who frees. And here is the part that
bites: data representations are usually specific to each language. A
string in C is laid out differently from a string in Rust, which is
different again from a string in JavaScript. There is no shared notion
of “string” at the module boundary, so every cross-language pairing
needs its own hand-written glue, and the glue is exactly the kind of
code that is easy to get subtly wrong.
This is not a defect, really. Core Wasm was designed as a compact,
fast, sandboxed instruction format. Keeping the type system minimal
was a reasonable choice for that goal. It just means core Wasm alone
is a poor lingua franca for composing software out of pieces written
by different people in different languages.
What a component is
A component is the layer built on top of core modules to fix this. The
mental split that helped me: a core module is a bag of functions that
trade in numbers; a component is a core module (or several) wrapped in
a typed description of what it imports and exports, where the types are
the high-level ones you actually care about.
Two pieces make that work.
The first is a richer way of describing function signatures. Instead of
“this takes two i32s,” a component’s interface can say “this takes a
string and returns a list of records.” These descriptions are called
interfaces, and the type language they are written in is WIT.
The second is the canonical ABI — the application binary
interface. The interface says what the types are; the canonical ABI
says how those high-level types are encoded down into the numbers and
linear-memory operations that core Wasm can actually execute. It is
the agreed-upon, language-neutral lowering. Because the lowering is
specified once and shared, two components can interoperate without
either one knowing what language the other was written in. The glue
that you used to write by hand is now generated from a single rulebook.
WIT, in practice
WIT (the Wasm Interface Type language) is an interface definition
language, and the docs are careful to say what it is not: “WIT isn’t
a general-purpose programming language and doesn’t define behaviour; it
defines only contracts between components.” You don’t write logic in
it. You write the shape of the boundary.
The value types are the ones a working programmer reaches for. There
are primitives, and then the compound types that matter:
- records — a set of named fields, like a struct
- variants — a value that is one of several named cases, like a
tagged union or a Rust enum with payloads
- lists —
list<T>, an ordered sequence of some type T
- strings — actual Unicode strings, not pointer-length pairs
- results —
result<T, E>, holding either a success value T or
an error E
That last one is quietly important. result<T, E> gives
cross-language calls a typed way to say “this failed, and here is the
typed reason,” instead of smuggling an error through a sentinel
integer or a side channel. Errors stop being a convention and become
part of the contract.
WIT groups types and functions into interfaces (a named set of
types and functions), and then groups interfaces into a world. A
world is the contract of a whole component: everything it imports and
everything it exports, in one named bundle. If an interface is a
single phone line, a world is the full description of every line
running into and out of the building. When you build a component, you
build it to a world. Tooling can then read the world and generate
type-safe bindings in your language — so a Rust author and a Python
author targeting the same world get bindings that line up, because
they were both generated from the same description.
Where WASI fits
This is where the Component Model stops being abstract. WASI, the
WebAssembly System Interface, is how a Wasm program reaches the outside
world — files, clocks, sockets, randomness. WASI 0.2, also called
Preview 2, is defined as a collection of WIT worlds and interfaces.
On 25 January 2024 the WASI Subgroup voted to launch WASI 0.2 and
declared its APIs stable. The headline of that release, in the
Bytecode Alliance’s own words, is that “WASI is now officially based on
the Wasm component model, which makes it cross-language and
virtualizable.” Two worlds shipped: wasi-cli, a command-line world
that roughly corresponds to POSIX (files, sockets, clocks, random
numbers), and wasi-http, an HTTP proxy world built around requests
and responses.
“Virtualizable” is the word doing real work there. Because a host
capability like the filesystem is just an interface a component
imports, you can satisfy that import with a real filesystem — or with
another component that pretends to be one. That makes sandboxing and
testing a property of the type system rather than a bolt-on.
The honest status
Here is where the calm version has to be plain: this is not finished.
The Component Model is, per its own specification repository, “being
incrementally developed and stabilized as part of WASI Preview 2.” WASI
0.2 itself is stable and pinnable — you can target a release >= 0.2.0
and rely on it, and there have been point releases since — but the
Component Model spec underneath is still moving through the W3C
WebAssembly Community Group’s standardization process. It is not yet a
finished, ratified standard the way core Wasm 1.0 is.
The next milestone, WASI 0.3 (Preview 3), is mostly concerned with
adding native asynchronous I/O and concurrency to the model. As of this
writing that work is in progress rather than shipped, and I will admit
I am not entirely sure how the async story settles in day-to-day code
yet — the design has shifted more than once, and I would rather say
that than pretend the picture is fully clear. Browser support is its
own separate question; core Wasm is everywhere, but the Component Model
is not something you assume a browser runs natively today. The strong
support right now lives in standalone runtimes and the toolchains
around them.
None of that makes the idea less worth understanding. The Component
Model is a real answer to a real problem — letting separately compiled,
differently authored pieces of software share strings and records and
typed errors instead of raw integers, with the encoding specified once
and the glue generated rather than written. If you have ever
hand-marshalled a string across a Wasm boundary, you already know why
that matters. The remaining work is turning a promising,
mostly-working design into a settled standard, and that part is
genuinely still underway.
Sources