The engine stack cannot be a Linux process image
A running WebAssembly engine owns a native control stack that guest code cannot
inspect, copy, or relocate. Linux needs stronger operations: a blocking path must
release its Worker, bounded preemption must stop at controlled points, and fork() must duplicate a continuation so parent and child can return from
the same call with different values.
HwjsCoroutinize changes the representation. Suspendable frames keep
their live state and resume point in WebAssembly linear memory. The scheduler can
then park, resume, reify, or copy that state without asking the JavaScript engine to
expose its own stack.
One mechanism: the shipped system does not use Emscripten, Binaryen, Asyncify, or JSPI. Suspension and continuation ownership are entirely compiler-native.
One pinned compiler lineage
brintOS LLVM fork
Clang, LLVM libraries, and wasm-ld are built from the same pinned LLVM 22 lineage, including the wasm32-hwjs coroutine, linker, and machine-code changes.
HwjsCoroutinize
An out-of-tree LLVM pass plugin marks the suspend-reachable closure, lowers frames, emits drive and relocation metadata, and refuses unsupported shapes.
hwjs-cc front door
wasmcc, wasmcc-glibc, wasmld, wasmar, and wasmrun make the pinned compiler usable
by ordinary package build systems.
LLVM pass plugins do not have a stable cross-version ABI. The plugin must be built against—and loaded by—the exact LLVM checkout used for compilation. This is an enforced dependency, not a recommendation.
The transform runs once over the merged program
Compilation preserves LLVM bitcode instead of prematurely producing final WebAssembly
objects. At link time, wasmld merges the application, runtime, and libc
bitcode closure, runs the pass once, and only then invokes wasm-ld. That
whole-module view lets the pass trace direct and indirect suspension reachability and
check the final transformed surface.
WASMLD_HWJS=1 wasmcc -O2 -c program.c -o program.o
WASMLD_HWJS=1 wasmcc program.o -o program.wasm
# conceptual link path
bitcode inputs → merged.bc → HwjsCoroutinize → wasm-ld → program.wasmSuspension is a call-graph property
Kernel and runtime interfaces publish blocking behavior with HWJS_SUSPENDS, which carries the hwjs.suspends.v1 annotation. The transform follows that contract through
direct calls, compatible indirect targets, and explicitly described host boundaries.
Every caller that can remain live over a park must participate in the transform.
HWJS_SUSPENDS- Publishes that the function may suspend and seeds the transitive transform wave.
HWJS_NATIVE- Marks an intentionally non-suspending native arm; it is a reviewed carve-out, not an escape hatch.
- Boundary import
- A host-facing leaf whose park is serviced outside the module.
- Refusal
- An attributed build failure when the pass cannot prove a safe transformation.
Selective publication avoids transforming code that cannot suspend. It is also a soundness contract: allowing an unpublished native frame to remain above a park would leave part of the continuation trapped on the engine stack.
From native frames to a flat resume drive
LLVM’s coroutine intrinsics provide the frame-splitting substrate. The pass prepares each suspendable function, makes live values frame-resident, records its resume identity, and lowers it through the pinned CoroSplit implementation. A flat in-wasm trampoline then drives child frames without rebuilding continuation depth as nested JavaScript or engine calls.
The frame also carries typed relocation information for pointers that must remain
valid when a continuation is materialized. Current production fork() copies at zero relocation delta, while the descriptors preserve the stronger ABI and
its validation path.
Park is cheap; reification is explicit
| Operation | State transition | Purpose |
|---|---|---|
| Park | Save the boundary frame’s live state and return to its driver | Release the Worker while work is blocked |
| Resume | Re-enter through the recorded frame and resume point | Continue after a wake or completion |
| Reify | Materialize the live continuation chain in linear memory | Make continuation structure explicit and inspectable |
| Fork | Copy memory and continuation state, then split return values | Create independently advancing parent and child processes |
| Safepoint | Cooperatively yield at a compiler-controlled bounded interval | Support bounded preemption without arbitrary engine interruption |
These operations share one continuation representation. There is no separate “fast” engine suspension whose opaque state later has to be reconciled with Linux process state.
C semantics survive suspension
Real libc and shell code exercise more than straight-line calls. The pass composes
coroutine lowering with native WebAssembly exception handling, setjmp/longjmp across frames, natural returns, indirect
dispatch, dynamic stack allocations, and compiler-inserted safepoints. Where an
address may outlive a park, it is either placed in a frame-owned region with known
pointer structure or refused.
Never guess: an opaque pointer escape, invalid SSA after edge insertion, unknown cross-frame transfer, or output verifier failure stops the link with the function and reason attached.
The coroutine ABI crosses dlopen()
A side module is compiled through the same merged-bitcode discipline and emits a
versioned hwjs.abi manifest. Load-time registries publish suspension,
capability, and function-pointer information that a whole-program static link cannot
know in advance. Cross-module indirect calls use conservative runtime checks and the
same flat drive used inside the main module.
The shared continuation arena, table-index identity, transfer side channel, and
generation-checked setjmp/longjmp markers let frames span
the main executable and loaded objects. ABI or registration mismatches reject the
module instead of falling back to a native frame.
Side-module rule: final native WebAssembly objects cannot bypass the pass. Dynamic modules enter as bitcode, are transformed once, and are linked as PIC only after the coroutine ABI has been emitted and checked.
A normal compiler interface for an unusual target
| Driver or triple | Role |
|---|---|
wasmcc | musl C compiler driver; compiles to bitcode and delegates links to wasmld |
wasmcc-glibc | glibc-aware C compiler driver with Linux headers and sysroot wiring |
wasmld | Bitcode closure, coroutine pass, manifest/provenance checks, and final wasm link |
wasmar | Archive wrapper that preserves bitcode members for the final closure |
wasm32-hwjs-linux-musl | musl compilation target |
wasm32-hwjs-linux-gnu | glibc compilation target |
wasm32-hwjs-unknown | Final link and freestanding kernel target |
The compile/link triple split gives libc the expected Linux preprocessor environment while using the WebAssembly linker’s supported freestanding form for final output. Autotools packages can call the wrappers as normal cross-compilers.
The build proves which pass produced the image
Production builds derive the plugin from the checked-out hwjs-cc revision. A provenance record binds the plugin to that source commit and records that
the tree was clean. Consumers reject stale, dirty, unattested, or ABI-incompatible
plugins before compiling an artifact.
Flipping RED arms
Capability gates deliberately disable one behavior and prove the expected refusal reappears.
Output validity
Function and module verification run on transformed IR; malformed output is never deferred downstream.
Bounded work
Wall-clock and deterministic work budgets convert a runaway analysis into an attributed refusal.
A platform compiler, not a generic wasm post-processor
- The pass and its LLVM fork are one versioned lineage; stock Clang is not interchangeable.
- The transform depends on LLVM IR and whole-program bitcode, not a rewrite of finished wasm bytes.
- libc is a supplied sysroot and bitcode pool, not bundled into
hwjs-cc. - Suspension is cooperative and published; arbitrary JavaScript interruption is not the scheduler.
- Performance cost is concentrated at transformed frames and suspension points; selective publication limits the surface.
- Unsupported shapes fail the build or module load rather than silently changing C or Linux semantics.
Read the implementation and normative contracts
Continue with the Linux architecture port for the kernel-facing publication wave, or the boundary reference for the runtime ABI shared with HardwareJS.