Skip to content

Run Rust programs

A Rust program compiled for wasm32-wasip1 runs in the VM as a real Linux process. A shell starts it, pipes bytes through it, waits for it and signals it like any native program. It reads and writes the VM’s live files, the same inodes native programs see, and its computation runs as WebAssembly in the browser, not as emulated x86.

Plain Rust covers standard streams, arguments, the environment, files, clocks and randomness. For what WebAssembly System Interface (WASI) Preview1 cannot express, such as native child processes, sockets, the process’s real identity and its working directory, add the oxidevm-bridge crate.

  1. Add the target once:

    Terminal window
    rustup target add wasm32-wasip1
  2. Build in release mode:

    Terminal window
    cargo build --release --target wasm32-wasip1

    The program is target/wasm32-wasip1/release/<name>.wasm.

  3. Keep the program within the limits. The default Rust linker output fits; set a maximum memory if your program needs a lot of it:

    .cargo/config.toml
    [target.wasm32-wasip1]
    rustflags = ["-C", "link-arg=--max-memory=268435456"]
Rust Works Notes
std::env::args, std::env::vars Yes UTF-8 strings.
println!, std::io::stdin/stdout/stderr Yes Binary-safe. A write to a closed pipe ends the program with SIGPIPE, or returns BrokenPipe when SIGPIPE is ignored, as on Linux.
std::io::IsTerminal Yes Agrees with Linux isatty: /dev/null and redirected files are not terminals.
std::fs (read, write, metadata, read_dir, create_dir_all, rename, hard_link, read_link, remove_*, set_len, sync_all) Yes On the VM’s live files. sync_all reports a storage failure as an error.
Relative paths, std::env::current_dir With oxidevm-bridge WASI has no working directory: without the crate a program starts at /. Linking the crate moves it to the directory it was started in, before main.
SystemTime::now, Instant, thread::sleep Yes The VM’s clock. A sleep waits without blocking the VM.
HashMap seeding, getrandom Yes The VM’s entropy.
Creating a symbolic link, positional reads and writes Not from stable std std::os::wasi::fs is unstable on stable Rust. The VM supports both.
std::process::Command No Use oxidevm_bridge::process::Command.
std::net No Use oxidevm_bridge::net.
std::thread::spawn, async runtimes, fork, dlopen No Programs are single-threaded.
[dependencies]
oxidevm-bridge = "0.1"
use oxidevm_bridge::process::{Command, ExitStatus};
use std::time::Duration;
let output = Command::new("/bin/gzip")
.args(["-n", "-c"])
.input(std::fs::read("summary.csv")?)
.timeout(Duration::from_secs(10))
.output()?;
assert_eq!(output.status, ExitStatus::Exited(0));
std::fs::write("summary.csv.gz", &output.stdout)?;
println!("gzip ran as pid {}", output.pid);
  • The program is an absolute path. There is no shell and no PATH lookup, so nothing is interpolated.
  • Input and output are bytes. Both output streams are drained together, so a child that fills a pipe while it reads cannot deadlock.
  • status is the child’s exact result, Exited(code) or Signaled(signal), and is never squeezed into your program’s own exit range.
  • The child belongs to your program. It is killed when its deadline passes, and when your program ends for any reason, kill -9 included. Children that it forks itself follow ordinary Linux rules.
  • Command::status() runs the child and writes its output to your program’s own standard output and error.
  • Errors: Error::TimedOut, Error::OutputLimit, Error::InvalidCommand (refused before anything starts) and Error::Host(errno) for a Linux error such as a missing program.
use oxidevm_bridge::net::{TcpStream, UnixStream};
use std::io::{Read, Write};
use std::net::Shutdown;
let mut db = TcpStream::connect("127.0.0.1", 5432)?;
db.write_all(b"ping")?;
db.shutdown(Shutdown::Write)?;
let mut reply = Vec::new();
db.read_to_end(&mut reply)?;
let mut local = UnixStream::connect("/workspace/app.sock")?;
let mut tls = TcpStream::connect_tls("example.com", 443, &["http/1.1"])?;

Connections use the VM’s own sockets and the network reach of the module that runs your program. TLS is verified against the VM’s /etc/ssl/cert.pem and cannot be switched off. Errors keep their std::io::ErrorKind: ConnectionRefused, NotFound, PermissionDenied for a reach the module does not have, BrokenPipe for a write to a closed peer (never a SIGPIPE).

let me = oxidevm_bridge::process::info()?; // pid, parent, uid, gid as the VM reports them
let started_in = oxidevm_bridge::process::cwd()?;

Outside OxideVM, or when the module does not provide what a call needs, every call returns Error::Unavailable (ErrorKind::Unsupported for sockets). Code that uses the crate builds and tests natively.

From a shell, with the oxidevm-rust module. Install it in your app’s folder:

Terminal window
npm exec -- oxidevm wasm add github:spacia-ai/oxidevm-rust@v0.1.1

Once the application turns on the module’s commands, any program in the VM runs a .wasm file:

Terminal window
rust-wasi ./report.wasm -- /workspace/data.csv
rust-wasi --dir /workspace/project:ro ./lint.wasm src/

rust-wasi reads the file once, checks it before any of its code runs, and starts it with its own arguments. By default the program may use the whole VM tree under the permissions of the user who ran it, like a native program. --dir grants only the directories named, and :ro grants one read-only. The working directory must then be inside a granted directory.

From your own module. Pin your compiled program as an artifact and run it with host.kit.wasi:

import { defineModule } from "@oxidevm/sdk/wasm";
export default defineModule({
manifest: {
id: "report", version: "1.0.0", abi: "wasi",
artifacts: [{ name: "report.wasm", from: "target/wasm32-wasip1/release/report.wasm" }],
process: { kind: "command" }, commands: ["report"],
requires: { host: ["files.sync", "files.at", "clock", "random", "process.info", "process.sync", "net.vm"] },
license: { spdx: "Apache-2.0" },
},
adapter: async host => {
const program = await host.kit.wasi.compile(await host.artifacts.bytes("report.wasm"));
return {
main: io => host.kit.wasi.run(host, program, io, { argv: [io.invocation.kind === "command" ? io.invocation.argv0 : "report", ...io.args] }),
};
},
});

Declare only what your program uses. Without files.at there are no files beyond the standard streams, without clock and random those calls fail, and the bridge calls need process.info, process.sync and a network reach.

  • An exit status from 0 to 119 is reported exactly. A larger one is reported as 1, with a line on standard error naming the original. The module command range ends at 119.
  • Ctrl+C, kill and every other signal end the program as Linux ends a process, as a signal death (130 for SIGINT, 143 for SIGTERM, 137 for SIGKILL), whether it is computing, sleeping or waiting for a child.
  • A Rust panic aborts the run with the runner’s adapter-failure status.
Limit Value
Program file (rust-wasi) 32 MiB
Memory One unshared 32-bit memory with a declared maximum of at most 256 MiB
Table At most one function table, with a declared maximum of at most 65,536 entries
Start section Not allowed
Arguments, environment 64 KiB each
Path 4,095 bytes
Open descriptors, native results and connections 256 together
Native child 64 arguments, 5 MiB input, 1 MiB per output stream, 180 s deadline

Direct WebAssembly computation is not counted in the VM’s instruction budget, and a running program is not part of a restore point: only the files it committed survive.

Message Meaning
has a start section, imports its memory, defines a memory without a maximum… The program does not fit the profile. Build it as a plain wasm32-wasip1 command.
imports from the unknown namespace env The program was built for Emscripten or wasm-bindgen, not WASI.
bridge ABI major 2 is not supported The program uses a newer oxidevm-bridge than the module serves.
the OxideVM bridge is not available to this program The module does not declare the capability the call needs.
the working directory … is not beneath a granted --dir rust-wasi was started outside every --dir it was given.