Running Rust on Wippy

Build a Rust WebAssembly component and run it as functions, CLI commands, and HTTP endpoints.

What We're Building

A Rust component with four exported functions:

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

We'll expose these as callable functions, a CLI command, and an HTTP endpoint.

Prerequisites

rustup target add wasm32-wasip1
cargo install cargo-component

Project Structure

rust-wasm-demo/
├── demo/                    # Rust component
│   ├── Cargo.toml
│   ├── wit/
│   │   └── world.wit       # WIT interface
│   └── src/
│       └── 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

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

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

Create demo/Cargo.toml:

[package]
name = "demo"
version = "0.1.0"
edition = "2024"

[dependencies]
wit-bindgen-rt = { version = "0.44.0", features = ["bitflags"] }

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

[profile.release]
opt-level = "s"
lto = true

[package.metadata.component]
package = "component:demo"

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

Get the SHA-256 hash for integrity verification:

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

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: gateway
    prefix: /

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

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

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
  • inline pool creates a fresh instance per call

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: greet
    kind: process.wasm
    meta:
      comment: Greet someone via WASM
      command:
        name: greet
        short: Greet someone via WASM
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet

  - name: ls
    kind: process.wasm
    meta:
      comment: List files from mounted WASI filesystem
      command:
        name: ls
        short: List files from mounted directory
    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.

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
  ls       List files from mounted directory
# Run greet
wippy run greet
# Run ls to list mounted directory
wippy run ls

Run as a Service

wippy run

This starts the HTTP server on port 8090. Test the endpoint:

curl -X POST http://localhost:8090/greet

Call from Lua

WASM functions are called the same way as Lua functions:

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

Three Ways to Expose WASM

Approach Entry Kind Use Case
Function function.wasm Called from Lua or other WASM via funcs.call()
CLI Command process.wasm + meta.command Terminal commands via wippy run <name>
HTTP Endpoint function.wasm + http.endpoint REST API via wasi-http transport

All three use the same compiled .wasm binary and reference the same methods.

Building for Other Languages

Any language that compiles to the WebAssembly Component Model works with Wippy. Define your WIT interface, implement the exports, compile to .wasm, and configure entries in _index.yaml.

See Also