Writing a WebAssembly Component
Start a new project
In this tutorial we’ll be making a template renderer component in Rust.
Prerequisites
Ensure you have wick-cli and just
installed.
Starting
The wick
repository includes templates in the templates/
folder with templates in the common liquid
template format. Use cargo generate
(cargo install cargo-generate
) to easily pull, render, and setup new components.
$ cargo generate candlecorp/wick templates/rust --name jinja
The template comes with a sample component.wick
that defines two operations, greet
and add
.
---
name: 'jinja'
kind: wick/component@v1
metadata:
version: '0.0.1'
component:
kind: wick/component/wasmrs@v1
ref: build/component.signed.wasm
operations:
- name: greet
inputs:
- name: input
type: string
outputs:
- name: output
type: string
- name: add
inputs:
- name: left
type: u64
- name: right
type: u64
outputs:
- name: output
type: u64
This file powers Wick’s code generation and is embedded into your WebAssembly module at build time. Wick uses the configuration and its operations, types, descriptions, and other metadata to automatically configure and validate Wick applications.
Get rid of the example operations and add one called render
. The render
operation will need the raw template and arbitrary data to render the template with. The template will be a string
type and – since template data can be anything – the data input can be a generic object
. The object
type represents any JSON-like object. Since it’s common for templates to stay static while the data changes, we can define the template
as part of the operation’s configuration, rather than its input. An operation will receive configuration once while its input streams can have any number of elements.
As for our output, it will be a string
and we’ll name it rendered
.
The component.wick
should now look like this:
---
name: 'jinja'
kind: wick/component@v1
metadata:
version: '0.0.1'
component:
kind: wick/component/wasmrs@v1
ref: build/component.signed.wasm
operations:
- name: render
with:
- name: template
type: string
inputs:
- name: data
type: object
outputs:
- name: rendered
type: string
Add dependencies
Add the template renderer minijinja
to our project with cargo add
or by modifying the Cargo.toml
file by hand. minijinja
is a Rust implementation of the jinja template library.
$ cargo add minijinja
Update implementation
This template’s build step will generate a Component
struct and traits for every operation defined in the manifest. The operation traits take each input as a separate stream argument and one final argument for the output stream(s).
_Note: The generated code can be found in your target
directory. Have a peek at the generated code by looking in the target/wasm32-unknown-unknown/debug/build/jinja-_/out/
directory (after running a build at least once). The code generator runs every time thecomponent.wick
file changes.*
Replace the contents of src/lib.rs
with the following:
mod wick {
wick_component::wick_import!();
}
use wick::*;
#[wick_component::operation(generic_raw)]
async fn render(
mut inputs: render::Inputs,
mut outputs: render::Outputs,
ctx: Context<render::Config>,
) -> Result<(), anyhow::Error> {
let mut templates = minijinja::Environment::new();
templates.add_template("root", &ctx.config.template)?;
let template = templates.get_template("root")?;
while let Some(input) = inputs.data.next().await {
let data = input.decode()?;
let rendered = template.render(data)?;
outputs.rendered.send(rendered);
}
Ok(())
}
The body of the implementation is standard Rust code that can use any crate you’ve added to your Cargo.toml
. Wick uses streams everywhere, so many operations start with a loop awaiting for values. Expecting everything to be a stream and accounting for streaming cases up front makes components more flexible and reusable while still working perfectly fine for common cases.
Since we get the template as part of our with
configuration block, we have access to it immediately and can pre-compile it with tips from the minijinja
guide.
Finally we send the rendered template to our output stream named rendered
with outputs.rendered.send()
.
Outside the loop we can do whatever cleanup we need and close the output streams with outputs.rendered.done()
. The streams will close automatically when the operation returns, but it’s good practice to close them explicitly.
Run just build
Run the just build
task to compile our code to WebAssembly, embed our component definition and types, and sign the module with our private key. If you don’t have any keys yet, they will be automatically generated for you by wick
when you run just build
and they will be put in ~/.wick/keys/
.
$ just build
Run your component
Use wick invoke
to invoke any operation in your signed component .wasm
file. By default, the built artifacts get copied to a build
directory in the project root.
wick invoke
takes the path to your .wick
file, the name of the operation to invoke, and any arguments to pass to the operation. The --
is required to separate the arguments to wick invoke
from the arguments to the operation.
Note: Wick expects Component and Operation configuration passed on the CLI to be valid JSON.
$ wick invoke ./component.wick render --op-with '{ "template": "{%raw%}Hello {{ name }}!{%endraw%}" }' -- --data '{ "name": "Samuel Clemens" }'
{"payload":{"value":"Hello Samuel Clemens!"},"port":"rendered"}
Note: Wick processes input configuration as a Liquid template which has similar syntax as Jinja. We’re using Liquid’s {%raw%}...{%endraw%}
syntax to treat the inner template as raw text.
Writing valid JSON in CLI arguments is cumbersome. We can use Wick’s @file
syntax to take data from the filesystem. The command above is equivalent to the following, assuming the data has been written to files config.json
and data.json
.
$ wick invoke ./component.wick render --op-with @config.json -- --data @data.json
{"payload":{"value":"Hello Samuel Clemens!"},"port":"rendered"}
Success! wick
loaded our component, validated it’s integrity, found our operation, and invoked it with the arguments we passed.
The output is a JSON-ified representation of Wick packets, the pieces of data that flow through pipelines in the wick runtime.
To get the raw output, we can use the --values
flag:
$ wick invoke ./component.wick render --values --op-with @config.json -- --data @data.json
Hello Samuel Clemens!
Now that you have a streaming WebAssembly component, there are a bunch of places to go next.
Do you want to: