xfetch can run plugins, effects and extensions compiled to WebAssembly. This
guide covers the authoring side; the runtime reference (manifest schema and
CLI tooling) lives in the core repository at
xfetch/docs/WASM.md.
| Shape | Target | How to build |
|---|---|---|
| Core module | wasm32-wasip1 |
Any language that produces a WASI command; JSON on stdin/stdout |
| Component | Component model | componentize-py, componentize-js, wit-bindgen |
Core modules reuse the exact same protocol as native guests, so existing
plugins only need a new target and a manifest. Components use the typed WIT
contract in wit/xfetch-runtime.wit, printable with
xfetch wasm wit.
Add the wasm target and build:
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
The existing API crates work unchanged on wasm32-wasip1. Host
operations come from xfetch-guest-api:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
xfetch-plugin-api = "0.2"
xfetch-guest-api = "0.2"
Only the guest depends on xfetch-guest-api; the core does not
pull it. With "0.2" Cargo resolves the newest compatible 0.2.x
release, so patch updates arrive without changes on your side.
use serde::Deserialize;
use xfetch_guest_api::{http_request, log, protocol_version};
use xfetch_plugin_api::{read_info_plugin_args_or_default, write_info_lines};
#[derive(Debug, Default, Deserialize)]
struct Args {
city: Option<String>,
}
fn main() {
let args = read_info_plugin_args_or_default::<Args>().expect("request");
let city = args.city.unwrap_or_else(|| "London".to_string());
log("info", "fetching weather");
let response = match http_request(
"GET",
&format!("https://wttr.in/{}?format=3", city),
&[],
None,
Some(5_000),
) {
Ok(response) => response,
Err(err) => {
write_info_lines(vec![format!("weather unavailable: {}", err)]).expect("response");
return;
}
};
let line = String::from_utf8_lossy(&response.body).trim().to_string();
let version = protocol_version().unwrap_or(1);
write_info_lines(vec![line, format!("host protocol {}", version)]).expect("response");
}
On non-wasm targets the same code compiles: host calls return
HostErrorKind::Unsupported, which lets you unit test the guest
natively.
componentize-py turns a Python class into a component. Generate
bindings from the WIT contract to see the exact method signatures:
python3 -m venv .venv
. .venv/bin/activate
pip install componentize-py
componentize-py -d path/to/api/wit -w plugin bindings ./bindingsImplement the generated protocol in your app module and bundle it:
import json
from wit_world.imports import host
class WitWorld:
def run(self, request: str) -> str:
payload = json.loads(request)
host.log("info", "hello from python")
return json.dumps({"lines": [f"kind: {payload.get('kind')}"]})componentize-py -d path/to/api/wit -w plugin componentize app -p . -o dist/app.wasm
Use -w effect or -w extension for the other
contracts; the exported method is always run.
Go 1.21+ targets WASI directly; no TinyGo is required:
GOOS=wasip1 GOARCH=wasm go build -o dist/app.wasm .
The request/response types are plain JSON structs. Go guests that only
compute values need no host calls; the runtime calls
proc_exit(0) on return and the host treats it as a clean exit.
Freestanding C needs no WASI sysroot: declare the two or three WASI imports
with clang attributes and build with -nostdlib.
__attribute__((import_module("wasi_snapshot_preview1"), import_name("fd_write")))
extern unsigned int fd_write(unsigned int fd, const void *iovs, unsigned int iovs_len, unsigned int *nwritten);
__attribute__((export_name("_start")))
void _start(void) {
/* read stdin and write a JSON response */
}clang --target=wasm32-wasip1 -O2 -nostdlib -fno-stack-protector \
-Wl,--no-entry -Wl,--export-memory -Wl,--allow-undefined \
-o dist/app.wasm main.c
Ship an xfetch-plugin.json, xfetch-effect.json or
xfetch-extension.json next to the source. The installer copies
it beside the artifact. Minimal example:
{
"manifest_version": 1,
"name": "my-guest",
"kind": "info_provider",
"runtime": "core",
"build": "cargo build --release --target wasm32-wasip1",
"artifact": "target/wasm32-wasip1/release/my-guest.wasm",
"capabilities": {
"http": { "allow": ["https://wttr.in/*"] }
},
"limits": { "timeout_ms": 15000, "memory_mb": 64 }
}
Capabilities are deny-by-default; see the core reference for every field and
the exec, filesystem and environment semantics.
xfetch-guest-api implements the core-module host bridge:
host_call(op, args): raw JSON dispatch.http_request(method, url, headers, body, timeout_ms): typed HTTP with base64 bodies.exec(program, args, stdin, env, timeout_ms): typed process execution.log(level, message): best-effort diagnostic output.protocol_version(): host handshake.
The crate also exports xfetch_alloc and xfetch_free
automatically, which the host needs to place responses in guest memory. It is
a guest-side dependency: installing or building xfetch never requires it.
The same guest source builds natively, where host calls report
Unsupported and stdin/stdout still work. For end-to-end checks:
xfetch wasm inspect ./dist/app.wasm
xfetch wasm run ./dist/app.wasm --request '{"version":1,"kind":"info_provider"}'
xfetch wasm run ./dist/effect.wasm --request_file request.json --kind effect