The WebAssembly Component Model
The WebAssembly Component Model is a proposal to build upon the core WebAssembly standard by defining how modules may be composed within an application or library. The repository where it is being developed includes an informal specification and related documents, but that level of detail can be a bit overwhelming at first glance.
In this article, we’ll begin by briefly explaining the motivation bethind the Component Model. From there, we’ll build an intuition for how it works, comparing it to how native code is linked and run on popular operating systems like Windows, macOS, and Linux. The intended audience is developers who are already familiar with natively compiled languages such as C, Rust, and Go, and with concepts such as dynamic linking and inter-process communication.
The core WebAssembly Specification defines an architecture-, platform-, and language-agnostic format for representing executable code. A WebAssembly module may import functions, global variables, etc. from the host runtime, as well as export such items to the host. However, there is no standard way to combine modules at runtime, nor is there a standard way to marshal high-level types (e.g. strings and records) across module boundaries.
In practice, the lack of module composition means modules must be statically linked at build time rather than dynamically linked at runtime, and they cannot communicate with other modules in a standard, portable way. The Component Model addresses this by providing three main features on top of core WebAssembly:
- Interface types: a language-agnostic way to define a module interface in terms of high-level types such as strings, records, collections, etc.
- A canonical ABI which specifies how high-level types are represented in terms of the low-level types of core WebAssembly
- Module and component linking: a mechanism for dynamically composing modules into components. These components may themselves be composed together into higher-level components.
Together, these features allow code to be packaged and reused without the duplication and security pitfalls of static linking. This type of reuse is particularly desireable in multi-tenant cloud computing, where code duplication across thousands of modules can add up to expensive storage and memory overhead.
A Native Code Analogy
If you’re familiar with how native code is linked and executed, then you’ve encountered concepts like “executables”, “processes”, and “shared libraries”. The Component Model has analagous concepts, but uses different terminology for them. Here we’ll match up each native code concept with its WebAssembly counterpart and consider their similarities and differences.
Component <-> Executable
A Wasm component is like a native executable: inert and stateless, waiting to be used.
Module <-> Shared Library
A Wasm module is like a shared library (e.g.
.so), which may export and/or import symbols to be linked together with other code. Like components, modules are inert and stateless, waiting to be used.
Component Instance <-> Process
An instance of a component is like a process; it is the loaded and running form of a component; it is stateful and dynamic. Like processes, several component instances may be organized into a tree such that each node can communicate with its immediate children. Also like processes, component instances do not share memory or other resources between each other and must communicate in some host-mediated way.
Note that multiple processes can be run from the same executable – sometimes simultaneously – and those processes do not share state with each other. Likewise, instances produced from the same component do not share state.
Module Instance <-> Loaded Shared Library
An instance of a module is like a library which has been loaded and linked into a process. It is stateful and dynamic, sharing memory and other resources with the rest of the component.
Note that a given module can be instantiated into several components simultaneously, and that those instances do not share state with each other.
Some shared libraries can also be executables; i.e. they can be either linked into processes or executed as independent processes. Likewise, Wasm module instances can either be linked into component instances or run independently by the host runtime.
Wasm Runtime <-> Operating System
The Wasm runtime (e.g.
wasmtime) is like an operating system in that it is responsible for loading and linking components (cf. processes) and mediating communication between them.
However, unlike most operating systems, which provide a few low-level inter-process communication methods (e.g. pipes, stdin/stdout, etc.), a component-enabled Wasm runtime provides a single, high-level communication method based on interface types and a canonical ABI. This method is analagous to an RPC protocol such as gRPC, except that it is intended (and optimized) for local communications rather than over a network.
For intra-component communication across module boundaries, the Component Model does not specify a standard ABI. This means that modules which are to be linked within a component must agree on a suitable ABI to target, which could either be language-specific or language-agnostic.
In contrast to the native model, where inter-process communication is usually more awkward and complicated than a library call, the Component Model is designed to make both inter-component and inter-module communication convenient and expressive. The upshot is that you can divide your Wasm application into multiple, individually sandboxed components with no more effort than if they were modules sharing the same sandbox.
The above discussion ignores a few nuances and exceptions for the sake of simplicity. For example, processes on popular OSes can share memory, file handles, etc., although they usually don’t. Likewise, it’s possible some future WASI API could allow a similar kind of sharing among component instances.
A (Borrowed) Visualization
The following diagram is hosted in the Component Model repository and illustrates static vs. dynamic linking of Wasm modules and components, with the diagram on the right representing module instances loaded into component instances. However, it could just as easily illustrate native static vs. dynamic linking, with the rightmost diagram representing processes and their loaded libraries.
Although the Component Model is relatively new and still being developed, it follows the same pattern that has been used to organize native code for decades. Modules, components, and their instantiations facilitate code reuse and isolation in a way analogous to how shared libraries, executables, and processes are used in your favorite OS.
Moreover, the Component Model improves on the native model by providing an expressive, high-level ABI which can be used across component boundaries. This makes it easy to reuse code while still isolating it from the state of your application, improving both security and reliability.
At Fermyon, we’re excited about the Component Model because it is central to making Spin microservices secure, efficient, and easy to develop. Spin already uses Wasmtime’s strong sandboxing guarantees to isolate services from each other, and it uses wit-bindgen to provide high-level, language-agnostic APIs for common features like HTTP requests. Moving forward, we’re actively contributing to Wasmtime and other projects to help implement component support and will adopt key features for use in Spin as they stabilize.