Skip to content

proposal: patched Binaryen as the WebAssembly build baseline #2632

Open

Description

@cpunion

This proposal updates the WebAssembly profile proposal. The J32/J64/W32 data model is unchanged.

Implementation status (2026-10-06)

Updated against main be669acd80cc9ea4019b8660d75cf00493a58a66. The core browser and WASI runtime work, including explicit WASI thread retirement in #2735, has merged. This proposal is not fully complete: cross-worker public JS semantics are implemented in #2738 but remain in review; the existing browser filesystem/GC failure (#2737) has a fix in that contribution with complete local worker acceptance passing, while contribution CI and full compatibility qualification remain open.

Area Delivered on main Remaining work
Toolchain Emscripten 6.0.8 and patched Binaryen llgo-v132.3, pinned and verified; #2652 Qualify future SDK/Binaryen upgrades before changing pins
Browser workers Bounded Fiber/Asyncify worker pool, worker-local scheduling, callbacks and STW linear GC; #2661 Shared worker-zero public JS realm in #2738 (in review); resolve #2737 and qualify compatibility before changing defaults
WASI pthreads/GC Thread startup, stack discovery, STW collector, collector stability and explicit thread retirement; #2669, #2695 and #2735 Keep the retirement stress gate and complete pinned-runner compatibility qualification
WASI profile Threaded W32 is the default; the single-thread/Asyncify WASI backend was removed in #2697 Continue full std/GOROOT qualification on the pinned runner
Filesystem Browser host and worker filesystem integration; #2539 and #2696 Track compatibility failures through the broader audit
Debugging Final-Wasm DWARF verification, embedded/external DWARF and debug-session tooling; #2660, #2662, #2269 and #2713 Re-run debug regressions for toolchain upgrades; do not infer every target/debugger combination from focused tests
Exception boundaries Reproducible C++ EH comparison and contained C++-to-Go status translation; #2654 and #2695 Preserve browser catch/callback/suspension compatibility; full-module encoding changes need separate qualification
Size foundations Artifact-based -size, reflection method correctness, C-only reachability pruning and WASI LTO fixes; #2719–#2721 The broader TinyGo-size target in #2679 still requires implementation and measured ablations
WASI runner LLGo-patched Wasmer llgo-v7.5.0.1, pinned in #2731 Full compatibility reports are separate acceptance evidence, not implied by focused CI

Upstream Binaryen #8964 merged on 2026-10-01. This does not automatically replace the tested version-132 backport or change LLGo's release pin. The former WAMR-first qualification plan is superseded by the pinned Wasmer baseline; old WAMR results remain historical evidence, not the current acceptance gate.

Decision

LLGo uses a pinned, LLGo-patched Binaryen as the supported WebAssembly build toolchain. The browser profile retains Emscripten, Fiber and Asyncify for C/C++ and JavaScript compatibility. Removing Binaryen is not a scheduled migration or acceptance criterion. A custom wasmresume stackless ABI, or LLVM CoroSplit, remains optional Wasm-only research until end-to-end tests demonstrate both a material benefit and a compatible C/JS boundary.

Target Execution and host integration Binaryen use
Browser J32/J64 Emscripten Fiber/Asyncify; single-worker default, opt-in bounded Web Worker pool Emscripten uses the pinned tools through EM_BINARYEN_ROOT
WASI Preview 1 W32 wasi-libc pthread stacks, shared memory and WASI threads; pinned LLGo Wasmer runner No Asyncify; LLVM emits standard Wasm EH directly
Other platforms Existing pthread/thread runtime; one goroutine creates one host thread None

The browser creates a bounded worker pool, not a worker per goroutine. WASI and native pthread backends retain one goroutine per host thread; M/G separation on those backends is outside this proposal.

Why keep the patched Binaryen baseline

Binaryen performs Asyncify instrumentation, post-link optimization and, where explicitly selected, EH translation. These transformations can invalidate LLVM's DWARF scope ranges. The #8964 fix was backported to Binaryen version_132, matching Emscripten 6.0.8. A focused LLGo browser debug build had 125 parent-containment and 24 overlap errors with stock 132 and none with the patched build; both artifacts ran in Node. Subsequent final-artifact/debug regression tests remain the qualification gate.

Upstream integration improves future upgrade options. It does not justify silently using a different Binaryen version with the pinned Emscripten SDK. The fork remains the reproducible build contract until a reviewed upgrade passes the same runtime, C/C++, EH and DWARF tests.

Keeping the C stack preserves the current C ecosystem: SDK, sysroot, linker, ports and JavaScript glue. The current ABI does not require users to annotate every C function as blocking or asynchronous. A blocking C call still occupies its worker or pthread; alternatives must account for that boundary before replacing it.

Fork, branch and release policy

The toolchain source is xgo-dev/binaryen, a fork of WebAssembly/binaryen:

  • main is a clean upstream mirror and contains no LLGo commits. Updates never merge into llgo automatically.
  • llgo is the default branch and sole source branch for LLGo releases. It started from upstream version_132 and carries the reviewed DWARF backport and build/test changes.
  • Changes to llgo go through contribution PRs from cpunion/binaryen; no direct pushes to the destination branch.
  • Release tags use llgo-v132.N, identify reviewed commits and record the upstream base, patch provenance, Emscripten version, checksums and license/notice files. Upgrades are reviewed, tested changes rather than automatic merges.
  • PR CI builds and packages the complete tool suite, including license/third-party notices, on supported Linux/macOS/Windows hosts. Only uploading the release artifacts is skipped for PR builds; packaging must not first run during release.

LLGo pins an exact release and checksum. Browser builds set EM_BINARYEN_ROOT to the extracted release root; Emscripten needs both wasm-emscripten-finalize and wasm-opt. LLGo's standalone post-link path uses WASMOPT. Selecting only WASMOPT does not select the browser toolchain. Diagnostics must identify the selected paths and versions. Binaryen is Apache-2.0; distributions include its license, third-party notices and patch provenance.

WASI CI/local setup separately pins xgo-dev/wasmer llgo-v7.5.0.1 and verified archive digests. This fork contains LLGo's required thread/exception and cache fixes. Stock Wasmer 7.5.0 is not interchangeable acceptance evidence.

Runtime and exception contract

Browser. Single-worker Fiber/Asyncify remains the default. LLGO_WASM_WORKERS=2 through 16 enables the bounded pool; 1 retains single-worker behavior. Each goroutine stays on its assigned worker, preserving Fiber/C-stack/TLS ownership. Allocation, root publication, cross-worker wakes, callbacks and STW GC are implemented.

#2738 replaces the earlier descendant-affinity/independent-closure restriction with a shared Go-main realm for public syscall/js values and functions. Once that contribution is merged, wasmworkers.GoIndependent may capture public JS values; raw Emscripten handles and C TLS remain local to their physical worker. The contribution does not move goroutines or add UI-thread DOM access. Complete review, resolve #2737 and qualify compatibility before changing the opt-in default. Work stealing is not required for this first pool.

WASI. W32 uses WASI threads with GC by default; -tags nogc remains supported. It imports shared env.memory, Preview 1 APIs and wasi.thread-spawn. Main now retires threads through wasi-libc after #2735, fixing #2727. The path preserves C cleanup handlers, TLS destructors and join semantics without WASIX thread_exit; repeated retirement has a bounded-memory stress gate. This gate must remain in CI.

The collector pauses registered threads and performs serial mark/sweep. A foreign C call that cannot acknowledge a safepoint causes that collection attempt to be skipped; the collector must not scan an actively mutating foreign stack. Threaded application support does not mean concurrent/parallel GC or a multiplexing WASI scheduler.

Exceptions. Keep the currently qualified browser JavaScript SjLj/Asyncify path and W32 direct standard Wasm EH. C++ exceptions remain contained in C++ wrappers and return a C ABI status for Go translation; foreign exceptions must not unwind through suspended Go frames. The merged comparison tests qualify the tested boundaries, not a full-module exnref default switch.

#2734 is draft because switching browser SjLj to native Wasm EH loses a previously working C++ catch → Go callback → Sleep/resume sequence. Negative tests detect that incompatibility but do not resolve it. Resolve the behavior or explicitly revisit the ABI before changing defaults. The SIMD/JS-wrapper bridge in #2722 has merged. Its integration is included in the latest-main qualification; it does not resolve the native-EH suspension incompatibility.

Later directions. WasmGC, W64, JSPI, WASI Preview 2/components and alternative continuation storage remain separate proposals. The linear-memory heap remains the supported baseline. Build/cache identities must include memory width, EH, thread capability and host ABI so incompatible objects are rejected.

Remaining implementation and acceptance

PR #2735 implements the WASI thread-exit gate below from current main. Each stress invocation now includes 32 warmup plus 4096 measured Go workers and 1024 C pthreads (eight concurrent). CI repeats all four GC/nogc × return/Goexit combinations three times. All 40 local stress invocations (10 per combination) pass, totaling 165,120 Go and 40,960 C thread retirements with linear memory staying at 64 MiB. Expanded O0/O2 and GC Thin/Full LTO checks, debug diagnostics tests and the complete WASI acceptance script pass on pinned Wasmer. It merged on 2026-10-05 with 97 successful checks. This completes the thread-exit implementation gate; broader compatibility qualification is still required.

  1. WASI retirement regression gate (implemented in runtime: retire WASI threads through libc on Goexit #2735, fixing runtime: WASI Goexit exhausts thread resources after sequential goroutine exits #2727): retain Go defers, runtime context cleanup, C cleanup handlers, libc TLS destructors and detached/joinable thread retirement. Compare ordinary return with at least 400 sequential Goexit calls under both GC and nogc; assert bounded linear-memory use rather than only process success. Keep initial/main Goexit, panic/recover, C longjmp, threaded GC and timers passing. Verify the fixed path no longer needs WASIX thread_exit.

  2. Browser JavaScript cross-worker semantics (runtime/wasm: add bounded Emscripten workers and multi-worker GC #2495): wasm: support cross-worker syscall/js values and callbacks #2738 implements public js.Value/js.Func sharing by routing operations to Go main's worker-zero realm. Each remote G reuses its own executor, preserving identity, callbacks, exception values, GC roots and caller-worker ownership. Node and Chrome JS acceptance passes for 2/4 workers and Memory32/64. The filesystem GC stress failure in wasm: browser filesystem callbacks can restore another Fiber stack under GC stress #2737 was also reproduced on main; its callback-registry and callback scheduling fixes now pass the complete local worker acceptance suite in the same contribution, including single/multi-worker filesystem checks in both memory widths. Raw Emscripten handles/C TLS remain worker-local; this does not add UI-thread DOM access. Complete review and compatibility qualification before changing defaults.

  3. Compatibility qualification: main GoJS GOROOT qualification passed all 16 shards for Go 1.26/1.27 and the strict 32-report summary; main full std qualification passed all 28 jobs. CI PR #34 adds explicit Emscripten worker counts and Node/Chrome worker acceptance; all 19 contribution checks pass. The complete browser job has a 45-minute budget, retaining the original individual deadlines and assertions.

    #2738, at ab4626f466084b20d04956c7b5ba8dc92c18f46a, includes in-place GC queue waits, direct contiguous-heap lookup, bounded allocator handoff, zero-span scanning and bounded/aligned metadata batching. Mixed/partial states retain individual handling and freed storage is cleared. The browser Memory64 filesystem stress failure exposed payload/request/response buffers held only by numeric JS addresses; explicit rooted allocations now retain each buffer until the proxy's finally cleanup. A deterministic collection-between-allocations regression reproduces the old data loss and verifies both memory widths; actual Chrome Memory32/64 GC stress passes.

    #2743, stacked on wasm: support cross-worker syscall/js values and callbacks #2738 at 218907942ded28f08460cd6e0c9f270d863d2dfd, uses compiler-generated static reflection entries for JS providers; native/C libffi retains its backend. It adds complete test/go worker acceptance with the original 60-second limit. Both contributions are rebased on main be669acd8. Full Memory32 passes locally around 20–21 seconds; hosted performance remains an acceptance gate. Metadata/segment tests cover aligned and partial boundaries, retained/freed tails, counters and cleared storage. The wide-graph overflow regression passes in both browser widths; threaded GC passes on WAMR and pinned Wasmer, with Wasmer remaining the W32 qualification gate.

    New full std/worker qualification and new Go 1.26/1.27 Wasm GOROOT qualification pin the complete new compiler SHA 218907942ded28f08460cd6e0c9f270d863d2dfd; both are pending. The previous 2861a8d27 GoJS GOROOT run passed all 16 shards, with its strict summary still pending: https://github.com/cpunion/llgo-compat-ci/actions/runs/37438152597. Its incomplete std run was cancelled after the source fix to release capacity. Earlier qualification exposed the 60-second Memory32 GC alarm and filesystem data loss; those failures are not passing evidence. The host GC source-extraction test also lacked new constants, now fixed and tested. No failed CI job was rerun or deadline increased. Current contribution CI and full compatibility qualification remain open; focused/local passes do not establish completion. Windows MSYS2 trust setup and restored GOROOT compiler/resource fixes have merged in ci: require package trust before installing Windows GDB #2740/fix: handle failures exposed by full GOROOT coverage #2741.

  4. EH compatibility: qualify the merged SIMD bridge together with the remaining browser exception work without losing catch/callback/suspension behavior. Any encoding/default change must compare Go panic/recover/Goexit, C++ catch, C/Go status translation, callbacks, suspension, LTO, GC and final DWARF across J32/J64/W32.

  5. Measure and optimize: record per-G storage, peak heap, allocation throughput, GC pauses and bounded-worker scaling. Include request/completion-channel allocations for remote JS operations and callback-only O(n) runqueue scans; ordinary remote property operations do not create a synchronous Go callback event. The static reflection fix costs about 62 KB Wasm on the minimal workload (JS drops about 13 KB), and about 2.45 MB on the large test/go module; reduce bridge/metadata reachability costs using measured ablations. Implement the remaining proposal: align wasm and embedded binary sizes with TinyGo through whole-program optimization #2679 size plan against TinyGo with measured cprintf/println/fmtprintf ablations; do not treat the already merged size-report/LTO fixes as completion of that target. Code splitting/external PCLN (proposal: optimize WebAssembly size and support module splitting #2480) is a separate deployment optimization from external DWARF.

  6. Optional continuation research: compare custom stackless ABI/LLVM CoroSplit with the supported backend on size, memory, latency, debugging, C/Go/JS callbacks, EH and GC. Select it only for Wasm after a demonstrated benefit. Other platforms continue to use pthread/thread.

Completed acceptance foundations include release pin/checksum verification, final-Wasm/DWARF checks, bounded browser workers with GC, threaded WASI GC, removal of the old WASI profile, filesystem integration and debug tooling. They must remain in CI as regressions while the remaining gates above are completed.

Tradeoff and revisit trigger

The browser allocates a Fiber stack and an Asyncify buffer per G (128 KiB each by default on wasm32), approximately 256 KiB/G before other state. Measure realistic workloads before reducing, pooling or replacing continuation storage. A stackless replacement becomes a delivery plan only after it passes the C/JS/EH/GC/debug suite and materially improves the measured limit. The patched Binaryen remains the supported build contract.

Activity

  1. changed the title [-]proposal: WebAssembly resumable execution architecture[/-] [+]proposal: patched Binaryen as the WebAssembly build baseline[/+] on Sep 23, 2026
  2. cpunion commented on Sep 23, 2026

    @cpunion
    CollaboratorAuthor

    Implementation status (2026-09-23):

    Next review gates: merge the Binaryen PRs through the fork workflow, publish and verify llgo-v132.N, then submit the exact LLGo artifact pin/checksums; independently complete #2656 and #2495 before changing supported runtime defaults. The native pthread backend and current single-thread WASI path are unchanged.

  3. cpunion commented on Sep 23, 2026

    @cpunion
    CollaboratorAuthor

    EH boundary update for #2654: the earlier Go-to-C++ link failure was caused by the exploratory fixture layout. A .cpp file in the Go package directory was compiled both by automatic C++ source collection and LLGoFiles; the linker selected the object without the EH compiler flag. Moving it under _wrap/ removes the duplicate. With Emscripten JS exception mode, C++ now catches internally, returns a C ABI status, and Go translates and recovers it at O0 and O2 in Node. The test is part of #2654. It still does not permit a C++ exception to unwind through Go, nor select direct exnref for the full browser link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions