Running Rust on Wippy

Build a Rust WebAssembly component, register it with Wippy, and expose it through function, CLI, and HTTP entries.

Classification: Runnable tutorial with an external Rust component toolchain. The page supplies the WIT, Rust implementation, Wippy registry, integrity-hash workflow, commands, expected results, and failure checks.

What We're Building

A Rust component with four exported functions:

  • greet — Accepts a name and returns a greeting
  • add — Adds two integers
  • fibonacci — Computes the nth Fibonacci number
  • list-files — Lists files in a mounted directory

The Wippy application registers these exports as callable functions, CLI commands, and an HTTP endpoint.

Prerequisites

  • Wippy runtime v0.3.32a.
  • Rust toolchain with the wasm32-wasip1 target.
  • A working C toolchain. On Linux, cargo-component also requires OpenSSL development libraries.
  • cargo-component 0.21.1, the release used by this tutorial.
rustup target add wasm32-wasip1
cargo install cargo-component --version 0.21.1 --locked

Create the generated component scaffold and Wippy directories:

mkdir rust-wasm-demo
cd rust-wasm-demo
cargo component new --lib demo
mkdir -p app/src/demo/wasm

In PowerShell:

New-Item -ItemType Directory -Path rust-wasm-demo
Set-Location rust-wasm-demo
cargo component new --lib demo
New-Item -ItemType Directory -Path app\src\demo\wasm -Force

cargo component new writes a compatible Cargo.toml, src/lib.rs, WIT file, and later regenerates src/bindings.rs. Keep the generated wit-bindgen-rt version paired with the installed cargo-component; the tool describes that interface as experimental and does not guarantee generated-code compatibility across versions.

Project Structure

rust-wasm-demo/
├── demo/                    # Rust component
│   ├── Cargo.toml
│   ├── wit/
│   │   └── world.wit       # WIT interface
│   └── src/
│       ├── bindings.rs      # generated by cargo-component
│       └── lib.rs           # implementation
└── app/                     # Wippy application
    ├── wippy.lock
    └── src/
        ├── _index.yaml      # Infrastructure
        └── demo/
            ├── _index.yaml  # CLI processes
            └── wasm/
                ├── _index.yaml          # WASM entries
                └── demo_component.wasm  # Compiled binary

Step 1: Create the WIT Interface

WebAssembly Interface Types (WIT) defines the contract between the host and guest component.

Create demo/wit/world.wit:

package component:demo;

world demo {
    export greet: func(name: string) -> string;
    export add: func(a: s32, b: s32) -> s32;
    export fibonacci: func(n: u32) -> u64;
    export list-files: func(path: string) -> string;
}

Each export becomes a function that Wippy can call.

Step 2: Implement in Rust

Keep the generated demo/Cargo.toml. Its package metadata must target component:demo, matching the WIT package, and its library crate type must remain cdylib.

Create demo/src/lib.rs:

#[allow(warnings)]
mod bindings;

use bindings::Guest;

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello, {}!", name)
    }

    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn fibonacci(n: u32) -> u64 {
        if n <= 1 {
            return n as u64;
        }
        let (mut a, mut b) = (0u64, 1u64);
        for _ in 2..=n {
            let next = a + b;
            a = b;
            b = next;
        }
        b
    }

    fn list_files(path: String) -> String {
        let mut result = String::new();
        match std::fs::read_dir(&path) {
            Ok(entries) => {
                for entry in entries {
                    match entry {
                        Ok(e) => {
                            let name = e.file_name().to_string_lossy().to_string();
                            let meta = e.metadata();
                            let (kind, size) = match meta {
                                Ok(m) => {
                                    let kind = if m.is_dir() { "dir" } else { "file" };
                                    (kind, m.len())
                                }
                                Err(_) => ("?", 0),
                            };
                            let line = format!("{:<6} {:>8}  {}", kind, size, name);
                            println!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                        Err(e) => {
                            let line = format!("error: {}", e);
                            eprintln!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                    }
                }
            }
            Err(e) => {
                let line = format!("cannot read {}: {}", path, e);
                eprintln!("{}", line);
                result.push_str(&line);
                result.push('\n');
            }
        }
        result
    }
}

bindings::export!(Component with_types_in bindings);

The bindings module is generated by cargo-component from the WIT definition.

Step 3: Build the Component

cd demo
cargo component build --release

This produces target/wasm32-wasip1/release/demo.wasm. Copy it to your Wippy app:

mkdir -p ../app/src/demo/wasm
cp target/wasm32-wasip1/release/demo.wasm ../app/src/demo/wasm/demo_component.wasm

In PowerShell:

New-Item -ItemType Directory -Path ..\app\src\demo\wasm -Force
Copy-Item -LiteralPath target\wasm32-wasip1\release\demo.wasm `
  -Destination ..\app\src\demo\wasm\demo_component.wasm

Get the SHA-256 hash for integrity verification:

sha256sum ../app/src/demo/wasm/demo_component.wasm

On PowerShell, use:

(Get-FileHash ..\app\src\demo\wasm\demo_component.wasm -Algorithm SHA256).Hash.ToLowerInvariant()

Copy the 64 lowercase hexadecimal characters into every YOUR_HASH_HERE below. The final field must have the form sha256:<64-hex-characters>; it is the hash of the copied binary, not the Rust source or the original build path.

Step 4: Wippy Application

Infrastructure

Create app/src/_index.yaml:

version: "1.0"
namespace: demo

entries:
  - name: gateway
    kind: http.service
    meta:
      comment: HTTP server
    addr: ":8090"
    lifecycle:
      auto_start: true

  - name: api
    kind: http.router
    meta:
      comment: Public API router
      server: demo:gateway
    prefix: /

  - name: processes
    kind: process.host
    lifecycle:
      auto_start: true

  - name: terminal
    kind: terminal.host
    lifecycle:
      auto_start: true

  - name: policy
    kind: security.policy
    meta:
      comment: Grants access to mounted filesystems and WASM functions
    policy:
      actions:
        - fs.get
        - funcs.call
      resources: "*"
      effect: allow

Mounting a filesystem into a WASM module and calling a WASM function are both guarded actions. The policy grants them; entries that need them reference it.

WASM Functions

Create app/src/demo/wasm/_index.yaml:

version: "1.0"
namespace: demo.wasm

entries:
  - name: assets
    kind: fs.directory
    meta:
      comment: Filesystem with WASM binaries
    directory: ./src/demo/wasm

  - name: greet_function
    kind: function.wasm
    meta:
      comment: Greet function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    pool:
      type: inline

  - name: add_function
    kind: function.wasm
    meta:
      comment: Add function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: add
    pool:
      type: inline

  - name: fibonacci_function
    kind: function.wasm
    meta:
      comment: Fibonacci function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: fibonacci
    pool:
      type: inline

Key points:

  • A single fs.directory entry provides the WASM binary.
  • Multiple functions reference the same binary with different method values.
  • The hash field verifies binary integrity at load time.
  • The inline pool serializes calls through one warm instance. It resets per-call execution state between synchronous calls; use another pool type when you need concurrent workers.

Functions with WASI

The list-files function accesses the filesystem, so it needs WASI imports:

  - name: list_files_function
    kind: function.wasm
    meta:
      comment: Filesystem listing with WASI mounts
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data
    pool:
      type: inline

The wasi.mounts section maps a Wippy filesystem entry to a guest path. Inside the WASM module, /data points to the demo.wasm:assets directory.

CLI Commands

Create app/src/demo/_index.yaml:

version: "1.0"
namespace: demo.cli

entries:
  - name: wasm_cli_policy
    kind: security.policy
    policy:
      actions:
        - fs.get
      resources:
        - demo.wasm:assets
      effect: allow

  - name: ls
    kind: process.wasm
    meta:
      comment: List files from mounted WASI filesystem
      command:
        name: ls
        short: List files from mounted directory
        security:
          actor: {id: demo.cli:ls}
          policies: [demo:policy]
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data

The meta.command block registers the process as a named CLI command. The greet command needs no WASI imports since it only uses string operations. The ls command needs filesystem access, so it also carries the security context that grants the mount.

HTTP Endpoint

Add to app/src/demo/wasm/_index.yaml:

  - name: http_greet
    kind: function.wasm
    meta:
      comment: Greet exposed via wasi-http transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    transport: wasi-http
    pool:
      type: inline

  - name: http_greet_endpoint
    kind: http.endpoint
    meta:
      comment: HTTP POST endpoint for WASM greet
      router: demo:api
    method: POST
    path: /greet
    func: http_greet

The wasi-http transport maps HTTP request/response context to WASM arguments and results.

Step 5: Initialize and Run

cd app
wippy init

Run CLI Commands

# List available commands
wippy run list
Available commands:

  greet  Greet someone via WASM  (demo.cli:greet)
  ls  List files from mounted directory  (demo.cli:ls)

Run with: wippy run <command>

Arguments after the command name are passed to the exported function as string parameters, so each command takes exactly the arguments its WIT signature declares:

# Run greet
wippy run greet World
Hello, World!
# Run ls to list mounted directory
wippy run ls /data

The command should print at least demo_component.wasm with its file size and exit with status 0. Wippy does not print arbitrary process.wasm return payloads, which is why the CLI example uses the Rust function that writes to WASI stdout.

Run as a Service

wippy run

This starts the HTTP server on port 8090. The wasi-http transport passes the request body as the function's single string argument:

curl -X POST http://localhost:8090/greet -d 'World'
Hello, World!

Call from Lua

WASM functions are called the same way as Lua functions. The calling process needs funcs.call on the target, which demo:policy grants:

local funcs = require("funcs")

local greeting, err = funcs.call("demo.wasm:greet_function", "World")
-- greeting: "Hello, World!"

local sum, err = funcs.call("demo.wasm:add_function", 6, 7)
-- sum: 13

local fib, err = funcs.call("demo.wasm:fibonacci_function", 10)
-- fib: 55

Troubleshooting and Cleanup

  • If cargo component is unknown, install it and rerun cargo component build; plain cargo build does not generate the same bindings/component output for this setup.
  • A missing src/bindings.rs before the first build is expected. A missing file after cargo component build indicates the WIT package or component metadata could not be resolved; fix that build error before copying a binary.
  • WASM hash mismatch means the binary changed after the documented digest was calculated or one placeholder remains. Recopy the release binary, recompute the digest, and update every entry that references it.
  • An import-instantiation error means the component imports a host profile omitted by the entry. Keep the documented wasi:cli, wasi:io, wasi:clocks, and wasi:filesystem imports on the filesystem examples.
  • cannot read /data means the wasi.mounts guest path or its filesystem entry does not match the registry.
  • Stop the HTTP runtime with Ctrl+C. Rust build output remains under demo/target/; remove that directory and the copied .wasm file to clean generated artifacts.

Next Steps