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.
Build a program
Section titled “Build a program”-
Add the target once:
Terminal window rustup target add wasm32-wasip1 -
Build in release mode:
Terminal window cargo build --release --target wasm32-wasip1The program is
target/wasm32-wasip1/release/<name>.wasm. -
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"]
What plain Rust does
Section titled “What plain Rust does”| 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. |
Use oxidevm-bridge
Section titled “Use oxidevm-bridge”[dependencies]oxidevm-bridge = "0.1"Run a native Linux program
Section titled “Run a native Linux program”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
PATHlookup, 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.
statusis the child’s exact result,Exited(code)orSignaled(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 -9included. 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) andError::Host(errno)for a Linux error such as a missing program.
Connect to a service
Section titled “Connect to a service”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).
Identity
Section titled “Identity”let me = oxidevm_bridge::process::info()?; // pid, parent, uid, gid as the VM reports themlet 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.
Run it
Section titled “Run it”From a shell, with the oxidevm-rust module. Install it in your app’s folder:
npm exec -- oxidevm wasm add github:spacia-ai/oxidevm-rust@v0.1.1Once the application turns on the module’s commands, any program in the VM runs a .wasm file:
rust-wasi ./report.wasm -- /workspace/data.csvrust-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.
Exit statuses and signals
Section titled “Exit statuses and signals”- 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,
killand 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.
Limits
Section titled “Limits”| 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.
Troubleshooting
Section titled “Troubleshooting”| 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. |