Section 03 / compiler substrate

hwjs-cc

A pinned LLVM/Clang toolchain and whole-program transformation that turn suspendable WebAssembly call stacks into explicit, resumable state in linear memory.

TOOLCHAIN IDENTITY
Compiler
brintOS LLVM 22 fork
Transform
HwjsCoroutinize
Link driver
wasmld
Default drive
flat trampoline
Suspension
instrumentation-native
Output
wasm32-hwjs
03.01 / PURPOSE

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.

03.02 / COMPONENTS

One pinned compiler lineage

LLVM

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.

PASS

HwjsCoroutinize

An out-of-tree LLVM pass plugin marks the suspend-reachable closure, lowers frames, emits drive and relocation metadata, and refuses unsupported shapes.

DRIVERS

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.

03.03 / PIPELINE

The transform runs once over the merged program

1 / COMPILEClangC or C++ → LLVM bitcode
2 / CLOSEllvm-linkProgram + runtime + libc bitcode
3 / LOWERHwjsCoroutinizeOne merged-module pass run
4 / EMITwasm-ldFinal executable or side module

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.wasm
03.04 / PUBLICATION

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

03.05 / TRANSFORMATION

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.

NORMALExecuteRun until return, descend, or park
DESCENDDrive childMove to the published child frame
PARKYieldReturn control with continuation state intact
RETURNUnwindResume the parent with the result

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.

03.06 / OPERATIONS

Park is cheap; reification is explicit

OperationState transitionPurpose
ParkSave the boundary frame’s live state and return to its driverRelease the Worker while work is blocked
ResumeRe-enter through the recorded frame and resume pointContinue after a wake or completion
ReifyMaterialize the live continuation chain in linear memoryMake continuation structure explicit and inspectable
ForkCopy memory and continuation state, then split return valuesCreate independently advancing parent and child processes
SafepointCooperatively yield at a compiler-controlled bounded intervalSupport 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.

03.07 / CONTROL-FLOW COVERAGE

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.

03.08 / DYNAMIC MODULES

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.

03.09 / DRIVERS & TARGETS

A normal compiler interface for an unusual target

Driver or tripleRole
wasmccmusl C compiler driver; compiles to bitcode and delegates links to wasmld
wasmcc-glibcglibc-aware C compiler driver with Linux headers and sysroot wiring
wasmldBitcode closure, coroutine pass, manifest/provenance checks, and final wasm link
wasmarArchive wrapper that preserves bitcode members for the final closure
wasm32-hwjs-linux-muslmusl compilation target
wasm32-hwjs-linux-gnuglibc compilation target
wasm32-hwjs-unknownFinal 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.

03.10 / ASSURANCE

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.

GATE-01

Flipping RED arms

Capability gates deliberately disable one behavior and prove the expected refusal reappears.

GATE-02

Output validity

Function and module verification run on transformed IR; malformed output is never deferred downstream.

GATE-03

Bounded work

Wall-clock and deterministic work budgets convert a runaway analysis into an attributed refusal.

03.11 / DESIGN BOUNDARIES

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.
03.12 / SOURCE INDEX

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.