ReferenceWriting Modules

Edge Python has no bundled stdlib. Three ways to add native functionality:

PathDistributionType coverageMaintenance
.wasm module via URL (WASM ABI)Publish .wasm to a CDN; any host loads dynamicallyPrimitives only (None, bool, i128, f64, bytes/str)Reference wasm-pdk (Rust), community PDKs, or hand-written wire boilerplate
Host capabilityCustom compiler.wasm (additional host imports declared) + host runtime they bridge toPrimitives + access to host services (DOM, FS, fetch) through embedder host importsYou own embedder + host runtime; bindings travel together
JS host modulePlain ESM via createWorker (mainThreadModules eager, hostModules lazy) or the host field of packages.jsonPrimitives only (same as Path A)Pure JS; no Rust, no .wasm, no build step

.wasm matches the marketplace pattern (from "https://x.wasm" import f works in any host). Host capability is for runtime distributions that own their compiler.wasm and expose host services to scripts (the same pattern print and input use). JS host modules keep upstream compiler_lib.wasm untouched while exposing main-thread surface (DOM, dialogs, FileReader, observers, anything window.*).

Path A: .wasm module by URL

Contract: the WASM module ABI, language-agnostic, three scalar types. Rust authors use the bundled wasm-pdk (#[plugin_fn] for free functions, #[plugin_class] + #[plugin_methods] for Python-visible classes, typed Handle / Value / Error); other languages use community PDKs or hand-roll the boilerplate.

Worked examples (with and without the SDK), encoding tables, and language-specific snippets: WASM module ABI. Script side:

from "./my_edge_mod.wasm" import add
print(add(2, 3)) # -> 5

Path B: host capability

Some native functionality can’t live in a CDN-distributed .wasm (Path A) because the work happens outside the WASM sandbox, DOM mutation, WASI filesystem I/O, native crypto. Path A modules see only the sealed 6 env.* imports; they have no channel to the host runtime. Path B closes that gap.

A host capability is shipped as part of a custom embedder. The embedder declares additional host imports beyond the sealed plugin ABI, these imports are the embedder’s private contract with its host runtime, not part of the public plugin contract.

Precedent: print(...) calls the embedder’s host_print import; input() drains a buffer the host fills via set_input. The same shape generalises, a browser-host distribution can register dom as a native module whose query, set_text, append_child operations bridge to JS through embedder-specific host imports. A WASI-host distribution can register fs against wasi_snapshot_preview1. Scripts see them as ordinary native modules:

from dom import document, query # browser host
from fs import read_text, write # WASI host

What ships in a host-capability distribution

ArtifactRole
Custom compiler.wasmVanilla compiler_lib plus declared additional host imports
Host runtimeBrowser shim / WASI loader / native binary that provides those imports
Pure-Python wrappers (.py) (optional)Ergonomic surface on top of the raw bridge, shipped as a code module

Users opt in by loading the custom compiler.wasm and matching host runtime together. Vanilla compiler.wasm keeps working for everyone else.

Sketch

// custom compiler.wasm declares an extra env import beyond the sealed plugin set
#[link(wasm_import_module = "env")]
unsafe extern "C" {
    fn host_dom_op(opcode: u32, ptr: *const u8, len: u32) -> u32;
}
 
// And exposes a `dom` module whose operations bridge through it.

The custom compiler.wasm declares env.host_dom_op alongside the standard env.host_print / env.host_fetch_bytes / env.host_call_native. The host runtime supplies the implementation.

Why this is not a third module flavor

Scripts still see two flavors (code and native, see Imports). Path B is a distribution pattern that ships additional bridges through the embedder; the compiler dispatches them the same way as built-in operations. Keeps the public language surface and the WASM module ABI untouched.

Path C: JS host module

Browsers run the engine in a Web Worker (no document, no window). Path C bridges: a capability ships as plain JavaScript, registers with createWorker({ mainThreadModules }), runs on the main thread. The runtime synthesises the native module registration so Python can from <name> import ...; each call is decoded in the Worker, shipped to main via postMessage, executed against document/window/etc., and the result encoded back. Python sees a synchronous call.

Async handlers (returning a Promise) run concurrently when several coroutines call them under gather: each result is routed back to the coroutine that issued it, and a rejected handler raises a catchable exception in that one coroutine without disturbing its peers.

Three ways to register: pass the imported object to mainThreadModules (eager, shown below); give a URL to hostModules or the packages.json host field, imported lazily the first time a run uses it; or, for the official libraries, rely on the runtime defaults with no config. No .wasm, no Rust, no build step.

Sketch

A module is a factory (ctx) => handlers (or {name: handler}). The factory receives { pushEvent } so async callbacks (event listeners, observers, FileReader, animation finished) can wake a paused receive().

// dom.js
export const dom = ({ pushEvent }) => {
    const nodes = [];
    const alloc = (n) => { if (n == null) return -1; nodes.push(n); return nodes.length - 1; };
    const node = (h) => nodes[h];
 
    return {
        query: (sel) => alloc(document.querySelector(sel)),
        set_text: (h, txt) => { node(h).textContent = txt; },
        bind_event: (h, type, msg) => {
            node(h).addEventListener(type, (e) => {
                pushEvent(JSON.stringify({ msg, type: e.type, target_id: e.target.id }));
            });
        },
    };
};
<script type="module">
    import { createWorker } from "https://runtime.edgepython.com/js/src/index.js";
    import { dom } from "./dom.js";
 
    const worker = await createWorker({
        wasmUrl: "https://runtime.edgepython.com/js/compiler_lib.wasm",
        mainThreadModules: { dom },
    });
    await worker.run(await (await fetch("./script.py")).text());
</script>
from dom import query, set_text, bind_event
bind_event(query("#btn"), "click", "click")
async def main():
    while True:
        receive()
        set_text(query("#btn"), "clicked")
run(main())

Or skip the manual wiring: the browser runtime’s <edge-python> element loads these declaratively from a host field in packages.json. See the runtime README.

Handlers take decoded JS values and return plain JS values. Supported tags: None, bool, int (i64, range-limited by JS Number), float, string bytes. Opaque object references (DOM nodes, files, observers) model as integer IDs into a main-thread registry the handlers own (the alloc / node pattern above).

Trade-offs vs Path B

Path BPath C
Compiler artifactCustom per capability setVanilla upstream
CompositionEmbed-timeLoad-time, by import
Binding languageRust (or C/Zig, any wasm32 target) compiled into the embedderJavaScript, primitives only
Per-op overheadNative call through embedder host importpostMessage round-trip (around 0.1 to 0.4 ms)
Threading modelWherever the embedder runsMain thread (handlers reach document)
Build pipelinecargoNone

Pick Path C when the capability needs main-thread browser surface (DOM, dialogs, observers, FileReader) and per-op latency is acceptable, invisible for UI-rate workloads (around 50 to 200 ops/frame). Reach for Path B when tight per-frame loops dominate or the capability lives in a non-browser host (WASI, native).

Reference implementation: host/.

Choosing between the three paths

You want…Use
Publish a module any Edge Python user can from "<url>" import without rebuildingPath A (.wasm ABI)
Wrap a C/Zig/AS libraryPath A (any wasm32-targeting language works)
Expose host services (DOM, FS, native crypto) bundled into your own runtime distributionPath B (host capability)
Expose browser-main-thread APIs (DOM, dialogs, observers) without shipping a custom embedderPath C (JS host module)

See also

  • WASM module ABI, the wire format spec for Path A.
  • Imports, script-side semantics, packages.json, integrity verification.