Home
The WebAssembly Component Model is a broad-reaching architecture for building interoperable WebAssembly libraries, applications, and environments.
This documentation is aimed at users of the component model: developers of libraries and applications.
note
Compiler and Wasm runtime developers can take a look at the Component Model specification to see how to add support for the component model to their project.
Table of contents
Understanding components | Building components | Using components |
---|---|---|
Why Components? | C/C++ | Composing |
Components | C# | Running |
Interfaces | Go | Distributing |
Worlds | JavaScript | |
Python | ||
Rust |
WebAssembly components
As with all programming, the goal of writing a component is to make new functionality available by building it out of existing functionality.
A WebAssembly component runs on a platform, which may be a Web browser, a stand-alone runtime, or even an operating system (when compiling WebAssembly to an executable). By running the component, the platform gains the functionality that the component implements. Likewise, the platform provides functionality that code in components can use to interact with the outside world.
For example:
- A user of the component model can build a component that converts the system time to another time zone.
- For the component to work as intended, the underlying platform must provide the component with a means to access the current system time and the system time zone.
APIs for building WebAssembly components
In general, a platform that runs components must provide well-defined APIs for accessing functionality that components need: for example, reading from standard input, accessing environment variables, or manipulating network sockets.
It's useful to have a standard, shared set of APIs that WebAssembly components can depend on. WASI (the WebAssembly System Interface) is a standards-track specification that defines these APIs. A system or platform may expose some or all of the WASI APIs to components.
Status
The current stable release of WASI is WASI 0.2.0,
which was released on January 25, 2024.
WASI 0.2.0 is a stable set of WIT definitions
that components can target.
WASI proposals will continue to evolve and new ones will be introduced;
however, users of the component model can now pin to any stable release >= v0.2.0
.
The WASI.dev roadmap tracks upcoming releases.
Contributing
If you find a mistake, omission, ambiguity, or other problem, please let us know via GitHub issues.
If you'd like to contribute content to the guide, please see the contribution guide for information on how to contribute.
Why the Component Model?
At a high level, the component model builds upon WebAssembly core modules to enhance interoperability between languages and libraries, both by enriching the type system used for checking the safety of interactions between modules, and by clearly defining and enforcing the low-level calling contract between separately-compiled modules. To understand what the limitations of core modules are, we start by defining them.
WebAssembly core modules
A module is defined by the WebAssembly Core Specification.
WebAssembly programs can be written by hand,
but it's more likely that you will use a higher level programming language
such as Rust, C, Go, JavaScript, or Python to build WebAssembly programs.
Many existing toolchains currently produce a
WebAssembly core module—a single
binary .wasm
file.
A core module usually corresponds to a single binary .wasm
file.
Here's what the file
command outputs for a sample .wasm
file:
$ file adder.wasm
adder.wasm: WebAssembly (wasm) binary module version 0x1 (MVP)
The file can also be inspected
using the wasm-tools
CLI:
$ wasm-tools print adder.wasm | head -1
(module
A core module is a set of definitions. Kinds of definitions include:
- Functions define executable units of code (sequences of instructions along with declarations for the names of arguments and the types of arguments and return values).
- Linear memories define buffers of uninterpreted bytes that can be read from and written to by instructions.
- Imports define the names of other modules that are required to be available to execute the functions in the module, along with type signatures for required functions in the imported module.
- Exports define the names of functions within the module that should be accessible externally.
- And others; see the Core Specification for the complete list.
A compiled core module is sometimes called a "WebAssembly binary",
and usually corresponds to a single .wasm
file.
These modules can be run in the browser,
or via a separate runtime such as Wasmtime
or WAMR.
Limitations of core modules
Core modules are limited in the computation they can perform and
how they expose their functionality to the outside world.
In WebAssembly core modules, functions are restricted, essentially,
to using integer (i32
or i64
) or floating-point (f32
or f64
) types.
Only these types can be passed as arguments to functions,
and only these types can be returned from functions as results.
Compound types common in higher-level programming languages,
such as strings, lists, arrays, enums (enumerations), or structs (records),
have to be represented in terms of integers and floating-point numbers.
For example, for a function to accept a string, the string argument might be represented as two separate arguments: an integer offset into a memory and an integer representing the length of the string. Recall that a (linear) memory is an uninitialized region of bytes declared within a module.
In pseudocode, a type signature for a string-manipulating function might look like:
remove-duplicates: func(offset: i32, length: i32) -> [i32, i32]
supposing that remove-duplicates
is a function
to create a new string consisting of the unique characters
in its argument.
The return type is a list of two 32-bit integers.
The first integer is an offset into one of the linear memories
declared by the module—where the newly allocated string starts—and
the second integer is the length of the string.
After calling the function,
the caller has to reach into the appropriate linear memory
and read the output string, using the returned offset and length.
For this to work, the module defining the remove-duplicates
function
would also need to include
an export declaration that exports a memory to be used
for the argument and result strings. Pseudocode:
export "string_mem" (mem 1)
And, the module using the remove-duplicates
function
would need to import this memory. Pseudocode:
import "strings" "string_mem"
(This pseudocode is still simplified, since the importer also needs to declare the size of the memory being imported.)
Note that there is nothing in the type system to prevent the returned length from being confused with the returned offset, since both are integers. Also, the name of the memory used for the input and output strings must be established by convention, and there is also nothing in the type system to stop client code from indexing into a different memory (as long as the sum of the offset and length is within bounds).
We would prefer to write a pseudocode type signature like this:
remove-duplicates: func(s: string) -> string
and dispense with the memory exports and imports altogether.
The complexity doesn't stop there! Data representations are frequently specific to each programming language. For example, a string in C is represented entirely differently from a string in Rust or in JavaScript. Moreover, to make this approach work, modules must import and export memories, which can be error-prone, as different languages make different assumptions about memory layout.
For WebAssembly modules written in different languages to interoperate smoothly, there needs to be an agreed-upon way to expose these richer types across module boundaries.
Components
Components solve the two problems that we've seen so far: the limited type system of core module functions, and cross-language interoperability. Conceptually, a component is a WebAssembly binary (which may or may not contain modules) that is restricted to interact only through the modules' imported and exported functions. Components use a different binary format:
$ file add.component.wasm
add.component.wasm: WebAssembly (wasm) binary module version 0x1000d
Inspecting the file with the wasm-tools
CLI
shows more clearly that it contains a component:
$ wasm-tools print add.component.wasm | head -1
(component
Compared to core modules, components also use a richer mechanism by default for expressing the types of functions: interfaces.
Interfaces
Interfaces are expressed in a separate language called WebAssembly Interface Types (WIT). Interfaces contain definitions of types and type signatures for functions. The bit-level representations of types are specified by the Canonical ABI (Application Binary Interface). Together, interfaces and the Canonical ABI achieve the goal of clearly defining and enforcing the low-level calling contract between modules.
Interoperability
WebAssembly core modules are already portable across different architectures and operating systems; components retain these benefits and, using the Component Model ABI, add portability across different programming languages. A component implemented in Go can communicate directly and safely with a C or Rust component, by relying on the shared conventions of the Component Model ABI. Writing a component doesn't even require knowledge of which language its dependent components are implemented in, only the component interface expressed in WIT. Additionally, components can be composed into larger graphs, with one component's exports satisfying another's imports.
Benefits of the component model
Putting all of the pieces together: the component model introduces a binary WebAssembly format that encapsulates WebAssembly modules. This format enables the construction of WebAssembly modules that interact with each other only through exports and imports of functions whose types are expressed using WIT.
Building upon Wasm's strong sandboxing, the component model has further benefits. Rich types make it easier to know what a component or interface is doing at a glance and have guarantees of what bad things cannot happen. Richer type signatures express richer semantic properties than type signatures made up only of integers and floats. The relationships within a graph of components can be statically analysed: for example, to verify that a component containing business logic has no access to a component containing personally identifiable information.
Moreover, a component interacts with a runtime or other components only by calling its imports and having its exports called. Specifically, unlike core modules, a component may not export a memory and thus it cannot indirectly communicate to others by writing to its memory and having others read from that memory. This not only reinforces sandboxing, but enables interoperation between languages that make different assumptions about memory: for example, allowing a component that relies on garbage-collected memory to interoperate with one that uses conventional linear memory.
Using components
Now that you have a better idea about how the component model can help you, take a look at how to build components in your favorite language!
Further reading
For more background on why the component model was created, take a look at the specification's goals, use cases and design choices.
Component Model Concepts
The WebAssembly Component Model extends core WebAssembly in several ways. The Component Model:
- Adds consistent representation of higher-level types
- Enables interface-driven development
- Makes core WebAssembly composable: components that provide functionality and those that use them can be composed together into one resulting component.
This section introduces the core concepts behind the component model. For the rationale behind the component model, see the previous section.
Components
A WebAssembly Component is a binary that conforms to the Canonical ABI; often a WebAssembly core module extended with the features of the Component Model (higher-level types, interfaces). WebAssembly components are nestable: they may contain zero or more core modules and/or sub-components composed together. For example, a component implementing a simple calculator might be written by composing together a component that parses strings to floating-point numbers with a component that does the main arithmetic.
WebAssembly Interface Types (WIT)
WebAssembly Interface Types (WIT) is the Interface Definition Language (IDL) used to formally define functionality for WebAssembly components. WIT gives WebAssembly components the ability to express type signatures in a language-agnostic way, so any component binary can be checked, composed and executed.
Interfaces
An interface is a collection of type definitions and function declarations (function names accompanied by type signatures). Typically, a single interface describes a specific, focused bit of functionality.
For example, in wasi-cli,
three separate interfaces are used to implement stdin
, stdout
, and stderr
(streams typically available in command-line-like environments)
Worlds
A world is a collection of interfaces and types that expresses what features a component offers and what features it depends on.
For example, wasi-cli includes the command
world,
which depends on interfaces
that represent the stdin
, stdout
, and stderr
streams,
among other things.
A component implementing the command
world
must be invoked in an environment that implements those interfaces.
Packages
A package is a set of WIT files containing a related set of interfaces and worlds.
For example, the wasi-http package includes
an imports
world encapsulating the interfaces that an HTTP proxy depends on,
and a proxy
world that depends on imports
.
Platforms
In the context of WebAssembly, a host refers to a WebAssembly runtime capable of executing WebAssembly binaries. The runtime can be inside a browser or can stand alone. A guest refers to the WebAssembly binary that is executed by the host. (These terms borrow from their analogs in virtualization, where a guest is a software-based virtual machine that runs on physical hardware, which is the "host")
The Component Model introduces the idea of a platform to core WebAssembly—enabling the structured, standardized use of host functionality for WebAssembly guests. Components may import functionality that is provided by the platform on which they are executed.
WASI
The WebAssembly System Interface (WASI) defines in WIT
a family of interfaces for common system-level functions.
WASI defines a platform for component writers that mimics
existing programs that developers are familiar with
(for example, wasi-cli
or wasi-http
),
standardizing the functionality components depend on.
note
The Component Model is stewarded by the Bytecode Alliance and designed in the open.
See the WebAssembly/component-model
repository for goals, use cases, and high level design choices.
Components
Conceptually, a component is a self-describing WebAssembly binary that interacts only through interfaces instead of shared memory. Let's break down what each of these terms means:
- Self-describing: Like a WebAssembly core module, a component includes import and export declarations that declare both the names and types of imported and exported functions. Compared to core modules, components use a richer type system to describe these types, so it's easier to understand what functionality a module provides and what functionality it relies on.
- Interacts: When a component interacts with other components, that means either that it calls a function defined in a different component, or that another component calls a function defined in it. Interfaces specify what kinds of function calls are valid.
- Shared memory: In the "Why the Component Model?" section, we showed how WebAssembly core modules can only exchange compound data through shared memory. Components use memory in the same way that core modules do, except that in components, memories are never exported or imported; they are not shared.
Logically, a component is a structure that may contain core modules and/or other components. The component encodes the interfaces of these contained modules and sub-components using WebAssembly Interface Types (WIT).
For a more formal definition of a component, take a look at the Component Model specification.
The on-disk representation of a component is a specially-formatted WebAssembly binary. Internally, this file could include representations of one or many traditional ("core") WebAssembly modules and sub-components, composed together via their imports and exports. Two modules or components can be composed if the imports of one are satisfied by the exports of another. Composition can be repeated arbitarily, composing a single component out of many interlocking modules and components. Interfaces enable checking that a particular composition makes sense.
Each component is described by a world, which potentially collects together multiple interfaces to describe all the imports and exports of the component. The world only describes the types of imported and exported functions; the component internally defines the code to implement the world.
Composition
Two modules or components can be composed if the imports of one are satisfied by the exports of another. Composition can be repeated arbitarily, composing a single component out of many interlocking modules and components. Interfaces enable checking that a particular composition makes sense.
Interfaces
Interfaces are based on the idea of design by contract. In software design, a contract is a specification of how a unit of code should behave.
Concretely, an interface is a collection of type definitions and function declarations. Conceptually, an interface describes a single-focus, composable contract through which components can interact with each other and with hosts.
- Single-focus: By convention, an interface describes types and functions that are related to each other and collectively provide a relatively small unit of functionality, such as reading from the standard input stream in a command-line environment.
- Composable: Interfaces can be imported and exported. One component's interfaces can be built on top of interfaces defined in a different component. Interfaces enable typechecking so that interfaces can be composed only when it makes sense to do so.
The types and functions in an interface are used to enable interactions between components and hosts. For example:
- A "receive HTTP requests" interface might declare
a single "handle request" function,
along with definitions of types representing
incoming requests, outgoing responses,
HTTP methods and headers, and other data structures.
This might look like the
incoming-handler
interface in wasi-http - A "wall clock" interface might declare two functions,
one to get the current time
and one to get the granularity of the timer (whether time
is measured in seconds, milliseconds, nanoseconds, or another unit).
It would also define a type to represent an instant in time.
This might look like the
wall-clock
interface in wasi-clocks.
As an example of composing interfaces together, imagine defining a "timer" interface that declares two functions, one to start a timer and one to query whether the timeout has been exceeded. This interface could be defined by importing the "wall clock" interface. The result is an interface that exports the timer functionality, and imports anything imported by the "wall clock" interface.
Interfaces are defined using the WIT language.
For a more formal definition of an interface, take a look at the WIT specification.
WIT Worlds
A WIT world (or just "world") is a contract with a broader scope than a single interface. A world describes the functionality a component provides, and the functionality it requires in order to work.
A world can be used to describe a component, and a hosting environment for other components, depending on which imports and exports are specified. Worlds can represent either a component or host environment because components can be composed: a component can provide functionality required by another component, just like a host environment can.
Fulfilling worlds with components versus hosts
On the one hand, a world describes how a component relates to other components: it describes the functionality the component exposes and declares the functionality it depends on in order to be able to run. Functionality is exposed by defining interfaces to export, and dependencies are declared by importing interfaces. A world only defines the surface of a component, not its internal behaviour.
On the other hand, a world defines a hosting environment for components: that is, an environment in which a component can be instantiated and its functionality can be invoked.
- In WebAssembly, instantiation means turning a static description of a module into a dynamic structure in memory. It's analogous to loading an executable file.
A hosting environment supports a world by providing implementations for all of the imports and by optionally invoking one or more of the exports. If you're an application or library developer creating a component, you'll specify the world your component targets. Your component may target a custom world definition you have created with a unique set of imports and exports tailored just for your use case, or it may target an existing world definition that someone else has already specified. In either case, the world specifies all the external functionality your component needs. Targeting a world is analogous to relying on a particular version of a standard library, except that components give you the ability to precisely specify exactly what functions your code depends on.
Example: the wasi:cli
world
For example, WASI (the WebAssembly System Interface) defines a "command line" world
that imports interfaces that command-line programs typically expect to have available to them:
for example, file input/output, random number generation, and clocks.
This world has a single export for running the command-line tool.
Components targeting this world must provide an implementation for this single export,
and they may optionally call any of the imports.
For example, a component that prints out a summary of the sizes of files
in a particular directory (like the Unix du
command)
could target the "command line" world, and would depend on
the file input/output interfaces imported by the world.
A hosting environment that supports this world
must provide implementations for all of the imports
and may invoke the single export.
Running your example disk usage component
would mean invoking it in a hosting environment
that supports the "command line" world.
Worlds and interfaces
A world is a collection of interfaces, where each interface is directional. Each interface is explicitly labeled as either an export or an import. Exported interfaces are available for outside code to call, whereas imported interfaces must be fulfilled by outside code. These interfaces define a strict boundary for a component. The only ways a component can interact with anything outside itself are by having its exports called, or by calling its imports. This boundary provides very strong sandboxing: for example, if a component does not have an import for a secret store, then it cannot access that secret store, even if the store is running in the same process.
For a component to run, its imports must be fulfilled, by a host or by other components. Connecting up some or all of a component's imports to other components' matching exports is called composition.
A world is defined in a WIT file; a single WIT files can contain multiple worlds.
Example worlds
- A (trivial) "HTTP proxy" world would export a "handle HTTP requests" interface and import a "send HTTP requests" interface. A host, or another component, would call the exported "handle" interface, passing an HTTP request; the component would forward it on via the imported "send" interface. To be a useful proxy, the component may also need to import interfaces such as I/O and clock time: without those imports the component could not perform on-disk caching or other needed features.
- A "regex parser" world would export a "parse regex" function, and would import nothing. This declares not only that the component implementing this world can parse regular expressions, but also that it calls no other APIs. A user of such a parser could know, without looking at the implementation, that it does not access the file system or send the user's regexes to a network service.
For a more formal definition of what a WIT world is, take a look at the WIT world specification.
WIT Packages
A WIT package is a set of one or more WebAssembly Interface Type (WIT) files that, taken together, contain a set of interfaces and worlds that are related to each other. WIT is an interface definition language (IDL) for the component model. Packages provide a way for worlds and interfaces to refer to each other, and thus for an ecosystem of components to share common definitions.
A WIT package groups related interfaces and worlds together for ease of discovery and reference. A package is not a world: a package maps to one or more files and contains worlds. A world is a bundle of imported and exported types and interfaces.
- The WebAssembly System Interface (WASI) defines a number of packages,
including one named
wasi:clocks
. Our HTTP proxy world could import thewasi:clocks/wall-clock
interface (read as "thewall-clock
interface from thewasi:clocks
package"), rather than having to define a custom clock interface.
For a more formal definition of what a WIT package is, take a look at the WIT specification.
WIT By Example
This section includes two examples to introduce WIT: a simpler "clocks" example and a more complicated "filesystems" example. For a full WIT reference, see the next section.
Clocks
The following is a simplified version of the world defined in the wasi:clocks package.
Suppose we want to write a component that provides clock functionality.
This component will represent a "wall clock", which can be reset
(the clock is not monotonic).
(The real wasi:clocks
package provides two interfaces,
one for a wall clock and one for a monotonic clock.)
Declaring a world
We declare a world that imports one interface:
package wasi-example:clocks;
/// The following is a simplified copy of a world from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
world imports {
import wall-clock;
}
For exposition, version numbers have been removed.
This file contains a package declaration, which declares that
this world is in the clocks
package in the wasi-example
namespace.
The world is declared using the keyword world
, followed by
the name imports
.
World declarations must begin with world
, but the name imports
is an arbitrary choice.
What follows is a list of import
declarations enclosed in curly braces,
each of which consists of the import
keyword
followed by the name of an interface.
Each declaration is followed by a semicolon.
Declaring an interface: wall-clock
package wasi-example:clocks;
/// The following is a simplified copy of an interface from wasi:clocks.
/// For the full version, see https://github.com/WebAssembly/wasi-clocks/tree/main/wit
interface wall-clock {
record datetime {
seconds: u64,
nanoseconds: u32,
}
now: func() -> datetime;
}
Like a world, an interface is declared with a keyword (interface
) in this case,
followed by a name, followed by a semicolon-separated list of declarations enclosed
in curly braces.
In this case, declarations are type declarations or function declarations.
Type declarations
Record types are one of the possible types that can be declared in WIT.
record datetime {
seconds: u64,
nanoseconds: u32,
}
The record
keyword is followed by a name, then by a list of
field declarations separated by commas.
Each field declaration is a field name (a string), followed by
a colon, followed by a type name.
A record is analogous to a struct
in C or Rust,
in that it groups together named fields.
It is also analogous to a JavaScript object, except
that it has no methods or prototype.
In short, the datetime
type is a record with two fields:
seconds
, an unsigned 64-bit integer, and nanoseconds
,
an unsigned 32-bit integer.
Function declarations
The following declares a function named now
:
now: func() -> datetime;
The empty parentheses ()
indicate that the function has no arguments.
The return type is the type after the final arrow (->
),
which is datetime
.
Putting it together: now()
is a nullary function that returns a datetime.
Summing up
The imports
world contains an interface for wall clocks.
(Real worlds usually contain multiple interfaces.)
The wall clock world defines a record type that represents a time value
in terms of seconds and nanoseconds,
as well as a function to get the current time.
WIT By Example: Filesystems
That was just a warm-up; let's look at an example that uses more of WIT's built-in and user-defined types.
The following is a very simplified version of the main interface defined in the wasi-filesystem package. Much of the functionality has been removed. Here, a file descriptor supports just two operations:
open-at()
: Open a file.read()
: Read from a file, starting at a particular offset.
package wasi-example:filesystem;
/// The following is a simplified copy of an interface from wasi:filesystems.
/// For the full version, see https://github.com/WebAssembly/wasi-filesystem/tree/main/wit
interface types {
enum error-code {
access,
bad-descriptor,
}
resource descriptor {
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
open-at: func(
path: string,
) -> result<descriptor, error-code>;
}
}
Let's look at some WIT features used in this interface.
Enums
enum error-code {
access,
bad-descriptor,
}
This declaration defines an enumeration type named error-code
with two alternatives: access
and bad-descriptor
.
The contents of the curly brackets is just a list of comma-separated names.
Enum types are similar to enums in C, and are useful for
expressing types that have a known, small set of values.
This declaration expresses the possible error codes
that filesystem operations can return.
In reality, there are many more possible errors,
which would be expressed by adding more alternatives to the enumeration.
Resources
A resource describes an interface for objects.
This is not the same kind of "interface" as a WIT interface;
a WIT interface can contain many different resource
declarations.
The declaration of the descriptor
resource says that
a descriptor
is an object that implements two methods:
read
and open-at
.
Let's look at the method declarations one at a time:
Reading from files
read: func(
length: filesize,
offset: filesize,
) -> result<tuple<list<u8>, bool>, error-code>;
Method declarations use the same syntax as regular function declarations,
like the ones we already saw in the clocks example.
This declaration says that the read()
method has two arguments,
length
and offset
, both of which have type filesize
.
The return type of read
is a result
.
result
is another parameterized type, like option
.
Let's look at the parameters before we look at the entire type:
list
is also a parameterized type; in this case, it's applied tou8
(unsigned 8-bit integer), solist<u8>
can be read as "list of bytes".tuple
is like a list with a known size, whose elements can have different types.tuple<list<u8>, bool>
represents a 2-tuple (pair) of a list of bytes and a boolean.error-code
was defined as anenum
type.
If a
and b
are both types, then result<a, b>
represents
a type that can be either a
or b
.
Often, but not always, b
is a type that represents an error,
like in this case.
So the type result<tuple<list<u8>, bool>, error-code>
means
"either a tuple of a list of bytes and a bool; or an error code".
This makes sense for the read()
function because it takes a
number of bytes to read and an offset within a file to start at;
and the result is either an error, or a list of bytes containing
the data read from the file,
paired with a boolean indicating whether the end of the file was
reached.
Opening files
The open-at()
method is a constructor, which we know because
it returns a descriptor
when it doesn't fail (remember that
these methods are attached to the resource type descriptor
):
open-at: func(
path: string,
) -> result<descriptor, error-code>;
open-at()
returns a new descriptor, given a path string and flags.
Further reading
We've seen how using rich types, WIT can encode a multitude of ideas about how functions interrelate, which are not available in the type system of core WebAssembly.
For more WIT examples, see the tutorial section. The next section, WIT Reference, covers WIT syntax more thoroughly.
An Overview of WIT
The WIT (Wasm Interface Type) language is used to define Component Model interfaces and worlds. WIT isn't a general-purpose programming language and doesn't define behaviour; it defines only contracts between components.
To define a new component, you will need to define worlds and interfaces by writing code in the Wasm Interface Type (WIT) language. WIT also serves as documentation for existing components that you may wish to use.
This topic provides an overview of key elements of the WIT language.
The official WIT specification and history can be found in the WebAssembly/component-model
repository.
- An Overview of WIT
Structure of a WIT file
A WIT file contains one or more interfaces or worlds. An interface or world can define types and/or functions.
Types and functions can't be defined outside of interfaces or worlds.
A file may optionally start with a package declaration.
Comments
WIT comment syntax is similar to the one used by the C++ family of languages:
- Everything from
//
to end of line is a comment. - Any text enclosed in
/*
...*/
is a comment.- Unlike the C++ family, block comments can be nested, e.g.
/* blah /* rabbit */ rhubarb */
.
- Unlike the C++ family, block comments can be nested, e.g.
Documentation
WIT defines special comment formats for documentation:
- Everything from
///
to end of line is documentation for the following item. - Any text enclosed in
/**
...*/
is documentation for the following item.
For example:
/// Prints "hello".
print-hello: func();
/**
Prints "hello".
*/
print-hello: func();
Identifiers
Identifiers are names for variables, functions, types, interfaces, and worlds. WIT identifiers have a slightly different set of rules from what you might be familiar with in languages like C, Rust, and Java. These rules apply to all names, except for packages. Package identifiers are a little more complex and will be covered in the Packages section.
- Identifiers are restricted to ASCII
kebab-case
: sequences of words, separated by single hyphens.- Double hyphens (
--
) are not allowed. - Hyphens aren't allowed at the beginning or end of the sequence, only between words.
- Double hyphens (
- An identifier may be preceded by a single
%
sign.- This is required if the identifier would otherwise be a WIT keyword.
For example,
interface
is not a legal identifier, but%interface
is legal.
- This is required if the identifier would otherwise be a WIT keyword.
For example,
- Each word in the sequence must begin with an ASCII letter, and may contain only ASCII letters and digits.
- A word cannot begin with a digit.
- A word cannot contain a non-ASCII Unicode character.
- A word cannot contain punctuation, underscores, etc.
- Each word must be either all
lowercase
or allUPPERCASE
.- Different words in the identifier may have different cases. For example,
WIT-demo
is allowed.
- Different words in the identifier may have different cases. For example,
- An identifier cannot be a WIT keyword such as
interface
(unless preceded by a%
sign).
Built-in types
The types in this section are defined by the WIT language itself.
Primitive types
WIT defines the following primitive types:
Identifier | Description |
---|---|
bool | Boolean value true or false . |
s8 , s16 , s32 , s64 | Signed integers of the appropriate width. For example, s32 is a signed 32-bit integer. |
u8 , u16 , u32 , u64 | Unsigned integers of the appropriate width. For example, u32 is an unsigned 32-bit integer. |
f32 , f64 | Floating-point numbers of the appropriate width. For example, f64 is a 64-bit (double precision) floating-point number. See the note on NaN s below. |
char | Unicode character. (Specifically, a Unicode scalar value.) |
string | A Unicode string: that is, a finite sequence of characters. |
The
f32
andf64
types support the usual set of IEEE 754 single and double-precision values, except that they logically only have a singlenan
value. The exact bit-level representation of an IEEE 754NaN
is not guaranteed to be preserved when values pass through WIT interfaces as the singular WITnan
value.
Lists
list<T>
for any type T
denotes an ordered sequence of values of type T
.
T
can be any type, built-in or user-defined:
list<u8> // byte buffer
list<customer> // a list of customers
This is similar to Rust Vec
, or Java List
.
Options
option<T>
for any type T
may contain a value of type T
, or may contain no value.
T
can be any type, built-in or user-defined.
For example, a lookup function might return an option in order to allow
for the possibility that the lookup key wasn't found:
option<customer>
This is similar to Rust Option
, C++ std::optional
, or Haskell Maybe
.
This is a special case of a variant type. WIT defines it so that there is a common way of expressing it, so that you don't need to create a variant type for every value type, and to enable it to be mapped idiomatically into languages with option types.
Results
result<T, E>
for any types T
and E
may contain a value of type T
or a value of type E
(but not both).
For example, a HTTP request function might return a result,
with the success case (the T
type) representing a HTTP response,
and the error case (the E
type) representing the various kinds of error that might occur:
result<http-response, http-error>
This is similar to Rust Result
, or Haskell Either
.
This is a special case of a variant type. WIT defines the
result
type so that there is a common way of expressing this behavior, so that developers don't need to create variant types for every combination of value and error types, and to enable it to be mapped idiomatically into languages with result or "either" types.
Sometimes there is no data associated with one or both of the cases.
For example, a print
function could return an error code if it fails,
but has nothing to return if it succeeds.
In this case, you can omit the corresponding type as follows:
result<u32> // no data associated with the error case
result<_, u32> // no data associated with the success case
result // no data associated with either case
The underscore _
stands in "no data" and is generally represented as
the unit type in a target language (e.g. ()
in Rust, null
in JavaScript).
Tuples
A tuple
type is an ordered fixed-length sequence of values of specified types.
It is similar to a record, except that the fields are identified by indices
instead of by names.
tuple<u64, string> // An integer and a string
tuple<u64, string, u64> // An integer, then a string, then an integer
This is similar to tuples in Rust or OCaml.
User-defined types
New domain-specific types can be defined within an interface
or world
.
Records
A record
type declares a set of named fields, each of the form name: type
,
separated by commas.
A record instance contains a value for every field.
Field types can be built-in or user-defined.
The syntax is as follows:
record customer {
id: u64,
name: string,
picture: option<list<u8>>,
account-manager: employee,
}
Records are similar to C or Rust struct
s.
User-defined records can't be generic (that is, parameterised by type). Only built-in types can be generic.
Variants
A variant
type represents data whose structure varies.
The declaration defines a list of cases;
each case has a name and, optionally,
a type of data associated with that case.
An instance of a variant type matches exactly one case.
Cases are separated by commas.
The syntax is as follows:
variant allowed-destinations {
none,
any,
restricted(list<address>),
}
This can be read as "an allowed destination is either none, any, or restricted to a particular list of addresses".
Variants are similar to Rust enum
s or OCaml discriminated unions.
The closest C equivalent is a tagged union, but variants in WIT
both take care of the "tag" (the case)
and enforce the correct data shape for each tag.
User-defined variants can't be generic (that is, parameterised by type). Only built-in types can be generic.
Enums
An enum
type is a variant type where none of the cases have associated data:
enum color {
hot-pink,
lime-green,
navy-blue,
}
This can provide a simpler representation in languages without discriminated unions.
For example, a WIT enum
can translate directly to a C/C++ enum
.
Resources
A resource is a handle to some entity that exists outside of the component. Resources describe entities that can't or shouldn't be copied: entities that should be passed by reference rather than by value. Components can pass resources to each other via a handle. They can pass ownership of resources, or pass non-owned references to resources.
If you're not familiar with the concepts of borrowing and ownership for references, see the Rust documentation.
Unlike other WIT types, which are simply plain data, resources only expose behavior through methods. Resources can be thought of as objects that implement an interface. ("Interface" here is used in the object-oriented programming sense, not in the sense of a WIT interface.)
For example, we could model a blob (binary large object) as a resource.
The following WIT defines the blob
resource type,
which contains a constructor, two methods, and a static function:
resource blob {
constructor(init: list<u8>);
write: func(bytes: list<u8>);
read: func(n: u32) -> list<u8>;
merge: static func(lhs: blob, rhs: blob) -> blob;
}
As shown in the blob
example, a resource can contain:
- methods: functions that implicitly take a
self
(AKAthis
) parameter that is a handle. (Some programming languages use thethis
keyword instead ofself
.)read
andwrite
are methods. - static functions: functions which do not have an implicit
self
parameter but are meant to be nested in the scope of the resource type, similarly to static functions in C++ or Java.merge
is a static function. - at most one constructor: a function that is syntactic sugar for
a function returning a handle of the containing resource type.
The constructor is declared with
constructor
.
A method can be rewritten to be a function with a borrowed self
parameter,
and a constructor can be rewritten to a function that returns a value
owned by the caller.
For example, the blob
resource above could be approximated as:
resource blob;
blob-constructor: func(bytes: list<u8>) -> blob;
blob-write: func(self: borrow<blob>, bytes: list<u8>);
blob-read: func(self: borrow<blob>, n: u32) -> list<u8>;
blob-merge: static func(lhs: blob, rhs: blob) -> blob;
When a resource
type name is wrapped with borrow<...>
,
it stands for a "borrowed" resource.
A borrowed resource represents a temporary loan of a resource
from the caller to the callee for the duration of the call.
In contrast, when the owner of an owned resource drops that resource,
the resource is destroyed.
(Dropping the resource means either explicitly dropping it
if the underlying programming language supports that,
or returning without transferring ownership to another function.)
More precisely, these are borrowed or owned
handles
of the resource. Learn more abouthandles
in the upstream component model specification.
Flags
A flags
type is a set of named booleans.
flags allowed-methods {
get,
post,
put,
delete,
}
A
flags
type is logically equivalent to a record type where each field is of typebool
, but it is represented more efficiently (as a bitfield) at the binary level.
Type aliases
You can define a new type alias using type ... = ...
.
Type aliases are useful for giving shorter or more meaningful names to types:
type buffer = list<u8>;
type http-result = result<http-response, http-error>;
Functions
A function is defined by a name and a function type. As with record fields, the name is separated from the type by a colon:
do-nothing: func();
The function type is the keyword func
,
followed by a parenthesised, comma-separated list of parameters (names and types).
If the function returns a value, this is expressed as an arrow symbol (->
) followed by the return type:
// This function does not return a value
print: func(message: string);
// These functions return values
add: func(a: u64, b: u64) -> u64;
lookup: func(store: kv-store, key: string) -> option<string>;
To express a function that returns multiple values, you can use any compound type (such as tuples or records).
get-customers-paged: func(cont: continuation-token) -> tuple<list<customer>, continuation-token>;
A function can be declared inside an interface, or can be declared as an import or export in a world.
Interfaces
An interface is a named set of types and functions,
enclosed in braces and introduced with the interface
keyword:
interface canvas {
type canvas-id = u64;
record point {
x: u32,
y: u32,
}
draw-line: func(canvas: canvas-id, from: point, to: point);
}
Notice that types and functions in an interface are not comma-separated.
Using definitions from elsewhere
An interface can reuse types declared in another interface via a use
directive.
The use
directive must give the interface where the types are declared,
then a dot, then a braced list of the types to be reused.
The interface can then refer to the types named in the use
.
interface types {
type dimension = u32;
record point {
x: dimension,
y: dimension,
}
}
interface canvas {
use types.{dimension, point};
type canvas-id = u64;
draw-line: func(canvas: canvas-id, from: point, to: point, thickness: dimension);
}
The canvas
interface uses the types dimension
and point
declared in the types
interface.
Even if you are only using one type, it must still be enclosed in braces. For example,
use types.{dimension}
is legal butuse types.dimension
is not.
This works across files as long as the files are in the same package (effectively, in the same directory). For information about using definitions from other packages, see the specification.
Worlds
Roughly, a world describes the contract of a component.
A world describes a set of imports and exports,
enclosed in braces and introduced with the world
keyword.
Imports and exports may be interfaces or specific functions.
Exports describe the interfaces or functions provided by a component.
Imports describe the interfaces or functions that a component depends on.
interface printer {
print: func(text: string);
}
interface error-reporter {
report-error: func(error-message: string);
}
world multi-function-device {
// The component implements the `printer` interface
export printer;
// The component implements the `scan` function
export scan: func() -> list<u8>;
// The component needs to be supplied with an `error-reporter`
import error-reporter;
}
This code defines a world called multi-function device
,
with two exports, a printer
interface and a scan
function.
The exported printer
interface is defined in the same file.
The imported error-reporter
interface is also defined in the same file.
From looking at the error-reporter
interface,
you can see that When a world imports an interface,
the full interface with types and function declarations
needs to be provided,
not just the name of the interface.
Interfaces from other packages
To import and export interfaces defined in other packages,
you can use package/name
syntax:
world http-proxy {
export wasi:http/incoming-handler;
import wasi:http/outgoing-handler;
}
As this example shows, import and export apply at the interface level, not the package level. You can import one interface defined in a package, while exporting another interface defined in the same package. A package groups definitions together; it doesn't describe a coherent set of behaviours.
WIT does not define how packages are resolved; different tools may resolve them in different ways.
Inline interfaces
Interfaces can be declared inline in a world:
world toy {
export example: interface {
do-nothing: func();
}
}
Including other worlds
You can include
another world.
This causes your world to export all that world's exports,
and import all that world's imports.
world glow-in-the-dark-multi-function-device {
// The component provides all the same exports, and depends on
// all the same imports, as a `multi-function-device`...
include multi-function-device;
// ...but also exports a function to make it glow in the dark
export glow: func(brightness: u8);
}
As with use
directives, you can include
worlds from other packages.
Packages
A package is a set of interfaces and worlds,
potentially defined across multiple files.
To declare a package, use the package
directive to specify the package ID.
A package ID must include a namespace and name, separated by a colon,
and may optionally include a semver-compliant version number:
package documentation:example;
package documentation:example@1.0.1;
All files must have the .wit
extension and must be in the same directory.
If a package spans multiple files,
only one file needs to contain a package declaration,
but if multiple files contain package declarations,
the package IDs must all match each other.
For example, the following documentation:http
package is spread across four files:
// types.wit
interface types {
record request { /* ... */ }
record response { /* ... */ }
}
// incoming.wit
interface incoming-handler {
use types.{request, response};
// ...
}
// outgoing.wit
interface outgoing-handler {
use types.{request, response};
// ...
}
// http.wit
package documentation:http@1.0.0;
world proxy {
export incoming-handler;
import outgoing-handler;
}
This package defines request and response types in types.wit
,
an incoming handler interface in incoming wit
,
an outgoing handler interface in outgoing.wit
,
and declares the package and defines a world that uses these interfaces
in http.wit
.
For a more formal definition of the WIT language, take a look at the WIT specification.
Creating components
Many popular programming languages can be compiled to WebAssembly, but the level of support varies across languages. This document details languages with compilers and runtimes that support WebAssembly with WASI as a target platform.
This is a living document, so if you are aware of advancements in a toolchain, please do not hesitate to contribute documentation. You can find more information about the development of support for specific languages in the Guest Languages Special Interest Group Proposal document.
One of the benefits of components is their portability across host runtimes. The runtime only needs
to know what world the component is targeting in order to import or execute the component. This
language guide hopes to demonstrate that with a prevailing adder
world defined in
examples/tutorial/wit/adder/world.wit
. Furthermore, an example host that understands the example
world has been provided in examples/example-host
for running components. Each
toolchain section walks through creating a component of this world, which can be run either in the
example host or from an application of that toolchain. This aims to provide a full story for using
components within and among toolchains.
Each section covers how to build and run components for a given toolchain. The last section, on WebAssembly Text Format (WAT), details how to write WebAssembly components by hand, without using a higher-level language front-end.
- C/C++ Tooling
- C# Tooling
- Go Tooling
- JavaScript Tooling
- Python Tooling
- Rust Tooling
- WebAssembly Text Format (WAT)
C/C++ Tooling
WebAssembly components can be built from C and C++ using clang
,
the C language family frontend for LLVM.
wit-bindgen
is a tool
to generate guest language bindings from a given .wit
file.
When compiling C or C++ code to WebAssembly components,
we say that C or C++ is the "guest" language,
and WebAssembly is the "host" language.
In this case, "bindings" are C or C++ declarations: type signatures that
correspond to WIT functions, and type definitions that correspond to WIT types.
The bindings generator only generates declarations; you have to write
the code that actually implements these declarations,
if you're developing your own .wit
files.
For WIT interfaces that are built in to WASI, the code is part of the
WebAssembly runtime that you will be using.
C/C++ currently lacks an integrated toolchain (like Rust's cargo-component
).
However, wit-bindgen
can generate source-level bindings for
Rust, C, Java (TeaVM), and TinyGo,
with the ability to add more language generators in the future.
wit-bindgen
can be used to build C applications that can be compiled directly to WebAssembly modules using clang
with a wasm32-wasi
target.
1. Download dependencies
First, install the following dependencies:
wit-bindgen
CLIwasm-tools
wasm-tools
can be used to inspect compiled WebAssembly modules and components, as well as converting between preview1 modules and preview2 components in the optional manual workflow.
- The
WASI SDK
- WASI SDK is a WASI enabled C/C++ toolchain which includes a version of the C standard
library (
libc
) implemented with WASI interfaces, among other artifacts necessary to compile C/C++ to WebAssembly. - On a Linux system, you can skip to the "Install" section. To build from source, start from the beginning of the README.
- WASI SDK is a WASI enabled C/C++ toolchain which includes a version of the C standard
library (
A WASI SDK installation will include a local version of clang
configured with a WASI sysroot.
(A sysroot is a directory containing header files and libraries
for a particular target platform.)
Follow these instructions to configure WASI SDK for use.
note
You can also use your installed system or Emscripten clang
by building with --target=wasm32-wasi
, but you will need some artifacts from WASI SDK
to enable and link that build target (see the text about libclang_rt.*.a
objects in
the WASI SDK README).
2. Generate program skeleton from WIT
Start by pasting the contents of the sample adder/world.wit
file
into a local file.
Then generate a C skeleton from wit-bindgen
using this file:
$ wit-bindgen c path/to/adder/world.wit
Generating "adder.c"
Generating "adder.h"
Generating "adder_component_type.o"
This command generates several files:
adder.h
(based on theadder
world). This header file contains, amidst some boilerplate, the prototype of theadd
function, which should look like this. (The name of the function has been prefixed with "exports
".)
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y);
adder.c
, which interfaces with the component model ABI to call your function. This file contains anextern
declaration that looks like:
extern void __component_type_object_force_link_adder(void);
adder_component_type.o
, which contains object code, including the definition of the__component_type_object_force_link_adder
function, which must be linked viaclang
.
3. Write program code
Next, create a file named component.c
with code that implements the adder
world:
that is, code which fulfills the definition of the interface function declared in adder.h
.
#include "adder.h"
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y)
{
return x + y;
}
4. Compile a WebAssembly Preview 2 component with wasi-sdk
's wasm32-wasip2-clang
"P1" refers to WASI Preview 1, the initial version of the WASI APIs. "P2" refers to WASI Preview 2, which introduced the component model.
While in the past building a P2 component required conversion from a P1 component,
we can now build a P2 component directly by using the wasm32-wasip2-clang
binary
that was installed by the WASI SDK.
If necessary, change /opt/wasi-sdk
to the path where you installed
the WASI SDK.
/opt/wasi-sdk/bin/wasm32-wasip2-clang \
-o adder.wasm \
-mexec-model=reactor \
component.c \
adder.c \
adder_component_type.o
Breaking down each part of this command:
-o adder.wasm
configures the output file that will contain binary WebAssembly code.-mexec-model=reactor
controls the desired execution model of the generated code. The argument can be eitherreactor
orcommand
. In this case, we pass in-mexec-model=reactor
to build a reactor component. A reactor component is more like a library, while a command component is more like an executable.component.c
contains the code you wrote to implement theadder
world.adder.c
andadder_component_type.o
were generated bywit-bindgen
and contain necessary scaffolding (e.g. function exports) to enable buildingcomponent.c
into a WebAssembly binary.
After this command completes, you will have a new file named adder.wasm
available in the source folder.
You can verify that adder.wasm
is a valid WebAssembly component with the following command:
> wasm-tools print adder.wasm | head -1
(component
For use cases that require building a P1 module and/or
adapting an existing P1 module into a P2 module,
such as building for a platform that does not support P2,
details on a more manual approach using wasi-sdk
's clang
and wasm-tools
can be found below:
Manual P1 and P2 build
Compile the component code into a WebAssembly P1 module via clang
:
Assuming you defined WASI_SDK_PATH
according to
the "Use" section
in the WASI SDK README, execute:
$WASI_SDK_PATH/bin/clang \
-o adder.wasm \
-mexec-model=reactor \
component.c \
adder.c \
adder_component_type.o
You can verify that adder.wasm
is a valid WebAssembly P1 component (i.e. a WebAssembly core module) with the following command:
> wasm-tools print adder.wasm | head -1
(module $adder.wasm
Alternatively, you can also use the published
ghcr.io/webassembly/wasi-sdk
container images for performing builds.For example, to enter a container with
wasi-sdk
installed:docker run --rm -it \ --mount type=bind,src=path/to/app/src,dst=/app \ ghcr.io/webassembly/wasi-sdk:wasi-sdk-27
Replace
path/to/app/src
with the absolute path of the directory containing the code for your sample app.Inside the container your source code will be available at
/app
. After changing to that directory, you can run:/opt/wasi-sdk/bin/clang \ -o adder.wasm \ -mexec-model=reactor \ component.c \ adder.c \ adder_component_type.o
Using the Dockerfile avoids the need to install the WASI SDK on your system.
See also:
Dockerfile
inwasi-sdk
Transform the P1 component to a P2 component with wasm-tools
Next, we need to transform the P1 component to a P2 component.
To do this, we can use wasm-tools component new
:
wasm-tools component new adder.wasm -o adder.component.wasm
note
The .component.
extension has no special meaning—.wasm
files can be either modules or components.
(optional) Build a WASI-enabled WebAssembly (P2) component with wasm-tools
Note that wasm-tools component new
may fail if your code references any
WASI APIs that must be imported:
for example, via standard library imports like stdio.h
.
Using WASI interfaces requires an additional step,
as the WASI SDK still references WASI Preview 1 APIs (those with wasi_snapshot_preview1
in their names)
that are not compatible directly with components.
For example, if we modify the above code to reference printf()
,
it would compile to a P1 component:
#include "adder.h"
#include <stdio.h>
uint32_t exports_docs_adder_add_add(uint32_t x, uint32_t y)
{
uint32_t result = x + y;
// On traditional platforms, printf() prints to stdout, but on Wasm platforms,
// stdout and the idea of printing to an output stream is
// introduced and managed by WASI.
//
// When building this code with wasi-libc (as a part of wasi-sdk), the printf call
// below is implemented with code that uses `wasi:cli/stdout` and `wasi:io/streams`.
printf("%d", result);
return result;
}
However, the module would fail to transform to a P2 component:
> wasm-tools component new adder.wasm -o adder.component.wasm
error: failed to encode a component from module
Caused by:
0: failed to decode world from module
1: module was not valid
2: failed to resolve import `wasi_snapshot_preview1::fd_close`
3: module requires an import interface named `wasi_snapshot_preview1`
To build a P2 component that uses WASI interfaces from a P1 component, we'll need to make use of adapter modules. An adapter module provides definitions for WASI Preview 1 API functions in terms of WASI Preview 2 API functions.
Download the appropriate reactor adapter module as documented here
and save it to the same directory that contains the .c
and .wasm
files you have been working with.
You can either get the linked release of wasi_snapshot_preview1.reactor.wasm
and rename it to wasi_snapshot_preview1.wasm
,
or build it directly from source in wasmtime
following
the instructions here
(make sure you git submodule update --init
first).
Now, you can adapt preview1 to preview2 to build a component:
wasm-tools component new \
adder.wasm \
--adapt wasi_snapshot_preview1.wasm \
-o adder.component.wasm
5. Inspect the built component
Finally, you can inspect a WIT representation of the imports and exports of your component (including any WASI imports if you used them):
$ wasm-tools component wit adder.component.wasm
package root:component;
world root {
import wasi:io/error@0.2.2;
import wasi:io/streams@0.2.2;
import wasi:cli/stdin@0.2.2;
import wasi:cli/stdout@0.2.2;
import wasi:cli/stderr@0.2.2;
import wasi:cli/terminal-input@0.2.2;
import wasi:cli/terminal-output@0.2.2;
import wasi:cli/terminal-stdin@0.2.2;
import wasi:cli/terminal-stdout@0.2.2;
import wasi:cli/terminal-stderr@0.2.2;
import wasi:clocks/wall-clock@0.2.2;
import wasi:filesystem/types@0.2.2;
import wasi:filesystem/preopens@0.2.2;
export add: func(x: s32, y: s32) -> s32;
}
...
6. Run the component from the example host
The following section requires you to have a Rust toolchain installed.
warning
You must be careful to use a version of the adapter (wasi_snapshot_preview1.wasm
)
that is compatible with the version of wasmtime
that will be used,
to ensure that WASI interface versions (and relevant implementation) match.
(The wasmtime
version is specified in the Cargo configuration file
for the example host.)
This repository contains an example WebAssembly host written in Rust
that can run components that implement the adder
world.
git clone https://github.com/bytecodealliance/component-docs.git
cd component-docs/component-model/examples/example-host
cargo run --release -- 1 2 <PATH>/adder.wasm
- The double dashes separate the flags passed to
cargo
from the flags passed in to your code. - The arguments 1 and 2 are the arguments to the adder.
- In place of
<PATH>
, substitute the directory that contains your generatedadder.wasm
file.
Note: When hosts run components that use WASI interfaces, they must explicitly add WASI to the linker to run the built component.
A successful run should show the following output
(of course, the paths to your example host and adder component will vary,
and you should substitute adder.wasm
with adder.component.wasm
if you followed the manual instructions above):
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
If not configured correctly, you may see errors like the following:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
This kind of error normally indicates that the host in question does not satisfy WASI imports.
7. Run the component from C/C++ Applications
It is not yet possible to run a WebAssembly Component using the wasmtime
C API.
See wasmtime
issue #6987 for more details.
The C API is preferred over directly using the example host Rust crate in C++.
However, C/C++ language guest components can be composed with components written in any other language
and run by their toolchains,
or even composed with a C language command component and run via the wasmtime
CLI
or any other host.
See the Rust Tooling guide
for instructions on how to run this component from the Rust example-host
(replacing the path to add.wasm
with your adder.wasm
or adder.component.wasm
above).
C# Tooling
WebAssembly components in C# can be built with componentize-dotnet, a NuGet package that can be used to create a fully ahead-of-time-compiled component, giving .NET developers a component experience comparable to those in Rust and TinyGo.
Building a Component with componentize-dotnet
componentize-dotnet
serves as a one-stop shop, wrapping several tools into one:
- NativeAOT-LLVM (compilation)
- wit-bindgen (WIT imports and exports)
- wasm-tools (component conversion)
- WASI SDK (SDK used by NativeAOT-LLVM)
- Wac (used to compose components)
First, install the .NET SDK. For this walkthrough, we’ll use the .NET 10 SDK preview. You should also have wasmtime installed so you can run the binary that you produce. You will also need to install wac for composing components.
1. Create a new project
Once you have the .NET SDK installed, create a new project:
dotnet new install BytecodeAlliance.Componentize.DotNet.Templates
dotnet new componentize.wasi.cli -o adder
cd adder
2. Create or download your WIT world
Next, create or download the WIT world you would like to target.
For this example we will use a WIT file containing two worlds
(we'll only use the example
world at first).
Copy and paste the following into a new file called "wit/component.wit
".
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
In the adder.csproj
project file, add a new <ItemGroup>
at the same level as the existing <ItemGroup>
:
<ItemGroup>
<Wit Update="wit/component.wit" World="example" />
</ItemGroup>
Since this component will only export functionality, dotnet considers this a library project.
Let's update the <OutputType>
to be a library in the adder.csproj
:
- <OutputType>Exe</OutputType>
+ <OutputType>Library</OutputType>
And remove the automatically generated Program.cs
file:
rm Program.cs
3. Write the implementation for the adder
world
If you try to build the project with dotnet build
, you'll get an error like the following:
➜ dotnet build
Restore complete (8.6s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
adder failed with 1 error(s) (25.6s)
/path/to/adder/obj/Debug/net10.0/wasi-wasm/wit_bindgen/AdderWorld.wit.exports.docs.adder.v0_1_0.AddInterop.cs(15,19): error CS0103: The name 'AddImpl' does not exist in the current context
Build failed with 1 error(s) in 34.6s
This is because we've promised an implementation, but haven't yet written one for the example
world.
To fix this, add the following code in a file called Component.cs
:
namespace ExampleWorld.wit.exports.docs.adder.v0_1_0;
public class AddImpl : IAdd
{
public static uint Add(uint x, uint y)
{
return x + y;
}
}
Then, we can build our component:
dotnet build
The component will be available at bin/Debug/net10.0/wasi-wasm/native/adder.wasm
.
4. (optional) Run the component from the example host
The following section requires you to have a Rust toolchain installed.
This repository contains an example WebAssembly host written in Rust
that can run components that implement the adder
world.
git clone https://github.com/bytecodealliance/component-docs.git
cd component-docs/component-model/examples/example-host
cargo run --release -- 1 2 <PATH>/adder.wasm
- The double dashes separate the flags passed to
cargo
from the flags passed in to your code. - The arguments 1 and 2 are the arguments to the adder.
- In place of
<PATH>
, substitute the directory that contains your generatedadder.wasm
file.
Note: When hosts run components that use WASI interfaces, they must explicitly add WASI to the linker to run the built component.
A successful run should show the following output (of course, the paths to your example host and adder component will vary):
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/debug/example-host 1 2 /path/to/adder.wasm`
1 + 2 = 3
If not configured correctly, you may see errors like the following:
cargo run --release -- 1 2 adder.wasm
Compiling example-host v0.1.0 (/path/to/component-docs/component-model/examples/example-host)
Finished `release` profile [optimized] target(s) in 7.85s
Running `target/release/example-host 1 2 /path/to/adder.component.wasm`
Error: Failed to instantiate the example world
Caused by:
0: component imports instance `wasi:io/error@0.2.2`, but a matching implementation was not found in the linker
1: instance export `error` has the wrong type
2: resource implementation is missing
This kind of error normally indicates that the host in question does not satisfy WASI imports.
Building a component that imports an interface
So far, we've been dealing with library components.
Now we will be creating a command component that implements the hostapp
world.
This component will import the add
interface that is exported from our adder
component
and call the add
function.
We will later compose this command component with the adder
library component we just built.
Now we will be taking the adder
component and executing it from another WebAssembly component.
dotnet new componentize.wasi.cli
creates a new project that creates an executable.
Change to the parent directory of your current project and create a new project:
cd ..
dotnet new componentize.wasi.cli -o host-app
cd host-app
Copy the following WIT file into a file called wit/add.wit
in your project:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
Add it to your host-app.csproj
project file as a new ItemGroup
at the top level:
<ItemGroup>
<Wit Update="wit/add.wit" World="hostapp" />
</ItemGroup>
Notice how the World
changed from example
to hostapp
.
The previous examples focused on implementing the class library
for this WIT file—the export
functions.
Now we'll be focusing on the executable side of the application—the hostapp
world.
Modify Program.cs
to look like this:
// Pull in all imports of the `hostapp` world, namely the `add` interface.
// example.component refers to the package name defined in the WIT file.
using HostappWorld.wit.imports.docs.adder.v0_1_0;
uint left = 1;
uint right = 2;
var result = AddInterop.Add(left, right);
Console.WriteLine($"{left} + {right} = {result}");
Once again, compile your component with dotnet build
:
$ dotnet build
Restore complete (0.4s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
host-app succeeded (1.1s) → bin/Debug/net10.0/wasi-wasm/host-app.dll
Build succeeded in 2.5s
At this point, you'll have two WebAssembly components:
- A component that implements the
example
world. - A component that implements the
hostapp
world.
Since the host-app
component depends on the add
function which is defined in the example
world,
it needs to be composed with the first component.
You can compose your host-app
component with your adder
component
by running wac plug
:
wac plug \
bin/Debug/net10.0/wasi-wasm/native/host-app.wasm \
--plug ../adder/bin/Debug/net10.0/wasi-wasm/native/adder.wasm \
-o main.wasm
If you get an error message like:
error: the socket component had no matching imports for the plugs that were provided
then make sure that the package names in both .wit files
(the one for your adder
component and the one for your host-app
component) are the same.
You can also automate the process by adding the following to your host-app.csproj
:
<Target Name="ComposeWasmComponent" AfterTargets="Publish">
<PropertyGroup>
<EntrypointComponent>bin/$(Configuration)/$(TargetFramework)/wasi-wasm/native/host-app.wasm</EntrypointComponent>
<DependencyComponent>../adder/bin/$(Configuration)/$(TargetFramework)/wasi-wasm/native/adder.wasm</DependencyComponent>
</PropertyGroup>
<MakeDir Directories="dist" />
<Exec Command="$(WacExe) plug $(EntrypointComponent) --plug $(DependencyComponent) -o dist/main.wasm" />
</Target>
This requires your original adder.wasm
component to be in ../adder
relative to the directory your host-app
component is in.
If you run dotnet build
again, you will have a composed component in ./dist/main.wasm
.
Then you can run the composed component:
wasmtime run ./dist/main.wasm
1 + 2 = 3
Check out the componentize-dotnet docs for more configuration options.
Go Tooling
The TinyGo compiler v0.34.0 and above has native support for the WebAssembly Component Model and WASI 0.2.0.
This guide walks through building a component that implements adder
world defined in the adder/world.wit
package.
The component will implement the adder
world, which contains add
interface with a add
function.
1. Install the tools
Follow the TinyGo installation instructions to install the TinyGo compiler.
Additionally, install the wasm-tools
CLI tool from the wasm-tools repository.
warning
wit-bindgen-go
comes with its own wasm-tools
vendored version, but tinygo still requires you to install it.
Even if unlikely, this could lead to version mismatch when using older versions of wasm-tools
.
Please make sure to keep your local wasm-tools
udpated, should you encounter any issues.
If using the Rust toolchain to install wasm-tools
, it can be installed like so:
cargo install --locked wasm-tools@1.235.0 --force
or via cargo binstall:
cargo binstall wasm-tools@1.235.0
To verify the installation, run the following commands:
$ tinygo version
tinygo version 0.34.0 ...
$ wasm-tools -V
wasm-tools 1.255.0 ...
Optional: Install the wkg
CLI tool to resolve the imports in the WIT file. The wkg
CLI is a part of the Wasm Component package manager
2. Create your Go project
Now, create your Go project:
mkdir add && cd add
go mod init example.com
Ensure that the following tool
s are installed:
tool (
go.bytecodealliance.org/cmd/wit-bindgen-go
)
note
go tool
was introduced in Golang 1.24 and can be used to manage tooling in Go projects.
Consider also running go mod tidy
after adding the above tool.
2. Determine which World the Component will Implement
Since we will be implementing the adder
world, we can copy the WIT to our project,
under the wit
folder (e.g. wit/component.wit
):
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
The wasip2
target of TinyGo assumes that the component is targeting wasi:cli/command@0.2.0
world
(part of wasi:cli
) so it requires the imports of wasi:cli/imports@0.2.0
.
We need to include those interfaces as well in component.wit
, by editing the adder
world:
world adder {
include wasi:cli/imports@0.2.0;
export add;
}
Using wkg
to automatically resolve and download imports
Tools like wkg
can be convenient to build a complete WIT package by resolving the imports.
Running the wkg wit fetch
command will resolve the imports and populate your wit
folder with all relevant
imported namespaces and packages.
$ wkg wit build
WIT package written to docs:adder@0.1.0.wasm
3. Generate bindings for the Wasm component
Now that we have our WIT definitions bundled together into a WASM file, we can generate the bindings for our Wasm component, by adding a build directive:
go tool wit-bindgen-go generate --world adder --out internal ./docs:adder@0.1.0.wasm
note
The go tool
directive (added in Golang 1.24) installs and enables use of wit-bindgen-go
,
part of the Bytecode Alliance suite of Golang tooling.
The internal
directory will contain the generated Go code that WIT package.
$ tree internal
internal
├── docs
│ └── adder
│ ├── add
│ │ ├── add.exports.go
│ │ ├── add.wasm.go
│ │ ├── add.wit.go
│ │ └── empty.s
│ └── adder
│ └── adder.wit.go
└── wasi
├── cli
│ ├── environment
│ │ ├── empty.s
│ │ ├── environment.wasm.go
│ │ └── environment.wit.go
│ ├── exit
│ │ ├── empty.s
│ │ ├── exit.wasm.go
│ │ └── exit.wit.go
│ ├── stderr
│ │ ├── empty.s
│ │ ├── stderr.wasm.go
│ │ └── stderr.wit.go
│ ├── stdin
│ │ ├── empty.s
│ │ ├── stdin.wasm.go
│ │ └── stdin.wit.go
│ ├── stdout
│ │ ├── empty.s
│ │ ├── stdout.wasm.go
│ │ └── stdout.wit.go
│ ├── terminal-input
│ │ ├── empty.s
│ │ ├── terminal-input.wasm.go
│ │ └── terminal-input.wit.go
│ ├── terminal-output
│ │ ├── empty.s
│ │ ├── terminal-output.wasm.go
│ │ └── terminal-output.wit.go
│ ├── terminal-stderr
│ │ ├── empty.s
│ │ ├── terminal-stderr.wasm.go
│ │ └── terminal-stderr.wit.go
│ ├── terminal-stdin
│ │ ├── empty.s
│ │ ├── terminal-stdin.wasm.go
│ │ └── terminal-stdin.wit.go
│ └── terminal-stdout
│ ├── empty.s
│ ├── terminal-stdout.wasm.go
│ └── terminal-stdout.wit.go
├── clocks
│ ├── monotonic-clock
│ │ ├── empty.s
│ │ ├── monotonic-clock.wasm.go
│ │ └── monotonic-clock.wit.go
│ └── wall-clock
│ ├── empty.s
│ ├── wall-clock.wasm.go
│ └── wall-clock.wit.go
├── filesystem
│ ├── preopens
│ │ ├── empty.s
│ │ ├── preopens.wasm.go
│ │ └── preopens.wit.go
│ └── types
│ ├── abi.go
│ ├── empty.s
│ ├── types.wasm.go
│ └── types.wit.go
├── io
│ ├── error
│ │ ├── empty.s
│ │ ├── error.wasm.go
│ │ └── error.wit.go
│ ├── poll
│ │ ├── empty.s
│ │ ├── poll.wasm.go
│ │ └── poll.wit.go
│ └── streams
│ ├── empty.s
│ ├── streams.wasm.go
│ └── streams.wit.go
├── random
│ ├── insecure
│ │ ├── empty.s
│ │ ├── insecure.wasm.go
│ │ └── insecure.wit.go
│ ├── insecure-seed
│ │ ├── empty.s
│ │ ├── insecure-seed.wasm.go
│ │ └── insecure-seed.wit.go
│ └── random
│ ├── empty.s
│ ├── random.wasm.go
│ └── random.wit.go
└── sockets
├── instance-network
│ ├── empty.s
│ ├── instance-network.wasm.go
│ └── instance-network.wit.go
├── ip-name-lookup
│ ├── abi.go
│ ├── empty.s
│ ├── ip-name-lookup.wasm.go
│ └── ip-name-lookup.wit.go
├── network
│ ├── abi.go
│ ├── empty.s
│ ├── network.wasm.go
│ └── network.wit.go
├── tcp
│ ├── abi.go
│ ├── empty.s
│ ├── tcp.wasm.go
│ └── tcp.wit.go
├── tcp-create-socket
│ ├── empty.s
│ ├── tcp-create-socket.wasm.go
│ └── tcp-create-socket.wit.go
├── udp
│ ├── abi.go
│ ├── empty.s
│ ├── udp.wasm.go
│ └── udp.wit.go
└── udp-create-socket
├── empty.s
├── udp-create-socket.wasm.go
└── udp-create-socket.wit.go
39 directories, 91 files
The adder.exports.go
file contains the exported functions that need to be implemented in the Go code called Exports
.
4. Implement the add
Function
//go:generate go tool wit-bindgen-go generate --world adder --out internal ./docs:adder@0.1.0.wasm
package main
import (
"example.com/internal/docs/adder/add"
)
func init() {
add.Exports.Add = func(x uint32, y uint32) uint32 {
return x + y
}
}
// main is required for the `wasi` target, even if it isn't used.
func main() {}
Go's init
functions are used to do initialization tasks that
should be done before any other tasks. In this case, we are using it to export the Add
function.
5. Build the Component
We can build our component using TinyGo by specifying the wit-package to be add.wit
and the WIT world to be adder
.
Under the hood, TinyGo invokes wasm-tools
to embed the WIT file to the module and componentize it.
tinygo build -target=wasip2 -o add.wasm --wit-package docs:adder@0.1.0.wasm --wit-world adder main.go
WARNING: By default, tinygo includes all debug-related information in your .wasm file. That is desirable when prototyping or testing locally to obtain useful backtraces in case of errors (for example, with
wasmtime::WasmBacktraceDetails::Enable
). To remove debug data and optimize your binary file, build with-no-debug
. The resulting .wasm file will be considerably smaller (up to 75% reduction in size).
We now have an add component that satisfies our adder
world, exporting the add
function, which
We can confirm using the wasm-tools component wit
command:
$ wasm-tools component wit add.wasm
package root:component;
world root {
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:random/random@0.2.0;
export add: func(x: s32, y: s32) -> s32;
}
...
5. Testing the add
Component
To run our add component, we need to use a host program with a WASI runtime that understands the
example
world -- we've provided an example-host
that does just that.
The example host calls the add
function of a passed in component providing two operands.
To use the example host, clone this repository and run the Rust program:
git clone git@github.com:bytecodealliance/component-docs.git
cd component-docs/component-model/examples/example-host
cargo run --release -- 1 2 /path/to/add.wasm
JavaScript Tooling
WebAssembly was originally developed as a technology for running non-JavaScript workloads in the browser at near-native speed.
JavaScript WebAssembly component model support is provided by a combination of tools:
- StarlingMonkey a WebAssembly component aware Javascript engine
componentize-js
a tool for building WebAssembly components from Javascript filesjco
a multi-tool for componentizing, type generation, and running components in NodeJS and browser contexts
Note that Typescript can also be used, given that it is transpiled to JS first by relevant tooling (tsc
).
jco
generates type declaration files (.d.ts
) by default, and also has a jco types
subcommand which generates typings that can be used with a Typescript codebase.
warning
While popular projects like emscripten
also build WebAssembly modules, those modules are not Component Model aware.
Core WebAssembly modules do not contain the advanced features (rich types, structured language interoperation, composition) that the component model makes available.
Installing jco
Installing jco
(which uses componentize-js
can be done via standard NodeJS project tooling:
npm install -g @bytecodealliance/jco
note
jco
and componentize-js
can be installed in a project-local manner with npm install -D
Overview of Building a Component with JavaScript
Building a WebAssembly component with JavaScript often consists of:
- Determining which interface our functionality will target (i.e. a WebAssembly Interface Types ("WIT") world)
- Writing JavaScript that satisfies the interface
- Compiling the interface-compliant JavaScript to WebAssembly
What is WIT?
WebAssembly Interface Types ("WIT") is a featureful Interface Definition Language ("IDL") for defining functionality, but most of the time, you shouldn't need to write WIT from scratch. Often, it's sufficient to download a pre-existing interface that defines what your component should do.
The adder
world contains an interface with a single add
function that sums two numbers:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
note
export
ing the add
interface means that environments that interact with the resulting WebAssembly component
will be able to call the add
function (fully qualified: docs:adder/add.add@0.1.0
).
To learn more about the WIT syntax, check out the WIT explainer
Implementing a JS WebAssembly Component
To implement the adder
world, we can write a JavaScript ES module:
export const add = {
add(x, y) {
return x + y;
}
};
warning
When building your JavaScript project, ensure to set the "type":"module"
option in package.json
,
as jco
works exclusively with JavaScript modules.
In the code above:
- The
adder
world is analogous to the JavaScript module (file) itself - The exported
add
object mirrors theexport
edadd
interface in WIT - The
add
function mirrors theadd
function inside theadd
interface
With the WIT and JavaScript in place, we can use jco
to create a WebAssembly component from the JS module, using jco componentize
.
note
You can also call componentize-js
directly -- it supports both API driven usage and has a CLI.
Our component is so simple (reminiscent of Core WebAssembly, which deals only in numeric
values) that we're actually not using any of the WebAssembly System Interface (access to files, network, etc).
This means that we can --disable
all unneeded WASI functionality when we invoke jco componentize
:
jco componentize \
--wit path/to/adder/world.wit \
--world-name example \
--out adder.wasm \
--disable all \
path/to/adder.js
note
If you're using jco
as a project-local dependency, you can run npx jco
You should see output like the following:
OK Successfully written adder.wasm.
warning
By using --disable all
, your component won't get access to any WASI interfaces that
might be useful for debugging or logging.
For example, you can't console.log(...)
or console.error(...)
without stdio
; you
can't use Math.random()
without random
; and you can't use Date.now()
or new Date()
without clocks
.
Please note that calls to Math.random()
or Date.now()
will return seemingly valid
outputs, but without actual randomness or timestamp correctness.
Running the Component in the example-host
To run the component we've built, we can use the example-host
project:
cd component-model/examples/example-host
cargo run --release -- 1 2 ../path/to/adder.wasm
1 + 2 = 3
warning
The example-host
Rust project uses the Rust toolchain, in particular cargo
,
so to run the code in this section you may need to install some more dependencies (like the Rust toolchain).
While the output isn't exciting, the code contained in example-host
does a lot to make it happen:
- Loads the WebAssembly binary at the provided path (in the command above,
../path/to/adder.wasm
) - Calls the
export
edadd
function inside theadd
interface with arguments - Prints the result
The important Rust code looks something like this:
#![allow(unused)] fn main() { let component = Component::from_file(&engine, path).context("Component file not found")?; let (instance, _) = Example::instantiate_async(&mut store, &component, &linker) .await .context("Failed to instantiate the example world")?; instance .call_add(&mut store, x, y) .await .context("Failed to call add function") }
A quick reminder on the power and new capabilities afforded by WebAssembly -- we've written, loaded, instantiated and executed JavaScript from Rust with a strict interface, without the need for FFI, subprocesses or a network call.
Running a Component from JavaScript Applications (including the Browser)
While JavaScript runtimes available in browsers can execute WebAssembly core modules, they cannot yet execute WebAssembly components, so WebAssembly components (JavaScript or otherwise) must be "transpiled" into a JavaScript wrapper and one or more WebAssembly core modules which can be run by available WebAssembly engines.
Given an existing WebAssembly component (e.g. adder.wasm
which implements the adder
world), we can
"transpile" the WebAssembly component into runnable JavaScript by using jco tranpsile
:
jco transpile adder.wasm -o dist/transpiled
You should see output similar to the following:
Transpiled JS Component Files:
- dist/transpiled/adder.core.wasm 10.1 MiB
- dist/transpiled/adder.d.ts 0.1 KiB
- dist/transpiled/adder.js 1.57 KiB
note
To follow along, see the jco
example adder
component.
With the project pulled locally, you also run npm run transpile
which outputs to dist/transpiled
Thanks to jco
transpilation, you can import the resulting dist/transpiled/adder.js
file and run it from any JavaScript application
using a runtime that supports the core WebAssembly specification as implemented for JavaScript.
To use this component from NodeJS, you can write code like the following:
import { add } from "./dist/transpiled/adder.js";
console.log("1 + 2 = " + add.add(1, 2));
You can execute the JavaScript module with node
directly:
node run.js
You should see output like the following:
1 + 2 = 3
This is directly comparable to the Rust host code mentioned in the previous section. Here, we are able to
use NodeJS as a host for running WebAssembly, thanks to jco
's ability to transpile components.
With jco transpile
any WebAssembly binary (compiled from any language) can be run in JavaScript natively.
Building Reactor Components with jco
Reactor components are WebAssembly components that are long running and meant to be called repeatedly over time. They're analogous to libraries of functionality rather than an executable (a "command" component).
Components expose their interfaces via WebAssembly Interface Types, hand-in-hand with the Component Model which enables components to use higher level types interchangeably.
Exporting WIT Interfaces with jco
Packaging reusable functionality into WebAssembly components isn't useful if we have no way to expose that functionality.
This section offers a slightly deeper dive into the usage of WIT in WebAssembly components that can use the Component Model.
As in the previous example, export
ing WIT interfaces for other components (or a WebAssembly host) to use is fundamental to developing WebAssembly programs.
Let's examine a jco
example project called string-reverse
that exposes functionality for reversing a string.
To build a project like string-reverse
from the ground up, first we'd start with a WIT like the following:
package example:string-reverse@0.1.0
@since(version = 0.1.0)
interface reverse {
reverse-string: func(s: string) -> string;
}
world string-reverse {
export reverse;
}
As a slightly deeper crash course on WIT, here's what the above code describes:
- We've defined a namespace called
example
- We've defined a package called
string-reverse
inside theexample
namespace - This WIT file corresponds to version
0.1.0
ofexample:string-reverse
package - We've defined an interface called
reverse
which contains one function calledreverse-string
- We specify that the
reverse
interface has existed since the0.1.0
version - The
reverse-string
function (AKA.example:string-reverse/reverse.reverse-string
) takes a string and returns a string - We've defined a
world
calledstring-reverse
which exports the functionality provided by thereverse
interface
warning
How do we know that reverse
actually reverses a string?
Unfortunately, that problem is not really solvable at this level -- this is between you and the writer of the component that implements the WIT interface.
Of course, with WebAssembly, you can enforce static checks if you're so inclined, before you run any given binary.
OK now let's see what the JS code looks like to implement the component
world:
/**
* This module is the JS implementation of the `string-reverse` WIT world
*/
/**
* This JavaScript will be interpreted by `jco` and turned into a
* WebAssembly binary with a single export (this `reverse` function).
*/
function reverseString(s) {
return s.reverse();
}
/**
* The JavaScript export below represents the export of the `reverse` interface,
* which which contains `reverse-string` as it's primary exported function.
*/
export const reverse = {
reverseString,
};
note
To view the full code listing along with instructions, see the examples/tutorials/jco/string-reverse
folder
To use jco
to compile this component, you can run the following from the string-reverse
folder:
npx jco componentize \
--wit wit/component.wit \
--world-name component \
--out string-reverse.wasm \
--disable all \
string-reverse.mjs
note
Like the previous example, we're not using any of the advanced WebAssembly System Interface features, so we --disable
all of them
Rather than typing out the jco componentize
command manually, you can also run
the build command with npm run build
from the string-reverse
folder.
You should see output like the following:
OK Successfully written string-reverse.wasm.
Now that we have a WebAssembly binary, we can also use jco
to run it in a native JavaScript context by transpiling
the WebAssembly binary (which could have come from anywhere!) to a JavaScript module.
npx jco transpile string-reverse.wasm -o dist/transpiled
You should see the following output:
Transpiled JS Component Files:
- dist/transpiled/interfaces/example-string-reverse-reverse.d.ts 0.1 KiB
- dist/transpiled/string-reverse.core.wasm 10.1 MiB
- dist/transpiled/string-reverse.d.ts 0.15 KiB
- dist/transpiled/string-reverse.js 2.55 KiB
tip
A gentle reminder that, transpilation does produce Typescript declaration file, for use in Typescript projects.
Now that we have a transpiled module, we can run it from any JavaScript context that supports core WebAssembly (whether NodeJS or the browser).
For NodeJS, we can use code like the following:
// If this import listed below is missing, please run `npm run transpile`
import { reverse } from "./dist/transpiled/string-reverse.mjs";
const reversed = reverse.reverseString("!dlroW olleH");
console.log(`reverseString('!dlroW olleH') = ${reversed}`);
note
In the jco
example project, you can run npm run transpiled-js
to build the existing code.
Assuming you have the dist/transpiled
folder populated (by running jco transpile
in the previous step), you should see output like the following:
reverseString('!dlrow olleh') = hello world!
While it's somewhat redundant in this context, what we've done from NodeJS demonstrates the usefulness of WebAssembly and the jco
toolchain.
With the help of jco
, we have:
- Compiled JavaScript to a WebAssembly module (
jco compile
), adhering to an interface defined via WIT - Converted the compiled WebAssembly module (which could be from any language) to a module that can be used from any compliant JS runtime (
jco transpile
) - Run the transpiled WebAssembly component from a JavaScript native runtime (NodeJS)
Advanced: Importing and Reusing WIT Interfaces via Composition
Just as export
ing functionality is core to building useful WebAssembly components, and similarly import
ing and
reusing functionality is key to using the strengths of WebAssembly.
Restated, WIT and the Component Model enable WebAssembly to compose. This means we can build on top of functionality
that already exists and export
new functionality that depends on existing functionality.
Let's say in addition to the reversing the string (in the previous example) we want to build shared functionality that also upper cases the text it receives.
We can reuse the reversing functionality and export a new interface which enables us to reverse and upper-case.
Let's examine a jco
example project called string-reverse-upper
that exposes
functionality for reversing and upper-casing a string.
Here's the WIT one might write to enable this functionality:
package example:string-reverse-upper@0.1.0;
@since(version = 0.1.0)
interface reversed-upper {
reverse-and-uppercase: func(s: string) -> string;
}
world revup {
//
// NOTE, the import below translates to:
// <namespace>:<package>/<interface>@<package version>
//
import example:string-reverse/reverse@0.1.0;
export reversed-upper;
}
This time, the world
named revup
that we are building relies on the interface reverse
in the package string-reverse
from the namespace example
.
We can make use of any WebAssembly component that matches that interface, as long as we compose their
functionality with the component that implements the revup
world.
The revup
world import
s (and makes use) of reverse
in order to export
(provide) the reversed-upper
interface,
which contains the reverse-and-uppercase
function (in JS, reverseAndUppercase
).
note
Functionality is imported via the interface
, not the world
. world
s can be included/used, but the syntax is slightly different for that.
The JavaScript to make this work (string-reverse-upper.mjs
in jco/examples
) looks like this:
/**
* This module is the JS implementation of the `revup` WIT world
*/
/**
* The import here is *virtual*. It refers to the `import`ed `reverse` interface in component.wit.
*
* These types *do not resolve* when the first `string-reverse-upper` component is built,
* but the types are relevant for the resulting *composed* component.
*/
import { reverseString } from 'example:string-reverse/reverse@0.1.0';
/**
* The JavaScript export below represents the export of the `reversed-upper` interface,
* which which contains `revup` as it's primary exported function.
*/
export const reversedUpper = {
/**
* Represents the implementation of the `reverse-and-uppercase` function in the `reversed-upper` interface
*
* This function makes use of `reverse-string` which is *imported* from another WebAssembly binary.
*/
reverseAndUppercase() {
return reverseString(s).toLocaleUpperCase();
},
};
We can build the component with jco componentize
:
npx jco componentize \
string-reverse-upper.mjs \
--wit wit/ \
--world-name revup \
--out string-reverse-upper.incomplete.wasm \
--disable all
While we've successfully built a WebAssembly component, unlike the other examples, ours is not yet complete.
We can see that if we print the WIT of the generated component by running jco wit
:
npx jco wit string-reverse-upper.incomplete.wasm
You should see output like the following:
package root:component;
world root {
import example:string-reverse/reverse@0.1.0;
export example:string-reverse-upper/reversed-upper@0.1.0;
}
This tells us that the component still has unfulfilled import
s -- we use the reverseString
function that's
in reverse
as if it exists, but it's not yet a real part of the WebAssembly component (hence we've named it .incomplete.wasm
.
To compose the two components (string-reverse-upper/string-reverse-upper.incomplete.wasm
and string-reverse/string-reverse.wasm
we
built earlier), we'll need the WebAssembly Composition tool (wac
). We can use wac plug
:
wac plug \
-o string-reverse-upper.wasm \
--plug ../string-reverse/string-reverse.wasm \
string-reverse-upper.incomplete.wasm
note
You can also run this step with npm run compose
.
A new component string-reverse-upper.wasm
should now be present, which is a "complete" component -- we can check
the output of jco wit
to ensure that all the imports are satisfied:
package root:component;
world root {
export example:string-reverse-upper/reversed-upper@0.1.0;
}
It's as-if we never imported any functionality at all -- the functionality present in string-reverse.wasm
has been
merged into string-reverse-upper.wasm
, and it now simply export
s the advanced functionality.
We can run this completed component with in any WebAssembly-capable native JavaScript environment by using a the transpiled result:
npx jco transpile string-reverse-upper.wasm -o dist/transpiled
note
In the example project, you can run npm run transpile
instead, which will also change the extension on dist/transpiled/string-reverse-upper.js
to .mjs
You should see output like the following:
Transpiled JS Component Files:
- dist/transpiled/interfaces/example-string-reverse-upper-reversed-upper.d.ts 0.12 KiB
- dist/transpiled/string-reverse-upper.core.wasm 10.1 MiB
- dist/transpiled/string-reverse-upper.core2.wasm 10.1 MiB
- dist/transpiled/string-reverse-upper.d.ts 0.19 KiB
- dist/transpiled/string-reverse-upper.js 6.13 KiB
tip
Notice that there are two core WebAssembly files? That's because two core WebAssembly modules were involved in creating the ultimate functionality we needed.
To run the transpiled component, we can write code like the following:
/**
* If this import listed below is missing, please run
*
* ```
* npm run build && npm run compose && npm run transpile`
* ```
*/
import { reversedUpper } from "./dist/transpiled/string-reverse-upper.mjs";
const result = reversedUpper.reverseAndUppercase("!dlroW olleH");
console.log(`reverseAndUppercase('!dlroW olleH') = ${result}`);
note
In the jco
example project, you can run npm run transpiled-js
You should see output like the following:
reverseAndUppercase('!dlroW olleH') = HELLO WORLD!
Python Tooling
Building a Component with componentize-py
componentize-py
is a tool that converts a Python
application to a WebAssembly component.
First, install Python 3.10 or later and pip if you don't already have them. Then, install componentize-py
:
pip install componentize-py
Next, create or download the WIT world you would like to target. For this example we will use an adder
world with an add
function (e.g. wit/component.wit
):
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
If you want to generate bindings produced for the WIT world (for an IDE or typechecker), you can generate them using the bindings
subcommand. Specify the path to the WIT interface with the world you are targeting:
componentize-py --wit-path wit --world adder bindings .
note
You do not need to generate the bindings in order to componentize
in the next step. componentize
will generate bindings on-the-fly and bundle them into the produced component.
If you attempt to run bindings generation twice, it will fail if the bindings
folder already exists.
Bindings are generated in a folder called wit_world
by default:
<project folder>
├── wit
│ └── component.wit
└── wit_world
├── exports
│ ├── add.py
│ └── __init__.py
├── __init__.py
└── types.py
The wit_world/exports
folder contains an Add
protocol which has an add
method that we can implement,
which represents the export defined in the adder
world WIT.
To implement the adder
world (in particular the add
interface that is exported), put the following code
in a file called app.py
:
from wit_world import exports
class Add(exports.Add):
def add(self, x: int, y: int) -> int:
return x + y
We now can compile our application to a Wasm component using the componentize
subcommand:
componentize-py \
--wit-path wit/component.wit \
--world adder \
componentize \
app \
-o add.wasm
Component built successfully
To test the component, run it using the Rust add
host:
$ cd component-model/examples/add-host
$ cargo run --release -- 1 2 ../path/to/add.wasm
1 + 2 = 3
See componentize-py
's examples to try out build HTTP, CLI, and TCP components from Python applications.
Running components from Python Applications
Wasm components can also be invoked from Python applications. This section walks through using tooling
to call the pre-built app.wasm
component in the examples.
wasmtime-py
is only able to run components built withcomponentize-py
when the--stub-wasi
option is used at build time. This is becausewasmtime-py
does not yet support resources, andcomponentize-py
by default generates components which use resources from thewasi:cli
world. See this example of using the--stub-wasi
option to generate awasmtime-py
-compatible component.
First, install Python 3.11 or later and pip if you don't already have them. Then, install wasmtime-py
:
$ pip install wasmtime
First, generate the bindings to be able to call the component from a Python host application.
# Get an add component that does not import the WASI CLI world
$ wget https://github.com/bytecodealliance/component-docs/raw/main/component-model/examples/example-host/add.wasm
$ python3 -m wasmtime.bindgen add.wasm --out-dir add
The generated package add
has all of the requisite exports/imports for the
component and is annotated with types to assist with type-checking and
self-documentation as much as possible. Inside the package is a Root
class
with an add
function that calls the component's exported add
function. We
can now write a Python program that calls add
:
from add import Root
from wasmtime import Store
def main():
store = Store()
component = Root(store)
print("1 + 2 =", component.add(store, 1, 2))
if __name__ == '__main__':
main()
Run the Python host program:
$ python3 host.py
1 + 2 = 3
Components in Rust
Rust has first-class support for the component model via the cargo-component
tool.
We will be using the cargo component
subcommand to create WebAssembly components using Rust as
the component's implementation language.
note
You can find more details about cargo-component
on crates.io.
1. Setup
Install cargo-component
:
cargo install --locked cargo-component
Install wasm-tools
:
cargo install --locked wasm-tools
Install wasmtime
:
curl https://wasmtime.dev/install.sh -sSf | bash
2. Scaffolding a Component
We will create a component in Rust that implements the add
function exported
by the adder
world world in the docs:adder
package.
First, we will create a new WebAssembly component package called add
:
cargo component new add --lib && cd add
3. Adding the WIT world
We now need to change our generated wit/world.wit
to match docs:adder
:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
The package.metadata.component
section of our Cargo.toml
should be changed
to the following:
[package.metadata.component]
package = "docs:adder"
4. Generating bindings
Now that we've updated our world.wit
and Cargo.toml
, we can re-generate
bindings with the command below:
cargo component bindings
cargo-component
will generate bindings for our
world and create a Guest
trait that a component should
implement.
5. Implementing the Guest
trait
Implement the Guest
trait in src/lib.rs
, using the scaffolded code. Your code should look something like the following:
#[allow(warnings)]
mod bindings;
// The comments that follow the `use` declaration below
// correlate the rust module path segments with their
// `world.wit` counterparts:
use bindings::exports::docs::adder::add::Guest;
// <- items bundled with `export` keyword
// <- package namespace
// <- package
// <- interface name
struct Component;
impl Guest for Component {
fn add(x: u32, y: u32) -> u32 {
x + y
}
}
bindings::export!(Component with_types_in bindings);
6. Building a Component
Now, let's build our component, being sure to optimize with a release build:
cargo component build --release
warning
Building with --release
removes all debug-related information from the resulting .wasm
file.
When prototyping or testing locally, you might want to avoid --release
to
obtain useful backtraces in case of errors (for example, with
wasmtime::WasmBacktraceDetails::Enable
).
Note: the resulting .wasm
file will be considerably larger (likely 4MB+).
You can use wasm-tools
to output the WIT package of the component:
wasm-tools component wit target/wasm32-wasip1/release/add.wasm
The command above should produce the output below:
package root:component;
world root {
export docs:adder/add@0.1.0;
}
package docs:adder@0.1.0 {
interface add {
add: func(x: u32, y: u32) -> u32;
}
}
Running a Component
To verify that our component works, lets run it from a Rust application that knows how to run a
component targeting the adder
world.
The application uses wasmtime
crates to generate
Rust bindings, bring in WASI worlds, and execute the component.
$ cd examples/example-host
$ cargo run --release -- 1 2 ../add/target/wasm32-wasip1/release/adder.wasm
1 + 2 = 3
Importing an interface
The world file (wit/world.wit
) we generated doesn't specify any imports.
If your component consumes other components, you can edit the world.wit
file to import their interfaces.
note
This section is about importing custom WIT interfaces from library components.
By default, cargo-component
imports any required WASI interfaces
for us without needing to explicitly declare them.
For example, suppose you have created and built an adder component as explained in the exporting an interface section and want to use that component in a calculator component. Here is a partial example world for a calculator that imports the add interface:
// in the 'calculator' project
// wit/world.wit
package docs:calculator;
interface calculate {
eval-expression: func(expr: string) -> u32;
}
world calculator {
export calculate;
import docs:adder/add@0.1.0;
}
Referencing the package to import
Because the docs:adder
package is in a different project, we must first tell cargo component
how to find it. To do this, add the following to the Cargo.toml
file:
[package.metadata.component.target.dependencies]
"docs:adder" = { path = "../adder/wit" } # directory containing the WIT package
note
The path for docs:adder
is relative to the wit
directory, not to the world.wit
file.
A WIT package may be spread across multiple files in the same directory; cargo component
will search them all.
Calling the import from Rust
Now the declaration of add
in the adder's WIT file is visible to the calculator
project. To invoke the imported add
interface from the calculate
implementation:
#![allow(unused)] fn main() { // src/lib.rs mod bindings; use bindings::exports::docs::calculator::calculate::Guest; // Bring the imported add function into scope use bindings::docs::calculator::add::add; struct Component; impl Guest for Component { fn eval_expression(expr: String) -> u32 { // Cleverly parse `expr` into values and operations, and evaluate // them meticulously. add(123, 456) } } }
Fulfilling the import
When you build this using cargo component build
, the add
interface remains imported. The calculator has taken a dependency on the add
interface, but has not linked the adder
implementation of that interface - this is not like referencing the adder
crate. (Indeed, calculator
could import the add
interface even if there was no Rust project implementing the WIT file.) You can see this by running wasm-tools component wit
to view the calculator's world:
# Do a release build to prune unused imports (e.g. WASI)
$ cargo component build --release
$ wasm-tools component wit ./target/wasm32-wasip1/release/calculator.wasm
package root:component;
world root {
import docs:adder/add@0.1.0;
export docs:calculator/calculate@0.1.0;
}
As the import is unfulfilled, the calculator.wasm
component could not run by itself in its current form. To fulfill the add
import, so that
only calculate
is exported, you would need to compose the calculator.wasm
with some adder.wasm
into a single, self-contained component.
Creating a command component with cargo component
A command is a component with a specific export that allows it to be executed directly by wasmtime
(or other wasi:cli
hosts). In Rust terms, it's the equivalent of an application (bin
) package with a main
function, instead of a library crate (lib
) package.
To create a command with cargo component, run:
cargo component new <name>
Unlike library components, this does not have the --lib
flag. You will see that the created project is different too:
- It doesn't contain a
.wit
file.cargo component build
will automatically export thewasi:cli/run
interface for Rustbin
packages, and hook it up tomain
. - Because there's no
.wit
file,Cargo.toml
doesn't contain apackage.metadata.component.target
section. - The Rust file is called
main.rs
instead oflib.rs
, and contains amain
function instead of an interface implementation.
You can write Rust in this project, just as you normally would, including importing your own or third-party crates.
All the crates that make up your project are linked together at build time, and compiled to a single Wasm component. In this case, all the linking is happening at the Rust level: no WITs or component composition is involved. Only if you import Wasm interfaces do WIT and composition come into play.
To run your command component:
cargo component build
wasmtime run ./target/wasm32-wasip1/debug/<name>.wasm
WARNING: If your program prints to standard out or error, you may not see the printed output! Some versions of
wasmtime
have a bug where they don't flush output streams before exiting. To work around this, add astd::thread::sleep()
with a 10 millisecond delay before exitingmain
.
Importing an interface into a command component
As mentioned above, cargo component build
doesn't generate a WIT file for a command component. If you want to import a Wasm interface, though, you'll need to create a WIT file and a world, plus reference the packages containing your imports:
-
Add a
wit/world.wit
to your project, and write a WIT world that imports the interface(s) you want to use. For example:package docs:app; world app { import docs:calculator/calculate@0.1.0; }
cargo component
sometimes fails to find packages if versions are not set explicitly. For example, if the calculator WIT declarespackage docs:calculator
rather thandocs:calculator@0.1.0
, then you may get an error even thoughcargo component build
automatically versions the binary export. -
Edit
Cargo.toml
to tellcargo component
about the new WIT file:[package.metadata.component.target] path = "wit"
(This entry is created automatically for library components but not for command components.)
-
Edit
Cargo.toml
to tellcargo component
where to find external package WITs:[package.metadata.component.target.dependencies] "docs:calculator" = { path = "../calculator/wit" } "docs:adder" = { path = "../adder/wit" }
If the external package refers to other packages, you need to provide the paths to them as well.
-
Use the imported interface in your Rust code:
use bindings::docs::calculator::calculate::eval_expression; fn main() { let result = eval_expression("1 + 1"); println!("1 + 1 = {result}"); }
-
Compose the command component with the
.wasm
components that implement the imports. -
Run the composed component:
$ wasmtime run ./my-composed-command.wasm 1 + 1 = 579 # might need to go back and do some work on the calculator implementation
Using user-defined types
User-defined types map to Rust types as follows.
WIT type | Rust binding |
---|---|
record | struct with public fields corresponding to the record fields |
variant | enum with cases corresponding to the variant cases |
enum | enum with cases corresponding to the enum cases, with no data attached |
resource | See below |
flags | Opaque type supporting bit flag operations, with constants for flag values |
For example, consider the following WIT:
interface types {
enum operation {
add,
sub,
mul,
div
}
record expression {
left: u32,
operation: operation,
right: u32
}
eval: func(expr: expression) -> u32;
}
When exported from a component, this could be implemented as:
#![allow(unused)] fn main() { impl Guest for Implementation { fn eval(expr: Expression) -> u32 { // Record fields become public fields on a struct let (l, r) = (expr.left, expr.right); match expr.operation { // Enum becomes an enum with only unit cases Operation::Add => l + r, Operation::Sub => l - r, Operation::Mul => l * r, Operation::Div => l / r, } } } }
Using resources
Resources are handles to entities that live outside the component, for example in a host, or in a different component.
Example
In this section, our example resource will be a Reverse Polish Notation (RPN) calculator. (Engineers of a certain vintage will remember this from handheld calculators of the 1970s.) A RPN calculator is a stateful entity: a consumer pushes operands and operations onto a stack maintained within the calculator, then evaluates the stack to produce a value. The resource in WIT looks like this:
package docs:rpn@0.1.0;
interface types {
enum operation {
add,
sub,
mul,
div
}
resource engine {
constructor();
push-operand: func(operand: u32);
push-operation: func(operation: operation);
execute: func() -> u32;
}
}
world calculator {
export types;
}
Implementing and exporting a resource in a component
To implement the calculator using cargo component
:
-
Create a library component as shown in previous sections, with the WIT given above.
-
Define a Rust
struct
to represent the calculator state:#![allow(unused)] fn main() { use std::cell::RefCell; struct CalcEngine { stack: RefCell<Vec<u32>>, } }
Why is the stack wrapped in a
RefCell
? As we will see, the generated Rust trait for the calculator engine has immutable references toself
. But our implementation of that trait will need to mutate the stack. So we need a type that allows for interior mutability, such asRefCell<T>
orArc<RwLock<T>>
. -
The generated bindings (
bindings.rs
) for an exported resource include a trait namedGuestX
, whereX
is the resource name. (You may need to runcargo component build
to regenerate the bindings after updating the WIT.) For the calculatorengine
resource, the trait isGuestEngine
. Implement this trait on thestruct
from step 2:#![allow(unused)] fn main() { use bindings::exports::docs::rpn::types::{GuestEngine, Operation}; impl GuestEngine for CalcEngine { fn new() -> Self { CalcEngine { stack: RefCell::new(vec![]) } } fn push_operand(&self, operand: u32) { self.stack.borrow_mut().push(operand); } fn push_operation(&self, operation: Operation) { let mut stack = self.stack.borrow_mut(); let right = stack.pop().unwrap(); // TODO: error handling! let left = stack.pop().unwrap(); let result = match operation { Operation::Add => left + right, Operation::Sub => left - right, Operation::Mul => left * right, Operation::Div => left / right, }; stack.push(result); } fn execute(&self) -> u32 { self.stack.borrow_mut().pop().unwrap() // TODO: error handling! } } }
-
We now have a working calculator type which implements the
engine
contract, but we must still connect that type to theengine
resource type. This is done by implementing the generatedGuest
trait. For this WIT, theGuest
trait contains nothing except an associated type. You can use an emptystruct
to implement theGuest
trait on. Set the associated type for the resource - in our case,Engine
- to the type which implements the resource trait - in our case, theCalcEngine
struct
which implementsGuestEngine
. Then use theexport!
macro to export the mapping:#![allow(unused)] fn main() { struct Implementation; impl Guest for Implementation { type Engine = CalcEngine; } bindings::export!(Implementation with_types_in bindings); }
This completes the implementation of the calculator engine
resource. Run cargo component build
to create a component .wasm
file.
Importing and consuming a resource in a component
To use the calculator engine in another component, that component must import the resource.
-
Create a command component as shown in previous sections.
-
Add a
wit/world.wit
to your project, and write a WIT world that imports the RPN calculator types:package docs:rpn-cmd; world app { import docs:rpn/types@0.1.0; }
-
Edit
Cargo.toml
to tellcargo component
about the new WIT file and the external RPN package file:[package.metadata.component] package = "docs:rpn-cmd" [package.metadata.component.target] path = "wit" [package.metadata.component.target.dependencies] "docs:rpn" = { path = "../wit" } # or wherever your resource WIT is
-
The resource now appears in the generated bindings as a
struct
, with appropriate associated functions. Use these to construct a test app:#[allow(warnings)] mod bindings; use bindings::docs::rpn::types::{Engine, Operation}; fn main() { let calc = Engine::new(); calc.push_operand(1); calc.push_operand(2); calc.push_operation(Operation::Add); let sum = calc.execute(); println!("{sum}"); }
You can now build the command component and compose it with the .wasm
component that implements the resource.. You can then run the composed command with wasmtime run
.
Implementing and exporting a resource implementation in a host
If you are hosting a Wasm runtime, you can export a resource from your host for guests to consume. Hosting a runtime is outside the scope of this book, so we will give only a broad outline here. This is specific to the Wasmtime runtime; other runtimes may express things differently.
-
Use
wasmtime::component::bindgen!
to specify the WIT you are a host for:#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit" }); }
-
Tell
bindgen!
how you will represent the resource in the host via thewith
field. This can be any Rust type. For example, the RPN engine could be represented by aCalcEngine
struct:#![allow(unused)] fn main() { wasmtime::component::bindgen!({ path: "../wit", with: { "docs:rpn/types/engine": CalcEngine, } }); }
If you don't specify the host representation for a resource, it defaults to an empty enum. This is rarely useful as resources are usually stateful.
-
If the representation type isn't a built-in type, define it:
#![allow(unused)] fn main() { struct CalcEngine { /* ... */ } }
-
As a host, you will already be implementing a
Host
trait. You will now need to implement aHostX
trait (whereX
is the resource name) on the same type as theHost
trait:#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new(&mut self) -> wasmtime::component::Resource<docs::rpn::types::Engine> { /* ... */ } fn push_operand(&mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>) { /* ... */ } // etc. } }
Important: You implement this on the 'overall' host type, not on the resource representation! Therefore, the
self
reference in these functions is to the 'overall' host type. For instance methods of the resource, the instance is identified by a second parameter (self_
), of typewasmtime::component::Resource
. -
Add a
wasmtime::component::ResourceTable
to the host:#![allow(unused)] fn main() { struct MyHost { calcs: wasmtime::component::ResourceTable, } }
-
In your resource method implementations, use this table to store and access instances of the resource representation:
#![allow(unused)] fn main() { impl docs::rpn::types::HostEngine for MyHost { fn new(&mut self) -> wasmtime::component::Resource<docs::rpn::types::Engine> { self.calcs.push(CalcEngine::new()).unwrap() // TODO: error handling } fn push_operand(&mut self, self_: wasmtime::component::Resource<docs::rpn::types::Engine>) { let calc_engine = self.calcs.get(&self_).unwrap(); // calc_engine is a CalcEngine - call its functions } // etc. } }
Language Agnostic Tooling
wasm-tools
provides a suite of subcommands for
working with WebAssembly modules and components.
WAT (WebAssembly Text Format)
WAT (WebAssembly Text Format) is a text-based language
that can be compiled to the WebAssembly binary format
by wasm-tools
and other tools.
It's useful for writing small examples for testing and experimentation.
Here's an example of a module expressed in WAT:
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "docs:adder/add@0.1.0#add" (func $add))
)
The module contains two top-level declarations, a function and an export.
The function declaration declares a function named $add
with two arguments, $lhs
and $rhs
.
(Variable names in WAT always start with a $
.)
Argument and result types need to be provided explicitly.
In this case, the types of both arguments and the result
are i32
(32-bit integer).
The body of the function is a list of WebAssembly instructions.
The two local.get
instructions push the values of $lhs
and $rhs
onto the stack.
The i32.add
instruction pops the two top values off the stack
and adds them, leaving the result on the stack.
The export
declaration connects the function that was just declared
to a name that should be used for calling it externally.
We want to use this WAT code to implement the interface specified in a WIT file,
so the external name has to follow a certain convention.
The name "docs:adder/add@0.1.0#add"
can be broken down as follows:
docs
is the package name.adder
is the name of a world inside thedocs
package.add
is the name of an interface defined in that world.- 0.1.0 is a version number.
- Separately,
add
is the name of a function defined in theadd
interface. All of these pieces come from the specific.wit
file we are using (see below).
There's much more than WAT can do; see the Mozilla Developer Network's a detailed guide to WAT for more information.
The wat2wasm tool converts
from WAT to the binary .wasm
format,
but it does not create components.
Building a Component from WAT with wasm-tools
wasm-tools
can be used to create a component from WAT.
Here's how to create a component from WAT
that implements the adder
world
and simply adds two numbers.
-
Install
wasm-tools
, a tool for low-level manipulation of Wasm modules and components. -
The
add
function is defined inside the following world. Create a file calledadder.wit
whose contents are as follows:
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
- Define an
add
core module in WAT that exports anadd
function that adds two parameters. Create a file calledadd.wat
whose contents are as follows (the same as the example in the WAT section):
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "docs:adder/add@0.1.0#add" (func $add))
)
-
Use
wasm-tools
to create a binary core module with component metadata embedded inside it:wasm-tools component embed adder.wit add.wat -o add.wasm
-
Use
wasm-tools
to create a new component.wasm
file from the binary core module you just created:wasm-tools component new add.wasm -o add.component.wasm
The suffix
.component.wasm
is just a convention. You could also name the output fileadd_component.wasm
or anything else with the.wasm
suffix.
Running a Component with Wasmtime
You can "run" a component by calling one of its exports. Hosts and runtimes often only support running components with certain exports.
Using the wasmtime
CLI,
we can execute the add
function in the component you just built,
passing in arguments:
wasmtime run --invoke 'add(1, 2)' add.component.wasm
The output is 3
.
You can try passing other arguments to add()
by changing the arguments inside the parentheses.
This example was tested with wasmtime
34.0.1.
Earlier versions of wasmtime
may not support the --invoke
option.
Any other compliant WebAssembly runtime that supports components
can also run this component.
Running Components
There are two standard wit worlds that runtimes support. These worlds are the wasi:cli/command
world and the wasi:http/proxy
world. All other wit worlds and interfaces are considered to be custom. In the following sections, you'll see how to run components that implement either world, as well as how to invoke custom exports.
Wasmtime
Wasmtime is the reference implementation of the Component Model. It supports running components that implement the wasi:cli/command
world and serving components that implement the wasi:http/proxy
world. Wasmtime can also invoke functions exported from a component.
Running command components with Wasmtime
To run a command component with Wasmtime, execute:
wasmtime run <path-to-wasm-file>
If you are using an older version of
wasmtime
, you may need to add the--wasm component-model
flag to specify that you are running a component rather than a core module.
By default, Wasmtime denies the component access to all system resources. For example, the component cannot access the file system or environment variables. See the Wasmtime guide for information on granting access, and for other Wasmtime features.
Running HTTP components with Wasmtime
You can now execute components that implement the HTTP proxy world with the wasmtime serve
subcommand. The Wasmtime CLI supports serving these components as of v14.0.3
.
To run a HTTP component with Wasmtime, execute:
wasmtime serve <path-to-wasm-file>
Try out building and running HTTP components with one of these tutorials
-
Hello WASI HTTP tutorial - build and serve a simple Rust-based HTTP component
-
HTTP Auth Middleware tutorial - compose a HTTP authentication middleware component with a business logic component
Running components with custom exports
As of Wasmtime Version 33.0.0, there is support for invoking components with custom exports.
As an example, if your component exports a function add
which takes two numeric arguments, you can make use of this feature with the following command.
wasmtime run --invoke 'add(1, 2)' <path-to-wasm-file>
Make sure to wrap your invocation in single quotes and to include parentheses, even if your function doesn't take any arguments. For a full list of ways to represent the various wit types when passing arguments to your exported function, visit the WAVE repo.
jco
jco is a fully native JavaScript tool for working with components in JavaScript. It supports the wasi:cli/command
world. jco
also provides features for transpiling Wasm components to ES modules, and for building Wasm components from JavaScript and WIT.
To run a component with jco
, run:
jco run <path-to-wasm-file> <command-args...>
jco
's WASI implementation grants the component full access to the underlying system resources. For example, the component can read all environment variables of the jco
process, or read and write files anywhere in the file system.
Composing and Distributing Components
The component model defines how components interface to each other and to hosts. This section describes how to work with components - from authoring them in custom code or by composing existing components, through to using them in applications and distributing them via registries.
Composing Components
Because the WebAssembly component model packages code in a portable binary format, and provides machine-readable interfaces in WIT with a standardised ABI (Application Binary Interface), it enables applications and components to work together, no matter what languages they were originally written in. In the same way that, for example, a Rust package (crate) can be compiled together with other Rust code to create a higher-level library or an application, a Wasm component can be linked with other components.
Component model interoperation is more convenient and expressive than language-specific foreign function interfaces. A typical C FFI involves language-specific types, so it is not possible to link between arbitrary languages without at least some C-language wrapping or conversion. The component model, by contrast, provides a common way of expressing interfaces, and a standard binary representation of those interfaces. So if an import and an export have the same shape, they fit together directly.
What is composition?
When you compose components, you wire up the imports of one "primary" component to the exports of one or more other "dependency" components, creating a new component. The new component, like the original components, is a .wasm
file, and its interface is defined as:
- The new component exports the same exports as the primary component
- The new component does not export the exports of the dependencies
- The new component imports all the imports of the dependency components
- The new component imports any imports of the primary component imports that the dependencies didn't satisfy
- If several components import the same interface, the new component imports that interface - it doesn't "remember" that the import was declared in several different places
For example, consider two components with the following worlds:
// component `validator`
package docs:validator@0.1.0;
interface validator {
validate-text: func(text: string) -> string;
}
world validator {
export validator;
import docs:regex/match@0.1.0;
}
// component 'regex'
package docs:regex@0.1.0;
interface match {
first-match: func(regex: string, text: string) -> string;
}
world regex {
export match;
}
If we compose validator
with regex
, validator
's import of docs:regex/match@0.1.0
is wired up to regex
's export of match
. The net result is that the composed component exports docs:validator/validator@0.1.0
and has no imports. The composed component does not export docs:regex/match@0.1.0
- that has become an internal implementation detail of the composed component.
Component composition tools are in their early stages right now. Here are some tips to avoid or diagnose errors:
- Composition happens at the level of interfaces. If the initial component directly imports functions, then composition will fail. If composition reports an error such as "component
path/to/component
has a non-instance import named<name>
" then check that all imports and exports are defined by interfaces. - Composition is asymmetrical. It is not just "gluing components together" - it takes a primary component which has imports, and satisfies its imports using dependency components. For example, composing an implementation of
validator
with an implementation ofregex
makes sense becausevalidator
has a dependency thatregex
can satisfy; doing it the other way round doesn't work, becauseregex
doesn't have any dependencies, let alone ones thatvalidator
can satisfy. - Composition cares about interface versions, and current tools are inconsistent about when they infer or inject versions. For example, if a Rust component exports
test:mypackage
,cargo component build
will decorate this with the crate version, e.g.test:mypackage@0.1.0
. If another Rust component imports an interface fromtest:mypackage
, that won't matchtest:mypackage@0.1.0
. You can usewasm-tools component wit
to view the imports and exports embedded in the.wasm
files and check whether they match up.
Composing components with WAC
You can use the WAC CLI to compose components at the command line.
To perform quick and simple compositions, use the wac plug
command. wac plug
satisfies the import of a "socket" component by plugging a "plug" component's export into the socket. For example, a component that implements the validator
world above needs to satisfy it's match
import. It is a socket. While a component that implements the regex
world, exports the match
interface, and can be used as a plug. wac plug
can plug a regex component's export into the validator component's import, creating a resultant composition:
wac plug validator-component.wasm --plug regex-component.wasm -o composed.wasm
A component can also be composed with two components it depends on.
wac plug path/to/component.wasm --plug path/to/dep1.wasm --plug path/to/dep2.wasm -o composed.wasm
Here component.wasm
is the component that imports interfaces from dep1.wasm
and dep2.wasm
, which export them. The composed component, with those dependencies satisfied and tucked away inside it, is saved to composed.wasm
.
The plug
syntax doesn't cover transitive dependencies. If, for example, dep1.wasm
has unsatisfied imports that you want to satisfy from dep3.wasm
, you'd need to be deliberate about the order of your composition. You could compose dep1.wasm
with dep3.wasm
first, then refer to that composed component instead of dep1.wasm
. However, this doesn't scale to lots of transitive dependencies, which is why the WAC language was created.
Advanced composition with the WAC language
wac plug
is a convenience to achieve a common pattern in component compositions like above. However, composition can be arbitrarily complicated. In cases where wac plug
is not sufficient, the WAC language can give us the ability to create arbitrarily complex compositions.
In a WAC file, you use the WAC language to describe a composition. For example, the following is a WAC file that could be used to create that validator component from earlier.
//composition.wac
// Provide a package name for the resulting composition
package docs:composition;
// Instantiate the regex-impl component that implements the `regex` world. Bind this instance's exports to the local name `regex`.
let regex = new docs:regex-impl { };
// Instantiate the validator-impl component which implements the `validator` world and imports the match interface from the regex component.
let validator = new docs:validator-impl { match: regex.match, ... };
// Export all remaining exports of the validator instance
export validator...;
Then, wac compose
can be used to compose the components, passing in the paths to the components. Alternatively, you can place the components in a deps
directory with an expected structure, and in the near future, you will be able to pull in components from registries. See the wac
documentation for more details.
wac compose --dep docs:regex-impl=regex-component.wasm --dep docs:validator-impl=validator-component.wasm -o composed.wasm composition.wac
For an in depth description about how to use the wac tool, you can check out the wac language index and examples.
Composing components with a visual interface
You can compose components visually using the builder app at wasmbuilder.app.
-
Use the Add Component Button to upload the
.wasm
component files you want to compose. The components appear in the sidebar. -
Drag the components onto the canvas. You'll see imports listed on the left of each component, and exports on the right.
-
Click the box in the top left to choose the 'primary' component, that is, the one whose exports will be preserved. (The clickable area is quite small - wait for the cursor to change from a hand to a pointer.)
-
To fulfil one of the primary component's imports with a dependency's export, drag from the "I" icon next to the export to the "I" item next to the import. (Again, the clickable area is quite small - wait for the cursor to change from a hand to a cross.)
-
When you have connected all the imports and exports that you want, click the Download Component button to download the composed component as a
.wasm
file.
Distributing and Fetching Components and WIT
Modern applications rely extensively on third-party packages - so extensively that distributing packages is almost an industry in itself. Traditionally, these have been specific to a language. For example, JavaScript developers are used to using packages from NPM, and Rust developers use crates.io
. Some runtimes support binary distribution and linking, enabling limited cross-language interop; for example, Maven packages can be written in any language that targets the Java runtime. Services like this are variously referred to as "package managers" or "registries."
Publishing and distribution are not defined by the core component model, but form important part of the component ecosystem. For example, if you're writing JavaScript, and want to pull in a highly optimised machine learning algorithm written in C and compiled to Wasm, you can pull it from a registry, ideally just as easily as you would add a NPM package from the NPM registry.
You can get involved with improving the packaging and hosting of Wasm components by joining the Bytecode Alliance Packaging Special Interest Group (SIG).
The wkg
Registry Tool
The wasm-pkg-tools
project enables fetching and publishing Wasm components to OCI registries. It contains a wkg
CLI tool that eases distributing and fetching components and WIT packages. The usual way of using wkg
is to address packages by their package name, i.e. example:adder@1.0.0
. When using wkg
this way, you don't need to know about the physical location of the package, as the wkg
configuration handles that for you. If you need to, though, you can also use wkg
to work with OCI artifacts directly, addressing them by OCI references when using the wkg oci
subcommand.
wkg
contains several subcommand:
wkg oci
- pushes/pulls Wasm artifacts to/from any OCI registrywkg publish
- publish components or WIT packages by package namewkg get
- pulls components or WIT packages by package namewkg wit
- commands for interacting with WIT files and dependencieswkg config
- interact with thewkg
configuration
The following sections detail a subset of actions that can be performed with wkg
.
wkg
Configuration Files
When you use most wkg
commands (wkg oci
being the exception), you don't interact with physical locations, only with package names. The wkg
configuration file is used to map package naming to physical location. It provides the ability to configure:
- The default registry for packages in a given namespace. For example, the location for
wasi
packages such aswasi:clocks
orwasi:http
. - Registry overrides for specific packages, or packages not stored in the same place as the rest of their namespace. For example, if
wasi:key-value
were stored in a different registry from otherwasi
packages. - The default registry for all packages not listed in one of the previous sections
The configuration file also includes credentials for private registries, or for pushing to registries where you have permission, and other configuration options. See the wkg
docs for more configuration options.
For example, to fetch WASI packages, such as wasi:clocks
and wasi:http
, you can add a line under the namespace_registries
section for the wasi
namespace. Specifically, the example below configures wkg
to fetch WASI packages from the WebAssembly OCI GitHub Container Registry, where the latest interfaces are published upon WASI releases. To edit your wkg
config file, run wkg config --edit
.
Remember, all package names consist of the a namespace field followed by the package field. The package name
wasi:clocks
has a namespace ofwasi
and package field ofclocks
. In this way, the following configuration ensures thatwkg
will know to route fetches and publishes of anywasi:<package field>
to the configured location.
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Tell wkg that packages with the `wasi` namespace are in an OCI registry under ghcr.io/webassembly
wasi = { registry = "wasi", metadata = { preferredProtocol = "oci", "oci" = {registry = "ghcr.io", namespacePrefix = "webassembly/" } } }
As a more generic example, The following configuration, instructs wkg
to use ttl.sh OCI registry for all packages with the docs
namespace.
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Instruct wkg to use the OCI protocol to fetch packages with the `foo` namespace from ttl.sh/wasm-components
docs = { registry = "docs-registry", metadata = { preferredProtocol = "oci", "oci" = {registry = "ttl.sh", namespacePrefix = "wasm-components/" } } }
Note: the registry name can be referenced in the
package_registry_overrides
section of thewkg
config to provide overrides for specific packages of a namespace.
Distributing WIT and Components by Package Name with wkg publish
Once you've configured wkg
to know where to publish packages to, you can use the wkg publish
command to publish components or interfaces to be consumed by others.
Imagine you have defined the following adder
world in WIT:
package docs:adder@0.1.0;
interface add {
add: func(a: u32, b: u32) -> u32;
}
world adder {
export add;
}
You can publish this WIT using wkg
by wrapping it up as a Wasm component. Yes, you heard that right! We are packaging WIT as Wasm.
# Package the contents of add WIT directory as Wasm
wkg wit build --wit-dir tutorial/wit/adder
# Publish the produced component
wkg publish docs:adder@0.1.0.wasm
If you had configured wkg
as described in the wkg
configuration section, this would publish the component to ttl.sh/wasm-components/docs/adder:0.1.0
. This WIT can then be fetched using wkg get
, specifying the format wit
:
wkg get --format wit docs:adder@0.1.0 --output adder.wit
Instead of publishing the WIT interface, you could publish the built component by running:
wkg publish adder.wasm --package docs:adder@0.1.0
You can then fetch the component by running:
wkg get docs:adder@0.1.0 --output adder.wasm
More Generic Operations with wkg oci
The wkg oci
subcommand enables pushing/pulling Wasm artifacts to/from any OCI registry. Unlike wkg publish
and wkg get
, providing the WIT package is not required.
To push a component to an OCI registry, use wkg oci pull
. The example below pushes a component to a GitHub Container Registry.
wkg oci push ghcr.io/user/component:0.1.0 component.wasm
To pull a component, run:
wkg oci pull ghcr.io/user/component:0.1.0 -o component.wasm
Fetching WIT Package Dependencies using wkg
Sometimes fetching a single package is not sufficient because it depends on other packages. For example, the following world describes a simple Wasm service which requires wasi:http/proxy
:
package foo:wasi-http-service;
world target-world {
include wasi:http/proxy@0.2.3;
}
You may be tempted to simply get the wasi:http
package with wkg get --format wit wasi:http@0.2.3 -o wit/deps/http/
. However, wasi:http
depends on other WASI packages such as wasi:clocks
and wasi:io
. To make sure to fetch a package and all its dependencies, use wkg wit fetch
, which will read the package containing the world(s) you have defined in the given wit directory (wit
by default). It will then fetch the
dependencies and write them to the deps
directory along with a lock file.
After placing the above file in ./wit
, run the following to fetch the dependencies:
wkg wit fetch
The ./wit
directory will be populated as follows:
wit
├── deps
│ ├── wasi-cli-0.2.3
│ │ └── package.wit
│ ├── wasi-clocks-0.2.3
│ │ └── package.wit
│ ├── wasi-http-0.2.3
│ │ └── package.wit
│ ├── wasi-io-0.2.3
│ │ └── package.wit
│ └── wasi-random-0.2.3
│ └── package.wit
└── world.wit
Now, you can use the language toolchain of your choice to generate bindings and create your component.
Tutorial
If you like to learn by doing, this tutorial will walk through how to build, compose, and run components through a calculator example. Calculators can conduct many operations: add, subtract, multiply, and so on.
In this example, each operation will be a component, that will be composed with
an eval-expression
component that will evaluate the expression using the expected operator. With
one operation per component, this calculator is exaggeratedly granular to show how independent logic
of an application can be contained in a component.
In production, components will likely have a larger scope than a simple mathematical operation.
Our eventual solution will involve three components:
- A calculator engine,
- An addition operation
- A command-line interface.
Once we have built these as separate Wasm components, we will compose them into a single runnable
component, and test it using the wasmtime
CLI.
The calculator interface
For tutorial purposes, we are going to put our "calculator engine" and "addition operation" interfaces into two separate WIT packages, each containing one WIT file.
This setup may seem excessive, but it illustrates a real-world use case where components come from different authors and packages.
These files can be found in the component book repository in the examples/tutorial/wit
directory under wit/adder/world.wit
and wit/calculator/world.wit
:
// wit/adder/world.wit
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
// wit/calculator/world.wit
package docs:calculator@0.1.0;
interface calculate {
enum op {
add,
}
eval-expression: func(op: op, x: u32, y: u32) -> u32;
}
world calculator {
export calculate;
import docs:adder/add@0.1.0;
}
world app {
import calculate;
}
These files define:
- A world
adder
that exports theadd
interface. Again, components such as the calculator can call it when they need to add numbers. - A world
calculator
describing the calculator component. This world exports the calculator interface, meaning that other components can call it to perform calculations. It imports the operation interfaces (such asadd
), meaning it relies on other components to perform those operations. - An interface
calculate
that contains an evaluate function and an enum that delineates the operations that can be involved in a calculation. In this tutorial, the only operation isadd
. - A world
app
describing the "primary" app component, which imports thecalculate
interface. This component will take in command line arguments and pass them to theeval-expression
function of the calculator component.
Create an add
component
Reference the language guide to create a component that implements the
adder
world of adder/wit/world.wit
.
For reference, see the completed example.
Create a calculator
component
Reference the language guide to create a component that implements the
calculator
world of wit/calculator/world.wit
.
For reference, see the completed example.
Once complete, the component should import the add
function from the adder
world and call it if the op
enum matches add
.
Create a command
component
A command is a component with a specific export that allows it to be executed directly by
wasmtime
(or other wasi:cli
hosts).
The WebAssembly host expects it to export the wasi:cli/run
interface, which is the equivalent
of the main
function to WASI.
cargo-component
will automatically resolve a Rust bin
package
with a main
function to a component with wasi:cli/run
exported. Scaffold a new Wasm application
with a command
component:
cargo component new command --command
This component will implement the app
world, which
imports the calculate
interface.
In Cargo.toml
, point cargo-component
to the WIT file and specify that it should pull in bindings
for the app
world from the path to calculator.wit
:
[package.metadata.component.target]
path = "../wit/calculator/world.wit"
world = "app"
Since the calculator world imports the add
interface, the command component needs to pull in the adder
WIT as a dependency, as well.
[package.metadata.component.target.dependencies]
"docs:adder" = { path = "../wit/adder" }
Now, implement a command line application that:
- Takes in three arguments: two operands and the name of an operator ("1 2 add")
- Parses the operator name and ensures it is supported in the
op
enum - Calls the
calculate
interface'seval_expression
, passing in the arguments.
For reference, see a completed example.
Composing the calculator
Now, we are ready to bring our components together into one runnable calculator component, using
wac
.
We will:
- Compose the calculator component with the add component to satisfy the calculator component's
adder
import - Compose that resolved calculator component once more with the command component to satisfy the command component's
calculate
import.
The result is a fully-formed command component that has all its imports satisfied and has a single
export (the wasi:cli/run
interface), which can be executed by wasmtime
.
wac plug calculator.wasm --plug adder.wasm -o composed.wasm
wac plug command.wasm --plug composed.wasm -o final.wasm
If you'd prefer to take a more visual approach to composing components, see the documentation on composing components with wasmbuilder.app.
Running the calculator
Now it all adds up! Run the final component with the wasmtime
CLI, ensuring you are using a
recent release (v14.0.0
or greater), as earlier releases of
the wasmtime
CLI do not include component model support.
wasmtime run final.wasm 1 2 add
1 + 2 = 3
To infinity and beyond!
To expand the exercise to add more components, modify calculator.wit
to add another operator world
and expand the op
enum. Then, modify the command
and calculator
components to support the
expanded enum.
Another extension of this tutorial could be to remove the op
enum and instead modify
eval-expression
to take in a string that can then be parsed to determine which operator component
to call. Maybe this parser is a component of its own?!
Canonical ABI
An ABI is an application binary interface - an agreement on how to pass data around in a binary format. ABIs are specifically concerned with data layout at the bits-and-bytes level. For example, an ABI might define how integers are represented (big-endian or little-endian?), how strings are represented (pointer to null-terminated character sequence or length-prefixed? UTF-8 or UTF-16 encoded?), and how composite types are represented (the offsets of each field from the start of the structure).
The component model defines a canonical ABI - an ABI to which all components adhere. This guarantees that components can talk to each other without confusion, even if they are built in different languages. Internally, a C component might represent strings in a quite different way from a Rust component, but the canonical ABI provides a format for them to pass strings across the boundary between them.
For a more formal definition of what the Canonical ABI is, take a look at the Canonical ABI explainer.
Frequently Asked Questions (FAQ)
This page hosts a series of questions that are frequently asked along with descriptions of concepts that may be confusing with regards to core WebAssembly, WebAssembly components (i.e. the Component Model), and the WebAssembly ecosystem as a whole.
Q: What is the difference between a module and component in WebAssembly?
A WebAssembly module (more precisely referred to as a "WebAssembly core module") is a binary that conforms to the WebAssembly Core Specification.
A WebAssembly component:
- Adheres to the component model binary format (as opposed to a WebAssembly core binary format)
- Uses the WebAssembly Interface types specification to encode type information.
- Adheres to the Component Model Canonical ABI for converting between rich types and those present in core WebAssembly.
WebAssembly Components can (and often do) contain core modules, but generally WebAssembly core modules cannot contain Components. WebAssembly components and WebAssembly core modules have a different binary format.
WebAssembly components can be expressed via both a binary and textual format ("WAT", the WebAssembly Text format).
Q: How can I tell if a WebAssembly binary is a component or a module?
After converting a WebAssembly binary to its textual format (e.g. via a tool like wasm-tools print
),
it is easy to tell a WebAssembly core module and a WebAssembly component apart.
A WebAssembly core module generally consists of a top level (module)
s-expression:
(module
;; ...
)
A WebAssembly component generally consists of a (component)
s-expression (and may contain
nested (core:module)
/(component)
s-expressions):
(component
;; ...
)
Q: How do WebAssembly Components and the WebAssembly System Interface (WASI) relate to each other?
While WebAssembly core module can represent higher level types using the available primitives, every binary and platform may do so in an ad-hoc manner. The Component Model presents a consistent way of representing a rich set of types familiar in most high level languages that is consistent across binaries and platforms.
The set of rich types which can be used by WebAssembly components are called WebAssembly Interface Types (WIT).
The WebAssembly System Interface (WASI) is a set of APIs (specified in WIT) developed for eventual standardization by the WASI Subgroup, which is a subgroup of the WebAssembly Community Group. WASI defines interfaces, functions and types that a system or platform can expose to a WebAssembly component. At a glance, many parts of WASI are UNIX-like, in that they match traditional expectations for programs like STDIN, STDOUT, and writing to files.
Some WASI system interfaces work at a much higher level than the command line however, like
wasi:http
. wasi:http
is included as a standardized platform due to the ubiquity
of the internet and the common use case of WebAssembly components with "the web" as a platform.
With WIT, platform builders can define any interface that WebAssembly components expect to access -- WASI is a standardized set which enables to build on a shared base set of abstractions.
Q: I see the terms Preview 1 and Preview 2 frequently. What do those refer to?
Preview 1 refers to the first iteration of the Component Model which was based on WITX and is now deprecated:
https://github.com/WebAssembly/WASI/tree/main/legacy
Preview 2 refers to a newer iteration of the Component Model which uses WebAssembly Interface Types (WIT):
https://github.com/WebAssembly/WASI/tree/main/wasip2
Many programming language toolchains may only support Preview 1 components natively, but this isn't a problem in practice as Preview 1 components can be adapted into Preview 2 components automatically.
While somewhat confusing a WASI Preview 1 "component" is in fact a WebAssembly core module. More precisely, a Preview 1 "component" is a WebAssembly core module with a well-defined set of imports and exports (legacy specification).
Q: What are component imports?
WebAssembly components are self-describing -- information about required external functionality (which must be provided by the platform or another component) is included in the binary.
For example, a WebAssembly component that may require some use of outside environment variables may import a WASI interface like wasi:cli/environment
.
note
The values provided by the wasi:cli/environment
are not guaranteed
to be ENV variables on the host machine -- this is a choice left to the
platform, in the implementation of wasi:cli/environment
that it exposes.
For example, platforms may choose to elide sensitive environment variables, or provide none at all, in practice.
Imports are easiest illustrated with WIT:
package example-namespace:example-package;
world example-world {
import wasi:cli/environment@0.2.4;
}
The environment
interface in wasi:cli
provides various types and functions for interacting with
environment variables.
The component is said to "import" the wasi:cli/environment
interface, using the available functions and types therein.
Q: What are component exports?
WebAssembly components are self-describing -- along with imports, WebAssembly components can also describe what functionality they export, which callers of the component (e.g. another component, a WebAssembly host) can reference.
Exports are easiest illustrated with WIT:
package example-namespace:example-package;
interface example-interface {
say-hello: func(name: string) -> string;
}
world example-world {
export example-interface;
}
For the component that inhabits the example-world
defined above, callers can expect the WebAssembly binary to
have a say-hello
function that is callable via the example-namespace:example-package/example-interface
interface.
The component is said to "export" the example-interface
interface, making available the functions and types therein.
Still have questions?
Please contribute to the Component Book by filing your question (or one that you think should be covered here) as an issue on GitHub.
Useful links
The following references are helpful in understanding the Component Model and related ecosystem/projects.