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-wasip1target. - A working C toolchain. On Linux,
cargo-componentalso 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.directoryentry provides the WASM binary. - Multiple functions reference the same binary with different
methodvalues. - The
hashfield verifies binary integrity at load time. - The
inlinepool 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 componentis unknown, install it and reruncargo component build; plaincargo builddoes not generate the same bindings/component output for this setup. - A missing
src/bindings.rsbefore the first build is expected. A missing file aftercargo component buildindicates the WIT package or component metadata could not be resolved; fix that build error before copying a binary. WASM hash mismatchmeans 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, andwasi:filesystemimports on the filesystem examples. cannot read /datameans thewasi.mountsguest 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.wasmfile to clean generated artifacts.
Next Steps
- WASM Overview — WebAssembly runtime overview
- WASM Functions — Function configuration reference
- WASM Processes — Process configuration reference
- Host Functions — Available WASI imports
- CLI Reference — CLI command documentation