This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Building Components in Rust

Building your first components in Rust.

1 - Prerequisites for Rust components

Dependencies you may need to install for this guide

Rust & Cargo

To build Rust components you will need the Rust compiler and companion tools installed.

Install rust & cargo via rustup for Windows, Mac, or Linux.

Windows users may need to install the C++ build tools with the “Desktop development with C++” module.

WebAssembly target for Rust

To compile Rust into WebAssembly, you need to install a wasm-* target.

Install both the cross-platform wasm32 target and the WASI (WebAssembly Systems Interface) implementation with this rustup command:

rustup target add wasm32-unknown-unknown wasm32-wasi

Node.js & npm

Wasmflow’s code generators are written in JavaScript to make it as easy as possible for people to contribute generators for new languages.

If you don’t have node.js & npm, install it via nvm on Mac or Linux or nvm-windows on Windows.

tomlq

tomlq is a command line parser for TOML files and Wasmflow uses it to grab information from toml files like Cargo.toml.

Install tomlq with the command

cargo install tomlq

wasmflow-codegen

wasmflow-codegen is Wasmflow’s code generator.

Install wasmflow-codegen via npm install -g @candlecorp/codegen

make on Windows

Wasmflow and generated projects use Makefiles to automate builds and code generation. The easiest way to install make on Windows is via Chocolatey.

$ choco install make

2 - Create a new project

Using wafl to create a new boilerplate project

Start a new project with wafl

$ wafl project new my-project rust

wafl will set up a new directory named my-project in the current directory and will clone a suitable boilerplate project. You can specify a git repository URL in place of the language argument to use your own boilerplate projects.

Note

The default boilerplate project comes with some suggested settings for Visual Studio Code users. Feel free to ignore, change, or remove them. They are not required to build the project.

Your first schema

Take note of the schema found in schemas/my-component.apex

namespace "my-component"

type Inputs {
  input: string
}

type Outputs {
  output: string
}

type Config {

}

This schema defines a component named my-component with one input port named input and one output port named output, both dealing with string data. The schema also defines a Config type that is currently empty.

Let’s change our component name to ‘greet’ so we have something slightly more meaningful. Your new schema should look like this:

namespace "greet"

type Inputs {
  input: string
}

type Outputs {
  output: string
}

type Config {

}

Finally, let’s finish this first step by adding our personal and project details to the Cargo.toml file. You can fill out the name, description, and authors to be anything you want, but the rest of this guide assumes your project is named my-project. Change the built artifact names accordingly if you use something different.

[package]
name = "my-project"
version = "0.0.0"
description = "A cool new project"
authors = ["You <you@email.com>"]
edition = "2018"
license = "BSD-3-Clause"

Next we’ll Build and Run your component.

3 - Build and run your new WebAssembly component

Build and run your WebAssembly component with wasmflow

Build your component.

Build your component by running make

$ make

Make generates source code, builds your WebAssembly module, generates keys, signs your module using wafl and stores both the signed and unsigned artifacts in the /build directory.

Tip

The build process also generates source code from your schema(s). Do not edit any that are prefixed with a “This is generated” warning or your changes will be lost on next build.

Run your component

Use [wasmflow] to load your module and execute a component on the command line, sending the string "my_input" to the input port named input.

$ wasmflow invoke ./build/my_project.signed.wasm greet -- --input="my_input"

If all has gone well, you will see nothing. Our component doesn’t output anything yet so naturally we don’t get any interesting output. To convince yourself that something is actually happening, pass --trace or --debug to [wasmflow] for more output. Remember to add the flag before the -- which separates arguments for [wasmflow] vs arguments destined for the executed module. You should see something similar to the below.

$ wasmflow invoke ./build/my_project.signed.wasm greet --trace -- --input="my_input"

2022-07-01T02:39:01 TRACE Logger initialized
2022-07-01T02:39:01 DEBUG Writing logs to ~/.local/state/wasmflow/logs
2022-07-01T02:39:01 DEBUG load as file location="./build/my_project.signed.wasm"
2022-07-01T02:39:01 TRACE bytes include wasm header? is_wasm=true bytes=[0, 97, 115, 109]
2022-07-01T02:39:01 DEBUG wasi enabled id=TODO config=Permissions { dirs: {} }
2022-07-01T02:39:01 DEBUG Collection permissions params=Permissions { dirs: {} }
2022-07-01T02:39:01 DEBUG WASI configuration params=WasiParams { argv: [], map_dirs: [], env_vars: [], preopened_dirs: [] }
2022-07-01T02:39:01 TRACE wasmtime instance loaded duration_μs=118656
2022-07-01T02:39:01 DEBUG wasmtime initialize duration_μs=118859
2022-07-01T02:39:01 DEBUG Input 'my_input' for argument 'input' is not valid JSON. Wrapping it with quotes to make it a valid string value.
2022-07-01T02:39:01 TRACE wasm invoke target=wafl://__local__.coll/greet
2022-07-01T02:39:01 DEBUG wasm invoke component="greet" id=4042109442 payload={"input": [168, 109, 121, 95, 105, 110, 112, 117, 116]}
2022-07-01T02:39:01 TRACE wasm call finished component="greet" id=4042109442 duration_μs=21125
2022-07-01T02:39:01 TRACE wasm call result component="greet" id=4042109442 result=Ok([])
2022-07-01T02:39:01 TRACE stream complete
Tip

The name of your component is dictated by the namespace definition in your schema. The filename is normalized based on best practices for the target language.

Next we’ll add logic to our component.

4 - Adding logic to your component

How to work with component ports

Open up the newly generated component file at ./src/components/greet.rs and add to it so it looks like this:

pub use crate::components::generated::greet::*;

#[async_trait::async_trait]
impl wasmflow_sdk::v1::ephemeral::BatchedComponent for Component {
    async fn job(
        inputs: Self::Inputs,
        outputs: Self::Outputs,
        _config: Option<Self::Config>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        let greeting = format!("Hello {}", inputs.input);
        outputs.output.done(greeting)?;
        Ok(())
    }
}

Take note of these lines:

let greeting = format!("Hello {}", inputs.input);
outputs.output.done(greeting)?;

The first line uses Rust’s format!() macro to format our input string into a suitable greeting.

The second line takes that greeting string and pushes it to the output port named output.

Finally, done() sends a packet and closes the port in one command.

Tip

Change the port names in your Apex schema and rebuild your component to see how the code generation reflects the changes.

Build and run your component with the new logic to see the output:

$ make
$ wasmflow invoke ./build/my_project.signed.wasm greet -- --input="my_input"
{"output":{"value":"Hello my_input"}}
Tip

Change the inputs.input expression to something like inputs.input.to_uppercase() to see how modules can act on data as it comes through.

Next we’ll add new components to our collection.

5 - Adding components

Adding additional components to our module

A single component is like a single function. A collection of components is like a library. It’s a collection of related functions.

Before we think about writing code, we have to define the contract first. The contract is a schema that describes the inputs, outputs, and configuration for a component.

Use wafl component new [component name] to create a new schema quickly. Let’s call this component concatenate.

$ wafl component new concatenate
2022-06-20T22:09:04  INFO Creating new schema for concatenate at schemas/concatenate.apex

Edit the Inputs and Outputs in your schema to look like the following.

type Inputs {
  left: string
  right: string
}

type Outputs {
  output: string
}
Note

Without knowing anything about the implementation, can you guess what this component will do? If you guessed that it is going combine two strings together as output, you’re correct!

Of course this scenario is contrived, but it’s a signature part of contract driven development and extends beyond “Hello World” style tutorials. The contract is often more important than the name.

Generate the new code

The wasmflow code generator will generate all the necessary files based off the .apex files found in ./schemas/.

Run it automatically with:

$ make codegen

Add our concatenation logic

Add this rust code to the new src/components/concatenate.rs file.

pub use crate::components::generated::concatenate::*;

#[async_trait::async_trait]
impl wasmflow_sdk::v1::ephemeral::BatchedComponent for Component {
    async fn job(
        inputs: Self::Inputs,
        outputs: Self::Outputs,
        config: Option<Self::Config>,
    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        outputs
            .output
            .done(format!("{} {}", inputs.left, inputs.right))?;
        Ok(())
    }
}

Build and run our new component

This command looks similar to our last command, but take note that we’re sending data on multiple ports now.

$ make
$ wasmflow invoke ./build/my_project.signed.wasm concatenate -- --left=Hello --right=World
{"output":{"value":"Hello World"}}

Success! Now that we’ve got a module, let’s see how we can turn it into a microservice.