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?!