Home
The WebAssembly Component Model is a broad-reaching architecture for building interoperable Wasm libraries, applications, and environments.
Understanding components | Building components | Using components |
---|---|---|
Why Components? | C/C++ | Composing |
Components | C# | Running |
Interfaces | Go | Distributing |
Worlds | JavaScript | |
Python | ||
Rust |
This documentation is aimed at users of the component model: developers of libraries and applications. 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.
Status
WASI 0.2.0 was released Jan 25, 2024, providing a stable release of WASI and the component model. This 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 the stable 0.2.0 release.
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?
If you've tried out WebAssembly, you'll be familiar with the concept of a module. Roughly speaking, a module corresponds to a single .wasm
file, with functions, memory, imports and exports, and so on. These "core" modules can run in the browser, or via a separate runtime such as Wasmtime or WAMR. A module is defined by the WebAssembly Core Specification, and if you compile a program written in Rust, C, Go or whatever to WebAssembly, then a core module is what you'll get.
Core modules are, however, limited in how they expose their functionality to the outside world to functions that take and return only a small number of core WebAssembly types (essentially only integers and floating-point numbers). Richer types, such as strings, lists, records (a.k.a. structs), etc. have to be represented in terms of integers and floating point numbers, for example by the use of pointers and offsets. Those representations are often times not interchangeable across languages. For example, a string in C might be represented entirely differently from a string in Rust or in JavaScript.
For Wasm modules to interoperate, therefore, there needs to be an agreed-upon way for exposing those richer types across module boundaries.
In the component model, these type definitions are written in a language called WIT (Wasm Interface Type), and the way they translate into bits and bytes is called the Canonical ABI (Application Binary Interface). A Wasm component is thus a wrapper around a core module that specifies its imports and exports using such Interfaces.
The agreement of an interface adds a new dimension to Wasm portability. Not only are components portable across architectures and operating systems, but they are now portable across languages. A Go component can communicate directly and safely with a C or Rust component. It need not even know which language another component was written in - it needs only the component interface, expressed in WIT. Additionally, components can be linked into larger graphs, with one component's exports satisfying another's imports.
Combined with Wasm's strong sandboxing, this opens the door to yet further benefits. By expressing higher-level semantics than integers and floats, it becomes possible to statically analyse and reason about a component's behaviour - to enforce and guarantee properties just by looking at the surface of the component. The relationships within a graph of components can be 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 Wasm 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 Wasm GC (garbage collected) memory to collaborate with one that uses conventional linear memory.
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!
For more background on why the component model was created, take a look at the specification's goals, use cases and design choices.
Components
- Logically, components are containers for modules - or other components - which express their interfaces and dependencies via WIT.
- Conceptually, components are self-describing units of code that interact only through interfaces instead of shared memory.
- Physically, a component is a specially-formatted WebAssembly file. Internally, the component could include multiple traditional ("core") WebAssembly modules, and sub-components, composed via their imports and exports.
The external interface of a component - its imports and exports - corresponds to a world. The component, however, internally defines how that world is implemented.
For a more formal definition of what a component is, take a look at the Component Model specification.
Interfaces
An interface describes a single-focus, composable contract, through which components can interact with each other and with hosts. Interfaces describe the types and functions used to carry out that interaction. For example:
- A "receive HTTP requests" interface might have only a single "handle request" function, but contain types representing incoming requests, outgoing responses, HTTP methods and headers, and so on.
- A "wall clock" interface might have two functions, one to get the current time and one to get the granularity of the timer. It would also include a type to represent an instant in time.
Interfaces are defined using the WIT language.
For a more formal definition of what an interface is, take a look at the WIT specification.
WIT Worlds
A WIT world is a higher-level contract that describes a component's capabilities and needs.
On one hand, a world describes the shape of a component - it says which interfaces the component exposes for other code to call (its exports), and which interfaces the component depends on (its imports). A world only defines the surface of a component, not the internal behaviour. If you're an application or library developer creating a component, you'll specify the world your component targets. This world describes the functionality your component exposes and declares the functionality your component depends on in order to be able to run. 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.
On the other hand though, a world defines a hosting environment for components (i.e., an environment in which a component can be instantiated and its functionality can be invoked). An environment supports a world by providing implementations for all of the imports and by optionally invoking one or more of the exports.
For example, WASI (the WebAssembly System Interface) defines a "command line" world which imports interfaces that command line programs typically expect to have available to them such as file I/O, random number generation, clocks and so on. 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. On the other hand, environments supporting this world must provide implementations for all of the imports and may invoke the single export.
A world is composed of interfaces, but each interface is directional - it indicates whether the interface is available for outside code to call (an "export"), or whether outside code must fulfill the interface for the component to call (an "import"). These interfaces strictly bound the component. A component cannot interact with anything outside itself except by having its exports called, or by it calling its imports. This 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.
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, for example, on-disk caching.
- 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 is 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.
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 coding language and doesn't define behaviour; it defines only contracts between components. 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
WIT identifiers have a slightly different set of rules from what you might be familiar with from, say, C, Rust, or Java. These rules apply to all names - types, functions, interfaces, and worlds. (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, allowing 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). This is typically used for "value or error" situations; 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 it so that there is a common way of expressing it, so that you don't need to create a variant type 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
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 their order 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
You can define your own types within an interface
or world
. WIT offers several ways of defining new types.
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 declares one or more cases. Each case has a name and, optionally, a type of data associated with that case. A variant instance contains exactly one case. Cases are separated by commas. The syntax is as follows:
variant allowed-destinations {
none,
any,
restricted(list<address>),
}
Variants are similar to Rust enum
s or OCaml discriminated unions. The closest C equivalent is a tagged union, but WIT both takes care of the "tag" (the case) and enforces 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++ enum
.
Resources
Resources are handles to some entity that lives outside of the component. They describe things that can't or shouldn't be copied by value; instead, their ownership or reference can be passed between two components via a handle. 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.
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
(often calledthis
in many languages) parameter that is a handle - static functions: functions which do not have an implicit
self
parameter but are meant to be nested in the scope of the resource type - at most one constructor: a function that is syntactic sugar for a function returning a handle of the containing resource type
Methods always desugar to a borrowed self
parameter whereas constructors
always desugar to an owned return value. 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.
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. In an instance of the type, each flag will be either true
or false
.
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 named type using type ... = ...
. This can be 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. Like in record fields, the name is separated from the type by a colon:
do-nothing: func();
The function type is the word 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>;
A function can have multiple return values. In this case the return values must be named, similar to the parameter list. All return values must be populated (in the same way as tuple or record fields).
get-customers-paged: func(cont: continuation-token) -> (customers: list<customer>, cont: continuation-token);
A function can be declared as part of 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 items 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);
}
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
A world describes a set of imports and exports, enclosed in braces and introduced with the world
keyword. Roughly, a world describes the contract of a component. Exports are provided by the component, and define what consumers of the component may call; imports are things the component may call. The imports and exports may be interfaces or individual functions.
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;
}
Interfaces from other packages
You can import and export interfaces defined in other packages. This can be done using 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. Packages group definitions; they don't represent behaviour.
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. This must include a namespace and name, separated by a colon, and may optionally include a semver-compliant version:
package documentation:example;
package documentation:example@1.0.1;
If a package spans multiple files, only one file needs to contain a package declaration (but if multiple files contain declarations then they must all be the same). All files must have the .wit
extension and must be in the same directory. 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;
}
For a more formal definition of the WIT language, take a look at the WIT specification.
WIT Packages
A WIT package is a set of one or more WIT (Wasm Interface Type) files containing a related set of interfaces and worlds. WIT is an IDL (interface definition language) 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 is not a world. It's a way of grouping related interfaces and worlds together for ease of discovery and reference, more like a namespace.
- The WebAssembly System Interface (WASI) defines a number of packages, including one named
wasi:clocks
. Our HTTP proxy world could import 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.
Wasm Language Support
WebAssembly can be targeted by the majority of top programming languages; however, the level of support varies. This document details the subset of languages that target WASI and support components.
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 example
world defined in
examples/example-host/add.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:
- Wasm Language Support
Language Agnostic Tooling
Building a Component with wasm-tools
wasm-tools
provides a suite of subcommands for
working with WebAssembly modules and components.
wasm-tools
can be used to create a component from WebAssembly Text (WAT). This walks through creating a component from WAT that implements the example
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 followingexample
world:package example:component; world example { export add: func(x: s32, y: s32) -> s32; }
-
Define an
add
core module in WAT that exports anadd
function that adds two parameters:(module (func $add (param $lhs i32) (param $rhs i32) (result i32) local.get $lhs local.get $rhs i32.add) (export "add" (func $add)) )
-
Use
wasm-tools
to create a component from the core module, first embedding component metadata inside the core module and then encoding the WAT to a Wasm binary.$ wasm-tools component embed add.wit add.wat -o add.wasm $ wasm-tools component new add.wasm -o add.component.wasm
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. The wasmtime
CLI can only run "command" components, so in
order to run the add
function above, it first must be composed with a primary "command" component
that calls it. See documentation on running components for
more details.
C/C++ Tooling
Building a Component with wit-bindgen
and wasm-tools
wit-bindgen
is a tool to generate guest language bindings from a given .wit
file. Although it is less integrated into language toolchains than other tools such as cargo-component
, it can currently generate source-level bindings for Rust
, C
, Java (TeaVM)
, and TinyGo
, with the ability for more language generators to be added in the future.
wit-bindgen
can be used to generate C applications that can be compiled directly to Wasm modules using clang
with a wasm32-wasi
target.
First, install the CLI for wit-bindgen
, wasm-tools
, and the WASI SDK
.
The WASI SDK will install a local version of clang
configured with a wasi-sysroot. Follow these instructions to configure it for use. Note that 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 (more information is available in WASI SDK's docs).
Start by generating a C skeleton from wit-bindgen
using the sample add.wit
file:
>wit-bindgen c add.wit
Generating "example.c"
Generating "example.h"
Generating "example_component_type.o"
This has generated several files - an example.h
(based on the name of your world
) with the prototype of the add
function - int32_t example_add(int32_t x, int32_t y);
, as well as some generated code in example.c
that interfaces with the component model ABI to call your function. Additionally, example_component_type.o
contains object code referenced in example.c
from an extern
that must be linked via clang.
Next, create an add.c
that implements your function defined in example.h
:
#include "example.h"
int32_t example_add(int32_t x, int32_t y)
{
return x + y;
}
Now, you can compile the function into a Wasm module via clang:
clang add.c example.c example_component_type.o -o add-core.wasm -mexec-model=reactor
Use the
clang
included in the WASI SDK installation, for example at<WASI_SDK_PATH>/bin/clang
.
Next, you need to transform the module into a component. For this example, you can use wasm-tools component new
:
wasm-tools component new ./add-core.wasm -o add-component.wasm
Do note this will fail if your code references any WASI APIs that must be imported. This requires an additional step as the WASI SDK still references wasi_snapshot_preview1
APIs that are not compatible directly with components.
For example, modifying the above to reference printf()
would compile:
#include "example.h"
#include <stdio.h>
int32_t example_add(int32_t x, int32_t y)
{
int32_t result = x + y;
printf("%d", result);
return result;
}
However, the module would fail to transform to a component:
>wasm-tools component new ./add-core.wasm -o add-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: module requires an import interface named `wasi_snapshot_preview1`
Install the appropriate reactor adapter module as documented here - 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 add-core.wasm --adapt wasi_snapshot_preview1.wasm -o add-component.wasm
Finally, you can inspect the embedded wit to see your component (including any WASI imports if necessary):
>wasm-tools component wit add-component.wasm
package root:component;
world root {
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;
import wasi:cli/terminal-input@0.2.0;
import wasi:cli/terminal-output@0.2.0;
import wasi:cli/terminal-stdin@0.2.0;
import wasi:cli/terminal-stdout@0.2.0;
import wasi:cli/terminal-stderr@0.2.0;
import wasi:clocks/wall-clock@0.2.0;
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
export add: func(x: s32, y: s32) -> s32;
}
Running a Component from C/C++ Applications
It is not yet possible to run a Component using the wasmtime
c-api
- see this issue. The c-api is preferred to trying to directly use the 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
.
C# Tooling
Building a Component with componentize-dotnet
componentize-dotnet makes it easy to compile your code to WebAssembly components using a single tool. This Bytecode Alliance project is a NuGet package that can be used to create a fully AOT-compiled component, giving .NET developers a component experience comparable to those in Rust and TinyGo.
componentize-dotnet serves as a one-stop shop for .NET developers, 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)
First, install the .NET SDK. For this walkthrough, we’ll use the .NET 9 SDK RC 1. You should also have wasmtime installed so you can run the binary that you produce.
Once you have the .NET SDK installed, create a new project:
dotnet new classlib -o adder
cd adder
The componentize-dotnet
package depends on the NativeAOT-LLVM
package, which resides at the
dotnet-experimental package source, so you will need to make sure that NuGet is configured to refer
to experimental packages. You can create a project-scoped NuGet configuration by running:
dotnet new nugetconfig
Edit your nuget.config file to look like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Now back in the console we’ll add the BytecodeAlliance.Componentize.DotNet.Wasm.SDK
package:
dotnet add package BytecodeAlliance.Componentize.DotNet.Wasm.SDK --prerelease
In the .csproj project file, add the following to the <PropertyGroup>
:
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<UseAppHost>false</UseAppHost>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
<SelfContained>true</SelfContained>
Next, create or download the WIT world you would like to target. For this example we will use an
example
world
with an add
function:
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
In the .csproj project file, add a new <ItemGroup>
:
<ItemGroup>
<Wit Update="add.wit" World="example" />
</ItemGroup>
If you try to build the project with dotnet build
, you'll get an error like "The name
'ExampleWorldImpl' does not exist in the current context". This is because you've said you'll
provide an implementation, but haven't yet done so. To fix this, add the following code to your
project:
namespace ExampleWorld;
public class ExampleWorldImpl : IOperations
{
public static int Add(int x, int y)
{
return x + y;
}
}
If we build it:
dotnet build
The component will be available at bin/Debug/net9.0/wasi-wasm/native/adder.wasm
.
Building a component that exports an interface
The previous example uses a WIT file that exports a function. However, to use your component from
another component, it must export an interface. That being said, you rarely find WIT that does not
contain an interface. (Most WITs you'll see in the wild do use interfaces; we've been simplifying by
exporting a function.) Let's expand our example
world to export an interface rather than directly
export the function. We are also adding the hostapp
world to our WIT file which we will implement
in the next section to demonstrate how to build a
component that imports an interface.
// add.wit
package example:component;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
If you peek at the bindings, you'll notice that we now implement a class for the add
interface
rather than for the example
world. This is a consistent pattern. As you export more interfaces
from your world, you implement more classes. Our add example gets the slight update of:
namespace ExampleWorld.wit.exports.example.component;
public class AddImpl : IAdd
{
public static int Add(int x, int y)
{
return x + y;
}
}
Once again, compile an application to a Wasm component using dotnet build
:
$ dotnet build
Restore complete (0.4s)
You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy
adder succeeded (1.1s) → bin/Debug/net9.0/wasi-wasm/adder.dll
Build succeeded in 2.5s
The component will be available at bin/Debug/net9.0/wasi-wasm/native/adder.wasm
.
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 console
creates a new project that creates an executable.
dotnet new console -o host-app
cd host-app
The componentize-dotnet
package depends on the NativeAOT-LLVM
package, which resides at the
dotnet-experimental package source, so you will need to make sure that NuGet is configured to refer
to experimental packages. You can create a project-scoped NuGet configuration by running:
dotnet new nugetconfig
Edit your nuget.config file to look like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Now back in the console we’ll add the BytecodeAlliance.Componentize.DotNet.Wasm.SDK
package:
dotnet add package BytecodeAlliance.Componentize.DotNet.Wasm.SDK --prerelease
In the .csproj project file, add the following to the <PropertyGroup>
:
<RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
<UseAppHost>false</UseAppHost>
<PublishTrimmed>true</PublishTrimmed>
<InvariantGlobalization>true</InvariantGlobalization>
<SelfContained>true</SelfContained>
Copy the same WIT file as before into your project:
// add.wit
package example:component;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world example {
export add;
}
world hostapp {
import add;
}
Add it to your .csproj project file as a new ItemGroup
:
<ItemGroup>
<Wit Update="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.example.component;
var left = 1;
var 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/net9.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 the first component. You can compose your host-app
component with
your adder
component by running wac plug
:
wac plug bin/Debug/net9.0/wasi-wasm/native/host-app.wasm --plug ../adder/bin/Debug/net9.0/wasi-wasm/native/adder.wasm -o main.wasm
Then you can run the composed component:
wasmtime run main.wasm
1 + 2 = 3
Go Tooling
The TinyGo toolchain has native support for WASI
and can build Wasm core modules. With the help of some component model tooling, we can then take
that core module and embed it in a component. To demonstrate how to use the tooling, this guide
walks through building a component that implements the example
world defined in the add.wit
package. The component will implement a simple add function.
Overview of Building a Component with TinyGo
There are several steps to building a component in TinyGo:
- Determine which world the component will implement
- Build a Wasm core module using the native TinyGo toolchain
- Convert the Wasm core module to a component using
wasm-tools
The following sections will walk through these steps, producing a core Wasm module that targets WASI preview 1 and converting this core module to a component that supports WASI preview 2.
1: The example
World
The next two sections walk through creating a component that implements the the following example
world:
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
This is a simple world that exports one add
function. If you want to go beyond a quick start to a
more realistic example, jump to the section on implementing worlds with
interfaces.
2: Creating a TinyGo Core Wasm Module
The TinyGo toolchain natively supports compiling Go programs to core Wasm modules. Let's create one that implements the add
function in the example
world.
First, implement a simple add function in add.go
:
package main
//go:wasm-module yourmodulename
//export add
func add(x, y int32) int32 {
return x + y
}
// main is required for the `wasi` target, even if it isn't used.
func main() {}
Note, we must still provide a main
function. This is a limitation of TinyGo's support of WASI as it currently only supports main
packages - commands that run start-to-finish and
then exit. Our example program, however, is more like a library which exports an add function that
can be called multiple times; and nothing will ever call its main
function.
Now, we can use TinyGo to build our core Wasm module:
tinygo build -o add.wasm -target=wasi add.go
You should now have an add.wasm
module. But at the moment, this is a core module. In the next section, we will convert it into a component.
3: Converting a Wasm Core Module to a Component
In the previous step, we produced a core module that implements our example
world. We now want to
convert to a component to gain the benefits of the component model, such as the ability to compose
with it with other components as done in the calculator
component in the
tutorial.
TinyGo is actively developing a wasip2
target (in this PR), but for now we must take additional steps to convert the module to a component.
We will use
wasm-tools
, a low level tool for manipulating
Wasm modules. Download the
latest release from the project's
repository.
We also need to download the WASI preview 1 adapter. TinyGo (similar to C) targets preview 1 of WASI
which does not support the component model (.wit
files). Fortunately, Wasmtime provides
adapters for adapting
preview 1 modules to preview 2 components. There are adapters for both reactor and command
components. Our add.wit
world defines a reactor component, so download the wasi_snapshot_preview1.reactor.wasm
adapter
from the latest Wasmtime release.
Now that we have all the prerequisites downloaded, we can use the wasm-tools component
subcommand
to componentize our Wasm module, first embedding component metadata inside the core module and then
encoding the module as a component using the WASI preview 1 adapter.
export COMPONENT_ADAPTER_REACTOR=/path/to/wasi_snapshot_preview1.reactor.wasm
wasm-tools component embed --world example ./add.wit add.wasm -o add.embed.wasm
wasm-tools component new -o add.component.wasm --adapt wasi_snapshot_preview1="$COMPONENT_ADAPTER_REACTOR" add.embed.wasm
We now have an add component that satisfies our example
world, exporting the add
function, which
we can confirm using another wasm-tools
command:
$ wasm-tools component wit add.component.wasm
package root:component
world root {
import wasi:io/streams
import wasi:filesystem/types
import wasi:filesystem/preopens
import wasi:cli/stdin
import wasi:cli/stdout
import wasi:cli/stderr
import wasi:cli/terminal-input
import wasi:cli/terminal-output
import wasi:cli/terminal-stdin
import wasi:cli/terminal-stdout
import wasi:cli/terminal-stderr
export add: func(x: s32, y: s32) -> s32
}
Testing an 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
to do
just that. It calls the add
function of a passed in component providing two operands. To use it,
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.component.wasm
Implementing Worlds with Interfaces with TinyGo and Wit-Bindgen
The example
world we were using in the previous sections simply exports a function. However, to use your component from another component, it must export an interface. This means we will need to use a tool to generate bindings to use as glue code, and adds a couple more steps (2-3) to building Wasm components with TinyGo:
- Determine which world the component will implement
- Generate bindings for that world using
wit-bindgen
- Implement the interface defined in the bindings
- Build a Wasm core module using the native TinyGo toolchain
- Convert the Wasm core module to a component using
wasm-tools
For this example, we will use the following world, which moves the add function behind an add
interface:
package docs:adder@0.1.0;
interface add {
add: func(a: u32, b: u32) -> u32;
}
world adder {
export add;
}
Our new steps use a low-level tool, wit-bindgen
to generate bindings, or wrapper code, for implementing the desired world.
First, install a release of wit-bindgen
, updating the environment variables for your desired version, architecture and OS:
export VERSION=0.26.0 ARCH=aarch64 OS=macos
wget https://github.com/bytecodealliance/wit-bindgen/releases/download/v$VERSION/wit-bindgen-$VERSION-$ARCH-$OS.tar.gz
tar -xzf wit-bindgen-$VERSION-$ARCH-$OS.tar.gz
mv wit-bindgen-$VERSION-$ARCH-$OS/wit-bindgen ./
rm -rf wit-bindgen-$VERSION-$ARCH-$OS.tar.gz wit-bindgen-$VERSION-$ARCH-$OS
Now, create your Go project:
mkdir add && cd add
go mod init example.com
Next, run wit-bindgen
, specifying TinyGo as the target language, the path to the
add.wit
package, the name of the world in that package to
generate bindings for (adder
), and a directory to output the generated code (gen
):
wit-bindgen tiny-go ./add.wit --world adder --out-dir=gen
The gen
directory now contains several files:
$ tree gen
gen
├── adder.c
├── adder.go
└── adder.h
The adder.go
file defines an ExportsDocsAdder0_1_0_Add
interface that matches the structure of our add
interface. The name of the interface is taken from the WIT package name (docs:adder@0.1.0
) combined with the interface name (add
). In our Go module, first implement the ExportsDocsAdder0_1_0_Add
interface by defining the Add
function.
package main
import (
. "example.com/gen"
)
type AdderImpl struct {
}
// Implement the `ExportsDocsAdder0_1_0_Add` interface to ensure the component satisfies the
// `adder` world
func (i AdderImpl) Add(x, y uint32) uint32 {
return x + y
}
// main is required for the `wasi` target, even if it isn't used.
func main() {}
After implementing the adder world, we need to load it by passing it to the SetExportsDocsAdder0_1_0_Add
function from our bindings (adder.go
). Since our component is a library, main
will not be called. However, only Go
programs with main
can target WASI currently. As a loophole, we will initialize our AdderImpl
type inside an init
function. 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 and
make it callable using the generated C bindings (adder.c
). After populating the init
function,
our complete implementation looks similar to the following:
package main
import (
. "example.com/gen"
)
type AdderImpl struct {
}
// Implement the ExportsDocsAdder0_1_0_Add interface to ensure the component satisfies the
// `adder` world
func (i AdderImpl) Add(x, y uint32) uint32 {
return x + y
}
// To enable our component to be a library, implement the component in the
// `init` function which is always called first when a Go package is run.
func init() {
example := AdderImpl{}
SetExportsDocsAdder0_1_0_Add(example)
}
// main is required for the `WASI` target, even if it isn't used.
func main() {}
Once again, we can build our core module using TinyGo, componentize it, and adapt it for WASI 0.2:
export COMPONENT_ADAPTER_REACTOR=/path/to/wasi_snapshot_preview1.reactor.wasm
tinygo build -o add.wasm -target=wasi add.go
wasm-tools component embed --world adder ./add.wit add.wasm -o add.embed.wasm
wasm-tools component new -o add.component.wasm --adapt wasi_snapshot_preview1="$COMPONENT_ADAPTER_REACTOR" add.embed.wasm
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.component.wasm
package root:component;
world root {
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;
import wasi:clocks/wall-clock@0.2.0;
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
export docs:adder/add@0.1.0;
}
JavaScript Tooling
jco
is a fully native JS tool for working with the
emerging WebAssembly Components specification in JavaScript.
Building a Component with jco
A component can be created from a JS module using jco componentize
. First, install jco
and componentize-js
:
$ npm install @bytecodealliance/jco
$ npm install @bytecodealliance/componentize-js
Next, create or download the WIT world you would like to target. For this example we will use an example
world with an add
function:
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
Create a JavaScript module that implements the add
function in the example
world.
export function add(x, y) {
return x + y;
}
Now, use jco
to create a component from the JS module:
$ jco componentize add.js --wit add.wit --world-name example --out add.wasm
OK Successfully written add.wasm with imports ().
Now, run the component using the Rust add
host:
$ cd component-model/examples/add-host
$ cargo run --release -- 1 2 ../path/to/add.wasm
1 + 2 = 3
Running a Component from JavaScript Applications
As the JavaScript runtime cannot yet execute Wasm components, a component must be transpiled into
JavaScript and a core module and then executed. jco
automates this transpilation:
$ jco transpile add.wasm -o out-dir
Transpiled JS Component Files:
- out-dir/add.core.wasm 6.72 MiB
- out-dir/add.d.ts 0.05 KiB
- out-dir/add.js 0.8 KiB
A core module and JavaScript bindings have been outputted to the out-dir
.
Now, you can import the resultant add.js
file and run it from a JavaScript application. This
example renames it and imports it as an ECMAScript module for ease of running locally with node:
// app.mjs
import { add } from "./out-dir/add.mjs";
console.log("1 + 2 = " + add(1, 2));
The above example :
$ mv out-dir/add.js out-dir/add.mjs
$ node app.mjs
1 + 2 = 3
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 example
world with an add
function:
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
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 /path/to/examples/example-host/add.wit --world example bindings .
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.
You can see that bindings were created in an example
package which contains an Example
protocol with an add
method that we can implement:
$ cat<<EOT >> app.py
import example
class Example(example.Example):
def add(self, x: int, y: int) -> int:
return x + y
EOT
We now can compile our application to a Wasm component using the componentize
subcommand:
$ componentize-py --wit-path /path/to/examples/example-host/add.wit --world example 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.
Building a Component that Exports an Interface
The sample add.wit
file exports a function. However, to use your component from another component, it must export an interface. That being said, you rarely find WIT that does not contain an interface. (Most WITs you'll see in the wild do use interfaces; we've been simplifying by exporting a function.) Let's expand our example world to export an interface rather than directly export the function.
// add-interface.wit
package example:component;
interface add {
add: func(a: u32, b: u32) -> u32;
}
world example {
export add;
}
If you peek at the bindings, you'll notice that we now implement a class for the add
interface rather than for the example
world. This is a consistent pattern. As you export more interfaces from your world, you implement more classes. Our add example gets the slight update of:
# app.py
import example
class Add(example.Example):
def add(self, a: int, b: int) -> int:
return a + b
Once again, compile an application to a Wasm component using the componentize
subcommand:
$ componentize-py --wit-path add-interface.wit --world example componentize app -o add.wasm
Component built successfully
Running components from Python Applications
Wasm components can also be invoked from Python applications. This walks through using tooling
to call the app.wasm
component from the examples.
wasmtime-py
does not currently support running components build withcomponentize-py
. This is becausewasmtime-py
does not yet support resources, which components built withcomponentize-py
always use, sincecomponentize-py
unconditionally imports most of thewasi:cli
world.
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. It is a cargo
subcommand for
creating WebAssembly components using Rust as the component's implementation language.
Installing cargo component
To install cargo component
, run:
cargo install cargo-component
You can find more details about
cargo component
in its crates.io page.
Building a Component with cargo component
Create a Rust library that implements the add
function in the example
world. First scaffold a project:
$ cargo component new add --lib && cd add
Note that cargo component
generates the necessary bindings as a module called bindings
.
Next, update wit/world.wit
to match add.wit
and modify the component package reference to change the
package name to example
. The component
section of Cargo.toml
should look like the following:
[package.metadata.component]
package = "component:example"
cargo component
will generate bindings for the world specified in a package's Cargo.toml
. In particular, it will create a Guest
trait that a component should implement. Since our example
world has no interfaces, the trait lives directly under the bindings module. Implement the Guest
trait in add/src/lib.rs
such that it satisfied the example
world, adding an add
function. It should look similar to the following:
mod bindings;
use bindings::Guest;
struct Component;
impl Guest for Component {
fn add(x: i32, y: i32) -> i32 {
x + y
}
}
Now, use cargo component
to build the component, being sure to optimize with a release build.
$ cargo component build --release
You can use wasm-tools component wit
to output the WIT package of the component:
$ wasm-tools component wit target/wasm32-wasip1/release/add.wasm
package root:component;
world root {
export add: func(x: s32, y: s32) -> s32;
}
Running a Component from Rust Applications
To verify that our component works, lets run it from a Rust application that knows how to run a
component targeting the example
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/add.wasm
1 + 2 = 3
Exporting an interface with cargo component
The sample add.wit
file exports a function. However, to use your component from another component, it must export an interface. This results in slightly fiddlier bindings. For example, to implement the following world:
package docs:adder@0.1.0;
interface add {
add: func(a: u32, b: u32) -> u32;
}
world adder {
export add;
}
you would write the following Rust code:
#![allow(unused)] fn main() { mod bindings; // Separating out the interface puts it in a sub-module use bindings::exports::docs::adder::add::Guest; struct Component; impl Guest for Component { fn add(a: u32, b: u32) -> u32 { a + b } } }
Importing an interface with cargo component
The world file (wit/world.wit
) generated for you by cargo component new --lib
doesn't specify any imports.
cargo component build
, by default, uses the Rustwasm32-wasi
target, and therefore automatically imports any required WASI interfaces - no action is needed from you to import these. This section is about importing custom WIT interfaces from library components.
If your component consumes other components, you can edit the world.wit
file to import their interfaces.
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 that the path is to the adder project's WIT directory, not to the world.wit
file. A WIT package may be spread across multiple files in the same directory; cargo component
will look at all the files.
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 exports-add.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 wasm: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 thewasm: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. } }
Creating and Consuming 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.
Authoring Components
You can write WebAssembly core modules in a wide variety of languages, and the set of languages that can directly create components is growing. See the Language Support section for information on building components directly from source code.
If your preferred language supports WebAssembly but not components, you can still create components using the wasm-tools component
tool. (A future version of this page will cover this in more detail.)
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 https://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.
Running Components
You can "run" a component by calling one of its exports. In some cases, this requires a custom host. For "command" components, though, you can use the wasmtime
command line. This can be a convenient tool for testing components and exploring the component model. Other runtimes are also available - see the "Runtimes" section of the sidebar for more info.
A "command" component is one that exports the
wasi:cli/run
interface, and imports only interfaces listed in thewasi:cli/command
world.
You must use a recent version of wasmtime
(v14.0.0
or greater), as earlier releases of the wasmtime
command line do not include component model support.
To run your component, run:
wasmtime run <path-to-wasm-file>
Running components with custom exports
If you're writing a library-style component - that is, one that exports a custom API - then you can run it in wasmtime
by writing a "command" component that imports and invokes your custom API. By composing the command and the library, you can exercise the library in wasmtime
.
-
Write your library component. The component's world (
.wit
file) must export an interface. (Do not export functions directly, only interfaces.) See the language support guide for how to implement an export. -
Build your library component to a
.wasm
file. -
Write your command component. The component's world (
.wit
file) must import the interface exported from the library. Write the command to call the library's API. See the language support guide for how to call an imported interface. -
Build your command component to a
.wasm
file. You will not be able to run this inwasmtime
yet, as its imports are not yet satisfied. -
Compose your command component with your library component by running
wac plug <path/to/command.wasm> --plug <path/to/library.wasm> -o main.wasm
. -
Run the composed component using
wasmtime run main.wasm
See Composing Components for more details.
Distributing Components
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 will form an 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 should be able to invoke it from a registry, just as easily as you would add a NPM package from the NPM registry.
Publishing and distribution is a work in progress. The proposed registry protocol is warg, but this is still in development, and there are no public warg registries as yet. You can find more information about the development of the registry protocol here.
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: one for the calculator engine, one for the
addition operation, and one for the 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 may seem excessive, but the reason is to illustrate real-world use cases where components come from different authors and packages.
These files can be found in the component book repository in the wit
directory under wit/adder/world.wit
and wit/calculator/world.wit
. These files define:
-
A world describing an world that exports the "add" interface. Again, components such as the calculator can call it when they need to add numbers.
// wit/adder/world.wit package docs:adder@0.1.0; interface add { add: func(a: u32, b: u32) -> u32; } world adder { export add; }
-
An interface for the calculator itself. We'll use this later to carry out calculations. It contains an evaluate function, and an enum that delineates the operations that can be involved in a calculation. In this tutorial, the only operation is
add
. -
Interfaces for the various operations the calculator might need to carry out as part of a calculation. For the tutorial, again, the only import we define is for the "add" operation from the "docs:adder" world defined previously.
-
A world 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 as "add"), meaning it relies on other components to perform those operations.
-
A world describing the "primary" app component, which imports the "calculate" interface. This is the component will take in command line arguments and pass them to the "eval-expression" function of the calculator component.
// 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; }
Create an add
component
Reference the language guide and authoring components
documentation 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 and authoring components
documentation to create a component that implements the
calculator
world of wit/calculator/world.wit
. For reference, see the completed
example. 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 wasm:cli
hosts). The 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 first compose the calculator component with the add component to satisfy it's
imports. We then compose that resolved calculator component with the command component to satisfy
its calculate
imports. The result is a command component that has all its imports satisfied and
exports the wasi:cli/run
function, 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
v14.0.0
or greater release, as earlier releases of
the wasmtime
command line 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?!
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.
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
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.
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.