Tutorial
If you like to learn by doing, this tutorial will walk through how to build, compose, and run components through a calculator example. Calculators can conduct many operations: add, subtract, multiply, and so on.
In this example, each operation will be a component, that will be composed with
an eval-expression component that will evaluate the expression using the expected operator. With
one operation per component, this calculator is exaggeratedly granular to show how independent logic
of an application can be contained in a component.
In production, components will likely have a larger scope than a simple mathematical operation.
Our eventual solution will involve three components:
- A calculator engine,
- An addition operation
- A command-line interface.
Once we have built these as separate Wasm components, we will compose them into a single runnable
component, and test it using the wasmtime CLI.
The calculator interface
For tutorial purposes, we are going to put our "calculator engine" and "addition operation" interfaces into two separate WIT packages, each containing one WIT file.
This setup may seem excessive, but it illustrates a real-world use case where components come from different authors and packages.
These files can be found in the component book repository in the examples/tutorial/wit directory under wit/adder/world.wit and wit/calculator/world.wit:
// wit/adder/world.wit
package docs:adder@0.1.0;
interface add {
add: func(x: u32, y: u32) -> u32;
}
world adder {
export add;
}
// wit/calculator/world.wit
package docs:calculator@0.1.0;
interface calculate {
enum op {
add,
}
eval-expression: func(op: op, x: u32, y: u32) -> u32;
}
world calculator {
export calculate;
import docs:adder/add@0.1.0;
}
world app {
import calculate;
}
These files define:
- A world
adderthat exports theaddinterface. Again, components such as the calculator can call it when they need to add numbers. - A world
calculatordescribing the calculator component. This world exports the calculator interface, meaning that other components can call it to perform calculations. It imports the operation interfaces (such asadd), meaning it relies on other components to perform those operations. - An interface
calculatethat contains an evaluate function and an enum that delineates the operations that can be involved in a calculation. In this tutorial, the only operation isadd. - A world
appdescribing the "primary" app component, which imports thecalculateinterface. This component will take in command line arguments and pass them to theeval-expressionfunction of the calculator component.
Create an add component
Reference the language guide to create a component that implements the
adder world of adder/wit/world.wit.
For reference, see the completed example.
Create a calculator component
Reference the language guide to create a component that implements the
calculator world of wit/calculator/world.wit.
For reference, see the completed example.
Once complete, the component should import the add function from the adder world and call it if the op enum matches add.
Create a command component
A command is a component with a specific export that allows it to be executed directly by
wasmtime (or other wasi:cli hosts).
The WebAssembly host expects it to export the wasi:cli/run
interface, which is the equivalent
of the main function to WASI.
To build a command component, cargo should be configured to build a --lib crate which
adheres to the wasi:cli interface. Crucially the component must export wasi:cli/run
To scaffold a new runnable Wasm application:
cargo new --lib example
cd example
This component will implement the app world, which
imports the calculate interface.
In Cargo.toml, configure this crate as a cdylib:
[lib]
crate-type = "cdylib"
In addition, we'll need to modify the WIT interface that imports the calculation function and makes use of it to be runnable:
world app {
import calculate;
+ export wasi:cli/run@0.2.7;
}
Since the calculator world imports the add interface, the command component needs to pull in the adder WIT as a dependency, as well.
To do this, we can wkg from wasm-pkg-tools. We must first configure wkg to find the docs:adder repository:
# $XDG_CONFIG_HOME/wasm-pkg/config.toml
default_registry = "ghcr.io"
[namespace_registries]
# Tell wkg that the component-book WITs can be found at ghcr.io/bytecodealliance/docs
docs = { registry = "docs", metadata = { preferredProtocol = "oci", "oci" = {registry = "ghcr.io", namespacePrefix = "bytecodealliance/" } } }
note
We have published the docs:adder WIT package to GHCR ahead of time, so it is easy to find/access via the configuration above.
To make your own custom WITs available, please use the wkg publish command.
With the project scaffolded, now we must actually 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
openum - Calls the
calculateinterface'seval_expression, passing in the arguments.
For reference, see the completed code listing.
Composing the calculator
Now, we are ready to bring our components together into one runnable calculator component, using
wac.
We will:
- Compose the calculator component with the add component to satisfy the calculator component's
adderimport - Compose that resolved calculator component once more with the command component to satisfy the command component's
calculateimport.
The result is a fully-formed command component that has all its imports satisfied and has a single
export (the wasi:cli/run interface), which can be executed by wasmtime.
wac plug calculator.wasm --plug adder.wasm -o composed.wasm
wac plug command.wasm --plug composed.wasm -o final.wasm
If you'd prefer to take a more visual approach to composing components, see the documentation on composing components with wasmbuilder.app.
Running the calculator
Now it all adds up! Run the final component with the wasmtime CLI, ensuring you are using a
recent release (v14.0.0 or greater), as earlier releases of
the wasmtime CLI do not include component model support.
wasmtime run final.wasm 1 2 add
1 + 2 = 3
To infinity and beyond!
To expand the exercise to add more components, modify calculator.wit to add another operator world
and expand the op enum. Then, modify the command and calculator components to support the
expanded enum.
Another extension of this tutorial could be to remove the op enum and instead modify
eval-expression to take in a string that can then be parsed to determine which operator component
to call. Maybe this parser is a component of its own?!