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:

  1. takes in three arguments: two operands and the name of an operator ("1 2 add")
  2. parses the operator name and ensures it is supported in the op enum
  3. calls the calculate interface's eval_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 wasm-tools. 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.

wasm-tools compose calculator.wasm -d adder.wasm -o composed.wasm
wasm-tools compose command.wasm -d 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](https://github.com/bytecodealliance/wasmtime/releases), 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?!