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
openum - calls the
calculateinterface'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?!