Linking at Build
Overview
At build-time, WebAssembly components may be composed, meaning they will be packaged together as a single component and linked internally. The composability of components is a core tenet of components' design. Open source CLI tools like wac
and WASI Virt enable you to compose components before deployment.
It's important to note that build-time composition is a matter of link declaration. These links will be resolved upon component instantiation by the WebAssembly runtime—in our case, Wasmtime, the underlying runtime used by the wasmCloud host.
When to link at build
Linking at build makes sense when...
- Components' functionality is tightly coupled, and you expect to always update the components at the same time
- You want to use a library from another language in your primary component
- Services performed by multiple components should scale together, and you want to maximize network efficiency
- Sensitive functionality such as filesystem operations need to be sandboxed
Example: Composition
In this example, we'll perform multiple compositions, using tools like WAC and WASI Virt to compose Wasm binaries that run in wasmCloud and Wasmtime, and ultimately arriving at a single composed component.
Prerequisites
This demo is designed to demonstrate multiple ecosystem tools, including:
Note that multiple tools require Cargo, and WASI Virt requires the nightly release channel for Rust, so you may wish to install that up front:
rustup toolchain install nightly
wasmCloud Shell (wash
)
Follow the instructions for your OS on the Installation page. Since several of the following tools use Cargo, you may wish to use wash
through Cargo as well.
cargo install --locked wash-cli
If you have cargo-binstall
installed, you can install even faster:
cargo binstall wash-cli
Wasmtime
On Linux or macOS, you can install Wasmtime locally with an install script:
curl https://wasmtime.dev/install.sh -sSf | bash
WebAssembly Compositions (WAC)
WAC requires Cargo. Once you've installed Cargo:
cargo install wac-cli
WASI Virt
WASI Virt requires the nightly release channel for Rust:
rustup toolchain install nightly
Install the wasi-virt
command line tool with Rust's cargo
package manager:
cargo +nightly install --git https://github.com/bytecodealliance/wasi-virt
Example files
The example files are included in the wasmCloud monorepo at github.com/wasmcloud/wasmcloud. You can perform a "sparse" checkout to download only the relevant example directory (and associated documentation and metadata):
git clone --depth 1 --no-checkout https://github.com/wasmCloud/wasmCloud.git
cd wasmcloud
git sparse-checkout set ./examples/rust/composition
git checkout
cd examples/rust/composition
For this example, we will use the pong
and http-hello2
directories as well as the compose.wac
file at the root of the composition
project directory.
Virtualizing a component
Navigate to the pong
directory. Here we have a Rust-based component with a custom interface called pong
that will return a string "ping" on its exported pingpong
interface. Build the component:
cd pong
wash build
We can use wash inspect
to take a look at the WIT for this component:
wash inspect --wit ./build/pong_s.wasm
package root:component;
world root {
import wasi:cli/environment@0.2.0;
import wasi:cli/exit@0.2.0;
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;
import wasi:random/random@0.2.0;
export example:pong/pingpong;
}
You can see the pingpong
interface listed as an export and wasi:cli/environment
as one of many imports.
Now it's time to perform our first composition in the form of a virtualization with WASI Virt. Using WASI Virt, we're going to compose pong_s.wasm
into an encapsulating component with an environment variable PONG
set to demo
. The resulting component will be named virt.wasm
.
wasi-virt build/pong_s.wasm --allow-random -e PONG=demo -o virt.wasm
Now let's view the WIT for our virtualized component:
wash inspect --wit virt.wasm
package root:component;
world root {
import wasi:random/random@0.2.0;
export example:pong/pingpong;
}
The virtualized component still exports pingpong
but no longer requires wasi:cli/environment
—that import (and all of the others except the new wasi:random/random
, which we added via a WASI Virt argument) is satisfied by the encapsulating component. If we run this component on wasmCloud or with Wasmtime, the host will be able to satisfy the random
import. But we still need another component to invoke this one via the exposed pingpong
interface.
Composing a component
In the http-hello2
directory, we have a modified http-hello-world
component that imports the pingpong
interface and calls for a string from pong
to append to the hello world message. Navigate to that directory and build the component.
cd ../http-hello2
wash build
For context, let's try running wasmtime serve
without composing pong
and http-hello-world
together.
wasmtime serve -S cli=y build/http_hello_world.wasm
Error: component imports instance `example:pong/pingpong`, but a matching implementation was not found in the linker
Caused by:
0: instance export `ping` has the wrong type
1: function implementation is missing
As we might expect, it doesn't work—the hello world component doesn't have anything linked to fulfill the pong
import. If we compose it with the pong
component, however, we essentially wire those corresponding imports and exports together within the encapsulating component. Head back to the root of the project directory with cd ..
and then we'll use WAC to perform the composition.
WAC is a tool for composing components at build-time. It has the ability to do complex compositions using the .wac
file format (an example of which can be found below as an optional step). But for purposes of our example, we're going to use the wac plug
command to perform the composition:
wac plug --plug pong/virt.wasm ./http-hello2/build/http_hello_world.wasm -o output.wasm
The "plug" in this example is the component exporting what the other component needs. So in our case, it is exporting example:pong/pingpong
.
Now we have a new file: output.wasm
. Taking a look at our composed component, we can see that we've ended up with a combination of both components:
wash inspect --wit output.wasm
package root:component;
world root {
import wasi:random/random@0.2.0;
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:http/types@0.2.0;
import wasi:cli/environment@0.2.0;
import wasi:cli/exit@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 wasi:http/incoming-handler@0.2.0;
}
Let's try running it with Wasmtime:
wasmtime serve -S cli=y output.wasm
curl localhost:8080
Hello World! I got pong demo
We can run the composed component in wasmCloud as well:
wash app deploy wadm.yaml
Congratulations! You've composed a component that runs anywhere supporting WASI P2. For more information on linking components at runtime or at build via composition, including when you might want to use each approach, see Linking Components in the wasmCloud documentation.
Advanced WAC
WAC can also work by encoding a composed component based on the instructions defined in a .wac file. The .wac file uses a superset of WIT also called WAC. This section contains an example that does the exact same thing as the plug
command above, but explicitly defining it with a WAC file. In this example we have a compose.wac
file. Here are the contents of that file:
package demo:composition;
let pong = new ping:pong { ... };
let hello = new hello:there { "example:pong/pingpong": pong.pingpong, ... };
export hello...;
This is a pretty simple composition, but the the design of WAC facilitates much more complex compositions of many components. You can learn more about WAC usage in the tool's readme and language guide.
For this demo's purposes, we simply need to run wac encode
while indicating the components we wish to compose with the --dep
argument, naming our output file with -o
, and specifying our .wac
instructions:
wac encode --dep ping:pong=./pong/virt.wasm --dep hello:there=./http-hello2/build/http_hello_world_s.wasm -o output.wasm compose.wac
You can then run the output.wasm
in the same way as above!
Keep reading
You can learn more about composing components in the Component Model documentation.