WebAssembly runtime for OpenWorkers using Wasmtime Component Model.
- Standard HTTP contract: guests are
wasi:http/proxycomponents - WASI Support: WebAssembly System Interface (WASIp2)
- Multi-language: Write workers in Rust, Go (TinyGo), C/C++, AssemblyScript
- Secure by design: Capabilities-based sandboxing, no Spectre concerns
+---------------------------------------------+
| openworkers-runner |
+----------------------+----------------------+
|
+----------------------v----------------------+
| openworkers-runtime-wasm |
| +---------------------------------------+ |
| | Wasmtime | |
| | +---------------------------------+ | |
| | | WASM Component (Worker) | | |
| | | - wasi:http/incoming-handler | | |
| | | - openworkers:worker/scheduled | | |
| | +---------------------------------+ | |
| +---------------------------------------+ |
+---------------------------------------------+
A worker is a WASI Preview 2 component that exports
wasi:http/incoming-handler, openworkers:worker/scheduled, or both. Any
component built against the standard wasi:http/proxy world runs unmodified;
wit/worker.wit adds the cron entry point on top of it:
world fetch-worker {
include wasi:http/proxy@0.2.0;
include openworkers:bindings/imports@0.1.0;
import wasi:cli/environment@0.2.0;
}
world worker {
include fetch-worker;
include scheduled-only;
}Target fetch-worker when the worker only serves HTTP and worker when it also
handles cron; the two differ by the scheduled export alone, which is how the
host tells them apart. scheduled-only carries that export on its own, for an
SDK that adds it to fetch-worker only when the guest has a handler.
wasi is declared at 0.2.0, the floor of the 0.2 line, not at the host's patch
level: every 0.2.x is semver-compatible and both wasm-tools and wasmtime resolve
a 0.2.0 import or export against the newer implementation, so a toolchain that
lags still builds against this contract.
The host provides:
wasi:http/outgoing-handler- outbound requests, routed to the runner'sOperationsHandlerwasi:cli/environment- the worker's environment variableswasi:cli/stdoutandwasi:cli/stderr- guest output, forwarded to the runner's log handler line by line (stdout as info, stderr as error)openworkers:bindings/{database,kv,storage}- the platform bindings, seewit/bindings.wit
Bodies are buffered at the boundary in both directions; streaming pass-through is not implemented yet.
Every binding call names its binding first, because a worker can hold several bindings of the same type:
use openworkers::bindings::database::{self, SqlParam, SqlValue};
let params = [SqlParam::Value(SqlValue::Integer(42))];
let row = database::first("DB", "SELECT * FROM items WHERE id = $1", ¶ms)?;database is shaped after D1: preparing a statement and binding its
parameters happens guest-side, and rows come back as JSON text. kv values
are JSON documents; storage bodies are opaque bytes.
RuntimeLimits field |
Mechanism |
|---|---|
max_cpu_time_ms |
fuel, 0 disables metering |
max_wall_clock_time_ms |
epoch interruption, 0 disables the deadline |
heap_max_mb |
a store memory limiter, 0 disables the cap |
heap_initial_mb has no wasm counterpart: a component's memory starts at the
size its module declares.
Fuel is charged per wasm operation, so what a millisecond of CPU buys is a
property of the machine: the runtime measures it once per process by burning a
known loop. OW_WASM_FUEL_PER_MS pins the rate instead, for a host that would
rather not measure or wants every node to charge the same.
wit_bindgen::generate!({
world: "worker",
path: "wit",
generate_all,
});
use exports::openworkers::worker::scheduled::Guest as ScheduledGuest;
use exports::wasi::http::incoming_handler::Guest as HttpGuest;
use wasi::http::types::{Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam};
struct MyWorker;
impl HttpGuest for MyWorker {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let response = OutgoingResponse::new(Fields::new());
let body = response.body().unwrap();
ResponseOutparam::set(response_out, Ok(response));
{
let stream = body.write().unwrap();
stream.blocking_write_and_flush(b"Hello from WASM!").unwrap();
}
OutgoingBody::finish(body, None).unwrap();
}
}
impl ScheduledGuest for MyWorker {
fn handle_scheduled(scheduled_time: u64) {
// Handle cron job
}
}
export!(MyWorker);Build:
cargo build --target wasm32-wasip2 --releaseuse openworkers_runtime_wasm::WasmWorker;
use openworkers_core::{Script, WorkerCode, Task};
// Load WASM component
let wasm_bytes = std::fs::read("worker.wasm")?;
let script = Script::new(WorkerCode::WebAssembly(wasm_bytes));
// Create worker
let mut worker = WasmWorker::new(script, None, None).await?;
// Execute task
worker.exec(task).await?;examples/hello-worker- HTTP plus cron, outbound fetch, environmentexamples/fetch-worker-world fetch-worker: HTTP and the bindings, no cronexamples/proxy-worker- a stockwasi:http/proxycomponent, built from the upstreamwasicrate with no OpenWorkers-specific WIT
# Build the examples
(cd examples/hello-worker && cargo build --target wasm32-wasip2 --release)
(cd examples/fetch-worker && cargo build --target wasm32-wasip2 --release)
(cd examples/proxy-worker && cargo build --target wasm32-wasip2 --release)
# Run tests
cargo testMIT