Quick start

Your first steps, in Rust, in about twenty minutes.

By the end of this page you will have written two test steps as ordinary Rust functions, assembled them into an executor you can copy to another machine, and watched Anvil refuse to start a run because you mistyped a parameter name. No instrument, no rack time, no licence.

The one idea to carry through it. The step measures; the sequence judges. Your Rust code never contains the pass/fail threshold, so tightening a tolerance is a text edit — not a recompile, not a redeploy, and not a phone call to whoever wrote the code.

What you need

  • Linux on x86-64. The binaries are statically linked; there is nothing to install. (The release also carries a Windows zip with the same binaries as .exe; the commands on this page are the Linux ones, and they are the ones that were run.)
  • A Rust toolchain, plus the wasm32-wasip2 target.
  • A clone of the Anvil repository — the step SDK lives there. You will not build Anvil itself.

The Rust step SDK is not published on crates.io yet, so for now you point at it by path inside a clone of the repository. That is a rough edge we know about, and it is the only part of this page that will get shorter.

Read this once

An executor is a folder, not a file.

anvil-exec-wasm is a program, and it serves every .wasm sitting next to its own binary. Each of those files is a module, named after the file, and a step is called <module>/<step>.

That gives the shape of everything below: a department is a folder you can copy — the executor's binary with its modules inside. A sequence says which executor it is addressing, by naming its binary or, when it lives on another machine, its address. It never says where the modules are, because that is the executor's business.

This page builds two modules rather than one, because with a single module you cannot see what the module name buys you.

Once, and never again

Set-up

Unpack, clone, add a target. Everything lands in one folder you can delete when you are done.

01

Get the binaries

The package holds two files: anvil, which carries its own WebAssembly engine — that is why it weighs what it weighs and why it runs on a factory PC with no internet — and anvil-exec-wasm, the executor that will serve your steps.

# from github.com/anlaco/anvil/releases
tar xzf anvil-v0.5.0-x86_64-linux-musl.tar.gz
cd anvil-v0.5.0-x86_64-linux-musl
./anvil --version
anvil 0.5.0

Everything below happens inside that folder. It also carries the example sequences, including a department already assembled — run ./anvil ejemplos/demo_departamento.yaml right now if you want to see the finished shape before building it yourself.

02

Bring in the step SDK

anvil-step is the toolbox you import when writing a step: one dependency and the plain Rust toolchain. There is no wit/ directory to copy, no generated bindings to keep, and no cargo component to install.

git clone https://github.com/anlaco/anvil.git anvil-src
rustup target add wasm32-wasip2

Cloned as anvil-src on purpose: a folder called anvil would collide with the binary you just unpacked.

Every day from now on

Write the steps, assemble the department, run the sequence.

03

Two modules, one workspace

A workspace rather than two loose projects, for a practical reason: members share one target/, so both .wasm files come out in the same directory.

mkdir -p hello-wasm/multimeter/src hello-wasm/plc/src

hello-wasm/Cargo.toml:

[workspace]
members = ["multimeter", "plc"]
resolver = "2"

hello-wasm/multimeter/Cargo.toml:

[package]
name = "multimeter"
version = "0.1.0"
edition = "2021"

[dependencies]
anvil-step = { path = "../../anvil-src/executors/rust/anvil-step" }

[lib]
crate-type = ["cdylib"]

The package name is the module name. It gives the .wasm its file name, and the file name is the multimeter/ you will write in the YAML. It is not declared anywhere else. crate-type = ["cdylib"] is required: without it you do not get a loadable component.

hello-wasm/plc/Cargo.toml is the same file with name = "plc".

04

The step is a function

hello-wasm/multimeter/src/lib.rs:

use anvil_step::{step, Outcome};

/// Measures DC voltage on a channel.
#[step(outputs(channel_used: f64))]
fn measure_voltage(channel: Option<f64>) -> Outcome {
    let channel = channel.unwrap_or(1.0);
    Outcome::measured(4.8)
        .message(format!("channel {channel}"))
        .output("channel_used", channel)
}

anvil_step::export!();

hello-wasm/plc/src/lib.rs:

use anvil_step::{step, Outcome};

/// Reads the 24 V rail the PLC feeds.
#[step]
fn measure_voltage() -> Outcome {
    Outcome::measured(23.7).message("24 V rail")
}

anvil_step::export!();

Both steps are called measure_voltage and they do not collide. That is exactly what the module name buys: multimeter/measure_voltage and plc/measure_voltage are two different steps.

Every part of that signature tells Anvil something. channel is the name a sequence will use; f64 says it is a number; and wrapping it in Option makes it optional. The doc comment is the description the catalogue publishes.

Note what is absent: nowhere does this code say whether 4.8 V is acceptable. That is not the step's business.

export!() goes at the end of each module, once. Outcome::measured(…) reports a number; Outcome::passed, failed and error report a verdict with no measurement. A step that could not measure returns error, never failed — a broken bench is not a bad unit.

05

Compile

cargo build --target wasm32-wasip2 \
  --manifest-path hello-wasm/Cargo.toml

hello-wasm/target/wasm32-wasip2/debug/multimeter.wasm
hello-wasm/target/wasm32-wasip2/debug/plc.wasm

The first build takes about a minute: it compiles the SDK and its dependencies. After that it is seconds.

06

Assemble the department

The executor serves what sits beside it, so a department is assembled by putting the binary and the modules in one folder.

mkdir -p department
cp anvil-exec-wasm department/
cp hello-wasm/target/wasm32-wasip2/debug/*.wasm department/

And that folder is the executor. Copy it to another machine, start it there, and the same sequence reaches it by naming an address instead of a path — the steps do not change.

Trap. The executor serves everything in that folder, so any .wasm left over from before is served as a module too. Rename a package and you must delete the old file. The --list below shows you immediately.

07

The signature is the catalogue

Ask the executor what it serves, without writing a sequence and without running anything. You wrote each description in exactly one place — the function — so it cannot drift from the code that runs.

./department/anvil-exec-wasm --list

multimeter  sha256:df251ca8537e4ff951187465be8f4c31b…
    …/department/multimeter.wasm
    multimeter/measure_voltage(channel: number = optional)
        Measures DC voltage on a channel.
        outputs: channel_used
plc  sha256:2e6ace99408b73165f564fd03960b6a1c…
    …/department/plc.wasm
    plc/measure_voltage()
        Reads the 24 V rail the PLC feeds.

Each module with its SHA-256, each step with its signature. Note that you never told it where to look: it reads the folder its own binary is in. This is what you consult before writing the YAML, instead of writing it blind.

08

The sequence is data

This is the file a test engineer writes and reviews — plain text, in a diff, with no programmer in the loop. Save it as hello.yaml.

name: hello_bench

executors:
  - name: bench
    type: wasm
    path: department/anvil-exec-wasm

main:
  - name: multimeter/measure_voltage
    executor: bench
    inputs:
      channel: 3
    limit:
      type: range
      min: 4.5
      max: 5.5

  - name: plc/measure_voltage
    executor: bench
    limit:
      type: range
      min: 23.0
      max: 25.0

path is the executor's binary, and it is relative to the YAML file, not to wherever you launch anvil from. It is not a .wasm and not the folder of modules — where those are is the executor's business.

limit is the acceptance criterion, and it lives here rather than in the code.

Since 0.5.0 a sequence may also be saved as hello.yseq: the same YAML, with an extension that says what the file is for. ./anvil hello.yseq runs it the same way.

If the executor runs on another machine, you declare a connection instead — type: grpc with its host and port — and nothing else changes. The steps are still multimeter/measure_voltage. Over there you start the same anvil-exec-wasm, from the folder you copied.

09

Check before you measure

Anvil asks each executor what it serves and checks your sequence against the answer — once, before the first step. Mistype a name and see for yourself:

./anvil hello.yaml --validate --with-executors

  - step 'multimeter/measure_voltage' (bench): it takes
    no input called 'channell' (it takes: channel)

And the same for a module that does not exist:

  - step 'multimetre/measure_voltage': executor 'bench'
    does not serve it (it serves:
    multimeter/measure_voltage, plc/measure_voltage)

Without that check the run would have started, the executor would have dropped the unknown value, and the step would have measured something else and reported a pass. The message names what you actually have, because the mistake is nearly always a typo.

--validate on its own reads and checks the file without connecting to anything, which is what makes it usable in CI with no hardware. --with-executors is the explicit opt-in for when they are up.

10

Run it

The verdict is also the exit code — 0 passed, anything else did not — so a sequence can gate whatever comes after it.

./anvil hello.yaml

=== hello_bench: pass ===
  [pass] multimeter/measure_voltage: channel 3
  [pass] plc/measure_voltage: 24 V rail

Diagnostics go to stderr and the report to stdout, so 2>/dev/null leaves you the report alone. Among those diagnostics is the SHA-256 of each module that answered — today the start-up log is the only place that is written down.

./anvil hello.yaml --json out.json --csv out.csv

11

Now change the criterion, not the code

Set max: 4.7 on the first step. Touch no Rust. Compile nothing. Run it again:

=== hello_bench: fail ===
  [fail] multimeter/measure_voltage: 4.8$ echo $?
1

The step still measured 4.8 and still reported it happily. What changed is the verdict, and the detail line names the value it read and the range it was judged against.

That is the whole thesis in one edit. Whoever owns the tolerance can change it without owning the code — and the change is a line in a text file, with an author and a date, not a rebuilt binary nobody can diff.

12

Iterating

Every time you touch a lib.rs: compile, and deploy again. The executor serves what is in department/, not what you just compiled.

cargo build --target wasm32-wasip2 \
  --manifest-path hello-wasm/Cargo.toml
cp hello-wasm/target/wasm32-wasip2/debug/*.wasm department/
./anvil hello.yaml

One step more than you might expect, and it is the price of the YAML knowing nothing about your build tree. Adding a module is adding a folder to the workspace and copying its .wasm: the executor is not touched, you only name the new step in the sequence.

Worth knowing before you hit them

Four traps

  • Compiling is not deploying. Change a step, forget the copy, and the previous one runs. The hash in the start-up log is the only clue.
  • path names the executor's binary. Pointing it at a .wasm is the natural mistake; it stops with a message that says so, rather than an "Exec format error" that sends you to look at your toolchain.
  • panic! ends the phase, not the run. The step comes back as error with the WebAssembly trap written into the report, the rest of main is skipped as after any error, and cleanup still runs. It used to cut the whole run (issue 58, fixed after 0.4.0). Still, return Outcome::error(…) and say why, rather than unwrap() and hand the report twenty lines of backtrace.
  • Parameters come in four types: f64, String, bool and Option of those. Anything else does not compile, on purpose — a sequence is text, and those are the things text can carry.

From here: return extra named values that land in the report alongside the measurement, split a long sequence into reusable sub-sequences, keep an open instrument session in the executor and carry a reference to it through several steps, or write your steps in Python or C# instead. All of it is in the repository documentation.

The sequence is data. The step is a function.

Everything above runs on a laptop. When the instrument is real, only the body of the function changes.

Anvil on GitHub ↗ See it against a simulated instrument