wasm-polycall
v2.0.0
Published
WebAssembly binding for Polycall: a Node.js host that implements the "polycall" imports of wasm32 guests by calling the real native Polycall core (binding ABI v1).
Maintainers
Readme
wasm-polycall
wasm-polycall is the WebAssembly binding for
Polycall (binding ABI v1, Polycall
= 1.1.0). It has two halves:
- Guest side —
include/polycall_wasm.h: the functions, status codes and limits of the nativepolycall.h, declared as imports from thepolycallimport module. A C (or Rust, Zig, ...) program compiled towasm32calls them like the native API. - Host side — a Node.js host (this npm package) that implements every
polycallimport by calling the real native Polycall core (polycall.dll/libpolycall.dll/libpolycall.so.1) through the koffi FFI. Nothing is emulated: configuration validation, RPC calls and peer nodes all run in the core.
guest.wasm (wasm32) Node.js host (wasm-polycall) native core
polycall_peer_send(...) ---> "polycall" imports: validate guest ---> libpolycall
(i32 pointers into the pointers, pass views of guest (binding ABI v1)
guest's linear memory) memory to the core via koffiWhy a host binding (and not the core compiled to wasm)
The core needs BSD sockets and POSIX threads. WASI preview 1 has neither:
wasi-libc does not declare socket/connect/bind/listen and has no
pthread.h, so the core cannot be compiled to wasm32-wasi with working
networking (checked with clang 14 + Debian wasi-libc). The guest therefore
calls the native core through host imports.
Install
npm install wasm-polycall # pulls koffi (prebuilt binaries)The native core must be installed separately (Polycall >= 1.1.0). The host
loads, in this order: POLYCALL_LIBRARY (full path), then polycall.dll
and libpolycall.dll on Windows, libpolycall.so.1 on Linux
(libpolycall.1.dylib on macOS, which is not tested — see Platforms). Every
symbol is resolved and polycall_ffi_abi_version() == 1 is checked before a
guest runs; a missing library, an old 1.0 library or an ABI mismatch throws a
PolycallLoadError (reason: not-found, missing-symbol,
abi-mismatch) naming the library — never a crash.
Guest API (wasm32)
#include "polycall_wasm.h" /* clang --target=wasm32-wasi -I<pkg>/include ... */
polycall_peer_t inbox, outbox;
polycall_peer_open("inbox", "127.0.0.1:0", NULL, &inbox);
polycall_peer_open("outbox", NULL, NULL, &outbox); /* send-only */
polycall_peer_send(outbox, "127.0.0.1:9001", "hi", 2, "m-1", 5000);Same signatures and semantics as polycall.h (caller-owned buffers,
snprintf rules, negative POLYCALL_E_* status, polycall_last_error for
the detail), with these differences forced by the wasm boundary:
| | |
| --- | --- |
| pointers / size_t | 32-bit offsets into the guest memory / unsigned 32-bit; size_t * and polycall_peer_t * outputs are 4-byte cells |
| polycall_strerror | a guest-side inline over the import polycall_wasm_strerror(status, buf, cap); returns a static guest buffer |
| polycall_get_version | not imported (1.0 API); use polycall_ffi_version |
| bad guest pointers | a pointer or region outside the guest memory, or an unterminated string, is POLYCALL_E_INVALID_ARGUMENT with a host detail in polycall_last_error; the core is not called |
| blocking | a polycall_peer_recv with a long timeout blocks the host thread running the guest: run such guests in a worker_threads Worker so another thread can call polycall_peer_cancel / polycall_peer_close (peer handles are process-wide, so any thread or guest instance may use them) |
The full import table (names, wasm types, C prototypes) is
generated/polycall/abi.json. The
wasm-polycall 1.x contract — env.polycall_ffi_run_config(path_ptr, run) —
is still provided and reaches the same core function.
Security: the imports give a guest the host's network (peer nodes, RPC) and read access to configuration files by host path. Instantiate trusted modules only.
Host API (Node.js >= 22)
const {
instantiatePolycallModule, runPolycallModule, createPolycallHost, loadPolycall,
PolycallLoadError, STATUS, LIMITS
} = require("wasm-polycall");
// run a WASI command guest to completion (exit code)
const code = await runPolycallModule("guest.wasm", { wasi: { args: ["guest.wasm", "x"], env: {} } });
// or instantiate and drive exports yourself
const { instance, host } = await instantiatePolycallModule("guest.wasm", {
imports: { env: { /* your own imports */ } }
});
console.log(host.library.path, host.library.version); // the loaded corecreatePolycallHost({ memory }) returns { imports, setMemory, library }
for embedding into your own instantiation (imports.polycall and the legacy
imports.env). WASI preview 1 (node:wasi) is supplied automatically when a
module imports it.
CLI
npx wasm-polycall doctor # which core was loaded, version, ABI
npx wasm-polycall run guest.wasm -- ARGS # run a guest against the real core
npx wasm-polycall inspect guest.wasm # imports/exports, polycall imports used
npx wasm-polycall host-smoke wasm-polycallrc # validate a config from inside a wasm moduleExample
clang --target=wasm32-wasi -O2 -I include examples/hello.c -o hello.wasm
npx wasm-polycall run hello.wasm -- wasm-polycallrc
# hello.wasm: Polycall 1.1.0, binding ABI 1
# hello.wasm: wasm-polycallrc is valid for running with this build
# hello.wasm: 127.0.0.1:40123 received 'hello from wasm' from wasm-outbox as hello-1node examples/run.js wasm-polycallrc does the same check without a wasm
toolchain (the module is assembled in JS).
Tests
npm ci
npm test # tests/run.js: exit 0 pass, 1 failure, 77 something could not runAll tests use the real core (POLYCALL_LIBRARY or the default names) and
the C CLI (POLYCALL_CLI or polycall on PATH); guests are built with
clang --target=wasm32-wasi (WASM_CC), or taken prebuilt from
WASM_POLYCALL_GUEST_DIR. Missing pieces are reported as SKIP, never as a
pass.
| file | what |
| --- | --- |
| host.test.js | imports over guest memory, a JS-assembled wasm module, guest-pointer validation, non-ASCII config path |
| real-core.test.js | tests/guest/abi_test.c (wasm32-wasi) — the whole BINDING_ABI.md checklist from inside the guest, call against polycall start and polycall daemon start, and payloads both ways with a polycall peer serve C node |
| workers.test.js | guests in 4 worker threads: concurrent senders (own nodes and one shared handle), concurrent calls, a blocked receive woken by cancel, close and an arriving message |
| loader.test.js | default names, POLYCALL_LIBRARY, missing / old / ABI-2 / partial libraries |
| bin.test.js, package.test.js | CLI and package metadata |
Platforms
Tested: Linux x86-64 and Windows x64 (both polycall.dll from MSVC and
libpolycall.dll from MSYS2 UCRT64). macOS is not claimed: the core has no
verified macOS build yet.
License
MIT © Nnamdi Michael Okpala
