Skip to content

Latest commit

 

History

History
222 lines (178 loc) · 6.97 KB

File metadata and controls

222 lines (178 loc) · 6.97 KB

WebAssembly Guests

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.

Two Guest Shapes

ShapeTargetHow 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.

Rust Core Modules

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.

Python Components

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 ./bindings

Implement 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 Core Modules

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.

C Core Modules

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

Manifest

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.

Guest API Crate

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.

Testing

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