crossbind
GitHub
GUIDE · PLATFORMS · EDGE

Edge

Two builds reach the edge. -e edge makes a single-threaded WebAssembly module and its loader, which a Cloudflare Worker imports and boots with initNative(). -e http makes a WASI HTTP component: a C++ request handler with no JavaScript at all, which wasmtime serve, Spin and wasmCloud run.

Cloudflare Workers

Workers, and the edge runtimes that follow their model, run JavaScript in V8 isolates: no Web Workers, no SharedArrayBuffer and no bundler plugin, so the Worker imports the build output itself. Install the CLI and name the project, which names the output files:

shell
npm install --save-dev crossbind@beta
crossbind.config.mjs
export default {
general: { name: 'my-worker' },
paths: { config: import.meta.url, output: 'dist' },
};

With no bundler to see your imports, the build binds every .h file under src/native, so a port is reached through a header of your own. Build, and the loader and its wasm land in dist:

shell
npx crossbind build -p wasm -a wasm32 -r st -e edge -b release
index.js
import initNative from './dist/my-worker-wasm-wasm32-st-release.edge.js';
import wasm from './dist/my-worker-wasm-wasm32-st-release.edge.wasm';
 
export default {
async fetch() {
const { Native } = await initNative({ getWasmFunction: () => wasm });
return new Response(Native.sample());
},
};

Native is a class declared in src/native/native.h. Edge runtimes cannot compile WebAssembly from bytes, so the Worker imports the .wasm as a module and getWasmFunction (Runtime API) hands it to the loader. initNative() boots once per isolate and returns the same module on every later call.

Boot from the handler rather than with a top-level await: Workers forbid random number generation in global scope, and Rust's standard library asks for random bytes while the module starts. The build also writes dist/edge/wasm.mjs, which does both imports itself and exports initNative and every bound name; it is the entry a package publishes for edge runtimes (Publishing packages).

Wrangler bundles the .wasm import with no extra rules:

wrangler.toml
name = "my-worker"
main = "index.js"
compatibility_date = "2024-09-08"
shell
npx wrangler dev
npx wrangler deploy

To start from a working Worker instead, npm create crossbind@beta -- my-worker Cloud "Cloudflare Worker" scaffolds one.

Limits

  • Single-threaded only. -r mt and useWorker need Web Workers, which an isolate does not have.
  • No filesystem for JavaScript. The edge build exports no m.FS and has no OPFS. Keep durable data in a store such as R2 or KV, read it in JavaScript and pass it to your bound functions.
  • No header constants. #define and const values bind only for names an app imports, and a Worker imports no header.
  • Platform caps. A free Worker gets 10 ms of CPU per request, and every plan caps a Worker's size: check your .wasm against yours.

WASI HTTP components

-e http links your C++ into a component that exports wasi:http/[email protected], the export of the wasi:http/service world. There is no main() and no JavaScript: src/native defines the one function <crossbind/http.h> declares, and the host calls it once per request.

src/native/handler.cpp
#include <crossbind/http.h>
 
namespace crossbind::http {
 
// Called once per request, after its whole body has arrived.
Response handle(Request request) {
return {
200,
{ { "content-type", "text/plain" } },
"hello " + request.path + "\n",
};
}
 
}
crossbind.config.mjs
export default {
general: { name: 'my-service' },
paths: { config: import.meta.url, output: 'dist' },
};
shell
npx crossbind build -p wasi -e http -b release
wasmtime serve dist/my-service-wasi-wasm32-st-release.http.wasm

Spin 4 serves the same file from a manifest, with spin up:

spin.toml
spin_manifest_version = 2
 
[application]
name = "my-service"
 
[[trigger.http]]
route = "/..."
component = "my-service"
 
[component.my-service]
source = "dist/my-service-wasi-wasm32-st-release.http.wasm"

wasmCloud's wash dev takes it as build.component_path in .wash/config.yaml (checked by hand with wash 2.10). The build compiles like any WASI command, and ports link in from their -wasi packages.

  • Request carries method, authority, path and query as the client sent them, headers with lowercase names, and body, read whole before handle runs. Response is status (200 unless set), headers and body.
  • An exception out of handle answers 500 and logs crossbind: <method> <path> threw: <what> to stderr.
  • A host can reuse one instance for many requests, so static state outlives a request. Calls of handle never overlap, but one can run while an earlier response body is still being written.
  • Each request runs on a 1 MiB stack of its own; -Wl,-z,stack-size=<bytes> in binary.wasiFlags changes it.

Not there yet: outgoing requests (wasi:http/client), streamed request and response bodies, trailers, threads, Rust, and the data files and environment of dependencies such as PROJ, GDAL and OpenSSL, which no host passes to a component. The component also imports the wasi:cli environment, exit and terminal interfaces, so a host that offers only the wasi:http/service imports rejects it.

examples/cloud-cloudflare-worker is a complete Worker, and e2e/cloud-wasi-http builds a component and tests it under wasmtime serve 47 and Spin 4.2.

Type to search every guide page and section.
↑↓ navigate↵ openesc close