pcbjam/docs/features/wasm-exceptions/README.md
Viktor Vaczi c1ef489cfa feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on)
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy
encoding) across the whole build, keeping Asyncify coroutines working via a
from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the
only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially.

Highlights:
- Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify;
  post-link Asyncify covers every app wasm (not just standalone test wasm).
- Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps
  with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports.
- libcontext fiber entry wired under native exceptions; while-loop main loop +
  currData shim injected into all wx apps.
- Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake
  C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch.
- 3D viewer enabled by default (real raytracer linked, not the stub).
- Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to
  shim-redundancy pins (native-EH stays clean with the legacy shims ablated).
- Fix the asyncify-races quiescence check to not require Asyncify.currData==0:
  under the native-EH per-frame-yield top loop the main stack is asyncify-suspended
  every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer,
  not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad.
- CI: drop the obsolete binaryen_version input/env (the build uses the binaryen
  submodule fork's wasm-opt, not a version download); key the wasm-output cache on
  the binaryen submodule SHA instead.

Bumps the wxwidgets + binaryen submodules to their squashed feature commits.

Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63
Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines;
asyncify 7/7 both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 09:40:26 +02:00

112 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `-fexceptions` vs `-fwasm-exceptions` in KiCad-WASM — research dossier
> **✅ FINALIZED:** native wasm-EH is now the **only** build mode — there is no `-fexceptions` /
> `WX_LEGACY_EH` path, and the 3D viewer builds by default. The migration plan, audit, and spike
> notes below are retained as the historical research/decision record.
> **Status:** research / decision record. A parallel session attempted the migration
> end-to-end and **parked it** on an emscripten-4.0.2 LLVM codegen bug — see
> [`docs/wasm-exceptions-experiment.md`](../../wasm-exceptions-experiment.md) (full
> build-plumbing patch preserved in its appendix; flags
> `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=0`, raw linked
> pcbnew 92 MB and zero `invoke_*`/`dynCall`). This dossier is the research companion:
> mechanism, measurements, audit, and the asyncify catch-block design.
> Authored 2026-06-11/12. Companion to [`docs/features/async/`](../async/) (the Asyncify
> `currData` contention dossier) — this dossier covers the *exception-handling* axis of
> the same machine.
> **UPDATE 2026-06-22 (see [`06-spike-plan.md`](06-spike-plan.md)).** A 5-agent spike refreshed
> this dossier and corrected three things below: (1) **the encoding is resolved to LEGACY**
> (`WASM_LEGACY_EXCEPTIONS=1`) — Asyncify can't consume exnref in any released Binaryen, so the
> "exnref → TryTable variant" fork is closed; the experiment's `=0` was a dead end. (2) **Binaryen
> is not a blocker** — CI/publish already pin `BINARYEN_VERSION=130` (the "v121 locally" note below
> is only the finalize/in-link copy). (3) The long pole is the **emsdk/LLVM compiler bump** for
> parseable legacy wasm-EH + the OCC `br_table` fix, *not* a newer wasm-opt. The phased red-green
> plan lives in 06.
## Why this exists
The whole build is on **`-fexceptions`** (Emscripten's JavaScript-based exception
handling). That choice is not cosmetic — it is the single largest driver of our Asyncify
cost: it forces `env.invoke_*` into `ASYNCIFY_IMPORTS`, which makes nearly the whole call
graph "suspension-capable" and therefore instrumented. We **measured** the consequence on
our own binary: 59% of the raw asyncify tax (64% of the gzipped tax) on pcbnew exists only
because of the invoke machinery. Migrating to native **`-fwasm-exceptions`** would cut the
shipped pcbnew download from **64.5 MB to ~36 MB (44%)** and the module from
**187 MB to ~122 MB (35%)**, plus an unmeasured-but-real runtime win on every try-region
hot path.
The migration is currently blocked by one upstream limitation — Binaryen's Asyncify pass
cannot handle a suspension *inside a catch handler* — and KiCad triggers exactly that
pattern (modal error dialogs from catch blocks) in **85 audited places**. This dossier
records the mechanism, the measurements, the toolchain status, the audit, and a concrete
**fork design (catch-arm hoisting)** that would remove the blocker without refactoring
KiCad at all.
## TL;DR / decision
- **Stay on `-fexceptions` for now.** Asyncify stays under any design (fibers + EM_ASYNC_JS
have no wasm-EH replacement); this is purely about how much it must instrument.
- The prize is measured, not estimated: **44% download, 35% module size** (see 02).
- Binaryen merged *partial* asyncify+wasm-EH support in **v125** (2025-11-19). Our emsdk
bundles **v121** — we don't even have the partial support locally.
- The remaining hole — unwind-from-catch — is fixable with a **bounded new Binaryen pass**
(catch-arm hoisting, ~400800 lines, 12 weeks, genuinely upstreamable; see 05). It is a
*pre-pass* before stock `--asyncify``Asyncify.cpp` itself needs zero changes, so
"fork" overstates it (one added file; `get-wasm-opt.sh` already has the
build-from-source deployment path). It **obsoletes the 85-site KiCad refactor** entirely.
- **Blocker ordering (learned from the parked experiment):** the catch-block limitation
is blocker #2. Blocker #1 is an **LLVM codegen bug in emscripten 4.0.2** (invalid
`br_table` arity in OpenCASCADE code under wasm-EH) — needs an emsdk bump first. And the
experiment had to force the **exnref encoding** (`WASM_LEGACY_EXCEPTIONS=0`, because
4.0.2's legacy encoding output doesn't even parse in Binaryen), which collides with the
fact that Binaryen's asyncify has **zero `TryTable` support**: the encoding choice after
the emsdk bump decides which variant of the catch fix applies (see 03 §experiment, 05).
- Trigger to act: when we are ready to invest ~2 weeks of toolchain work, or if upstream
lands full support on binaryen #4470 first. Until then the Asyncify arbiter work
(docs/features/async/) is the priority — it fixes shipping bugs and is needed either way.
## Document index
| File | Contents |
|---|---|
| [`01-background-two-eh-models.md`](01-background-two-eh-models.md) | How JS-EH (`invoke_*`) and wasm-EH actually work, and the three concrete couplings into our Asyncify machine. |
| [`02-measurements.md`](02-measurements.md) | Our controlled size experiment on pcbnew (methodology + numbers) and the published third-party benchmarks. |
| [`03-toolchain-status.md`](03-toolchain-status.md) | Compatibility matrix: emcc checks, binaryen history (what merged in v125, what didn't), JSPI/fibers, setjmp/longjmp, mixing modes. |
| [`05-asyncify-fork-design.md`](05-asyncify-fork-design.md) | Asyncify.cpp internals, why catch arms are structurally hard, and the catch-arm-hoisting fork design with limits and effort. |
| [`06-spike-plan.md`](06-spike-plan.md) | **(2026-06-22)** Refreshed findings + the phased red-green spike plan; supersedes the encoding/Binaryen-version framing above. |
| [`07-spike-results-and-opinion.md`](07-spike-results-and-opinion.md) | **(2026-06-22)** Toy-spike results: asyncify + legacy-wasm-EH works; the `HoistCppCatches` Binaryen pass flips suspend-in-catch green on all 3 engines; go/no-go opinion. |
| [`08-wx-app-render-rootcause.md`](08-wx-app-render-rootcause.md) | Why a native-EH wx app rendered blank: the `set_main_loop` `"unwind"` throw caught by native-EH `catch_all` cleanup pads tore down the main frame. |
| [`09-event-loop-deparking-plan.md`](09-event-loop-deparking-plan.md) | The EH-agnostic main-loop rework (de-park → per-frame-yield `while`-loop) fixing the blank render + the coroutine/menu regressions. |
| [`10-pthreads-native-eh.md`](10-pthreads-native-eh.md) | **(2026-06-24)** Native-EH × pthreads: the main-thread thread-spawn regression (`invalid state: 1` / re-entrant `main()`); the pool pattern survives; the KiCad raw→pool refactor plan + the test gap. |
## Relationship to docs/features/async/
Independent axes of the same machine. The async dossier is about *correctness* (one global
`Asyncify.currData` shared by overlapping suspensions → crash/hang); this dossier is about
*cost* (how much code Asyncify instruments). Fixing one does not fix the other. Sequencing:
async arbiter first (shipping bugs), wasm-EH migration second (size/speed), and the
migration plan below assumes the arbiter exists.
## Migration plan (when triggered)
0. Resume the parked experiment (`docs/wasm-exceptions-experiment.md`): bump emsdk past
the 4.0.2 LLVM `br_table` bug, `git apply` its appendix patch (`KICAD_WASM_EH=1`
gated), full clean deps rebuild (stamp-skip gotcha: stale sjlj objects in cairo etc.).
Then decide the EH encoding: if newer LLVM emits parseable **legacy** encoding, the
catch-arm-hoisting pre-pass (05) applies; if **exnref** stays forced, asyncify needs
`TryTable` support + exnref spilling instead (05 §new-EH variant).
1. Newer Binaryen for the post-link step only: `scripts/common/get-wasm-opt.sh` already
abstracts the binary — point it at a ≥ v125 build carrying the hoisting patch (05).
2. Validate partial support first: rebuild one app `-fwasm-exceptions` + asyncify-asserts,
run the e2e suites; the asserts tripwire makes any missed unwind-from-catch a
deterministic trap.
3. Uniform flag flip: `-fexceptions``-fwasm-exceptions` in
`scripts/build-wxuniversal-wasm.sh:141-142`, `scripts/kicad/build-kicad-target.sh`
(lines ~203/207/211/214), `tests/apps/Makefile.wasm` (all occurrences) — plus
`-sSUPPORT_LONGJMP=wasm` (default with wasm-EH; the `emscripten` flavor is a hard error).
4. Drop `env.invoke_*` from `ASYNCIFY_IMPORTS` in `scripts/common/apply-asyncify.sh:33`.
5. Expect to delete/shrink shim machinery that exists only for the invoke world
(`inject-dyncall-shims.sh` phases 12) — verify, don't assume.
6. Keep `catch_audit.py` as a CI gate only if shipping *without* the fork (i.e., the
hand-refactor path); with the fork it is informational.