Skip to content

Write a Rust component

You write Rust. RUSM compiles it to wasm32-wasip2 and runs it as a sandboxed, supervised process — isolated memory, capability-gated I/O, crash-recovered by the supervisor. No cargo-component, no jco, no manual WIT plumbing. Write logic; RUSM handles the rest.

Scaffold & run in 30 seconds

rusm new creates a complete new RUSM project — rusm.toml, a components/ folder, and a starter component ready to build and serve:

sh
rusm new myapp --rust   # new project with a Rust HTTP component
cd myapp
rusm build              # cargo build --target wasm32-wasip2 → wasm/api.wasm
rusm serve              # live on http://127.0.0.1:8080

Want WebSocket or SSE instead?

sh
rusm new myapp --rust --protocol ws    # new project with a WebSocket component
rusm new myapp --rust --protocol sse   # new project with an SSE component

Adding a component to an existing project

Use rusm generate component <name> [--lang ts|rust|go] [--protocol http|ws|sse] — it adds a new components/<name>/ and the matching rusm.toml entry without touching anything else. rusm generate bridge <name> scaffolds a new host bridge the same way.

A component is a folder under components/ with its own Cargo.toml and src/lib.rs:

my-app/
├── rusm.toml
├── components/
│   └── api/
│       ├── Cargo.toml      # rusm-rs dep, crate-type = ["cdylib"]
│       └── src/lib.rs
└── wasm/                   # rusm build writes api.wasm here

Two shapes

Service — a module of functions

Annotate a module with #[rusm_rs::service]. RUSM generates the receive → dispatch → reply loop and a typed Client — so callers reach it with ordinary method calls that are actually cross-process messages:

rust
// components/calc/src/lib.rs
#[rusm_rs::service]
pub mod calc {
    pub fn add(a: i64, b: i64) -> i64 { a + b }

    pub fn count_to(n: i64) -> impl Iterator<Item = i64> { 1..=n } // streaming

    pub fn work(progress: rusm_rs::Callback<i64>) -> String {      // callback
        for pct in [25, 50, 100] { progress.call(pct); }
        "done".into()
    }
}

Worker — a #[rusm_rs::main] fn

A one-shot entry point: runs once, does the job, exits. Use calc::Client::spawn to reach the service — the typed client hides spawn + send + receive behind a plain method call:

rust
// components/commander/src/lib.rs
use calc::calc::Client as CalcClient;

#[rusm_rs::main]
fn run() {
    let calc = CalcClient::spawn("calc").unwrap();

    println!("2 + 3 = {}", calc.add(2, 3).unwrap());             // typed call
    for n in calc.count_to(3).unwrap() { println!("{n}"); }       // streaming
    calc.work(|pct| println!("progress {pct}")).unwrap();         // callback
}

The calc crate is a path dependency — the Client type lives there (generated by the macro), and commander imports it. Neither component shares runtime memory; they share only the type.

Declare in rusm.toml

toml
[components.calc]
capability = "sandboxed"

[components.commander]
capability = "trusted"   # inherits allow-spawn

Build & run

sh
rusm build   # cargo build --target wasm32-wasip2 per component → wasm/*.wasm
rusm run     # spawn them per rusm.toml
rusm dev     # build + run, then watch ./components and hot-reload on every save

One toolchain, no extra steps — cargo build --target wasm32-wasip2 componentizes directly. rusm dev keeps running: save a file and the component rebuilds and reloads automatically.

What rusm-rs gives you

The full actor toolkit, all typed and serde-backed:

rusm_rs::me()this process's Pid
send_bytes(pid, &[u8]) / send(pid, &T)send a message
receive_bytes() / receive::<T>()wait for a message (parks the fiber)
spawn("name")spawn a component by rusm.toml name
svc::Client::connect(pid)typed cross-process call (the #[service]-generated client)
register("name") / whereis("name")named registry
register_tag("tag") / whereis_tag("tag")process-group tags
send_after(pid, ms, msg) / cancel_timer(h)timers
monitor(pid)watch for a process exit (a __down message)
kill(pid)terminate another process
Stream::open(pid) / Stream::accept()byte streams
set_label("label")a label for the observer

Errors are ordinary Results. Logging is the standard log crate — log::info!, log::error! — routed to the node's unified stream by #[rusm_rs::main] / #[handlers] automatically. The host stamps the time, component#pid, and severity. No setup, no allow-stdio.

Same wire as TypeScript and Go

A Rust service and a TypeScript or Go caller interoperate out of the box — they speak the same JSON wire. Mix languages freely; the type system keeps each component honest within its own boundary.

Go deeper

MIT licensed