Two research dossiers under docs/features: - async/: Asyncify currData contention — the clipboard crash and post-idle tool hang as one disease (single global suspension slot, three producers), asyncify internals walkthroughs, prior art, arbiter designs A/B. - wasm-exceptions/: -fexceptions vs -fwasm-exceptions — measured invoke-driven share of the asyncify tax on pcbnew (59% raw / 64% gzip; download 64.5->36 MB if migrated), KiCad dialog-in-catch audit (85 direct / 93 review of 636, catch_audit.py included), binaryen toolchain status (partial EH support since v125; TryTable unsupported), and a catch-arm-hoisting pre-pass design that would obsolete the KiCad-side refactor. Cross-linked with the parked KICAD_WASM_EH end-to-end experiment (docs/wasm-exceptions-experiment.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4.3 KiB
01 — The two exception-handling models, and where ours bleeds into Asyncify
What we build with today
Every layer is on -fexceptions = Emscripten's JavaScript-based EH:
scripts/build-wxuniversal-wasm.sh:141-142(wxWidgets CFLAGS/CXXFLAGS)scripts/kicad/build-kicad-target.sh:203/207/211/214(KiCad, with the comment at:196: "-fexceptions is required because wxWidgets is built with exceptions enabled")tests/apps/Makefile.wasm(standalone test apps)
No SUPPORT_LONGJMP is set anywhere → default emscripten flavor (JS-based setjmp/longjmp
through the same machinery).
Model 1 — JS-based EH (-fexceptions), what actually happens
- A C++
throwcompiles to a call out to JS (__cxa_throw), which throws a JavaScript exception carrying an i32 pointer (the C++ exception object in linear memory). - Every call that may need cleanup-during-unwind (anything inside a
try, or in a function with destructors on the unwind path) is routed through aninvoke_<sig>wrapper — a JS function that doesstackSave(), calls back into the wasm table inside a JStry/catch, and on catch runsstackRestore(sp)+_setThrew(1)so the wasm side can run destructors and re-dispatch. The generatedpcbnew.jscontains 1,049invoke_references. - Consequence: a "C++ call stack" is really a wasm→JS→wasm→JS sandwich, and C++ exceptions physically travel through JS frames.
Model 2 — native wasm-EH (-fwasm-exceptions)
try/catch/throware wasm instructions (the exception-handling proposal; standardized, supported in all modern browsers — note the legacy encoding vs the newerexnref/try_tableencoding, controlled by Emscripten'sWASM_LEGACY_EXCEPTIONS, default legacy as of this writing).- No
invoke_*wrappers exist at all. Throws never enter JS. The non-throwing path of atryis ~zero-cost in modern engines.SUPPORT_LONGJMP=wasmrides the same instructions.
The three concrete couplings into our Asyncify machine
1. JS-EH is why Asyncify instruments almost everything.
scripts/common/apply-asyncify.sh:33:
ASYNCIFY_IMPORTS="env.invoke_*,env.__asyncjs__*,env.emscripten_fiber_swap"
env.invoke_* is listed because suspensions genuinely flow through invokes (a modal opened
inside a parser's try, a tool yielding inside a try-scope). But every invoke is an import
call, so marking them suspension-capable breaks the static call graph at ~1,049 places and
the "can this suspend?" analysis answers yes for most of KiCad → near-total
instrumentation. This is the root of: the 338 MB pre-O2 binary, the V8 per-function-locals
silent-stall family (see memory/chrome-asyncify-rewind-crash), the ASYNCIFY_REMOVE
list, and the mandatory post-asyncify wasm-opt -O2 pass. Under wasm-EH the imports list
shrinks to env.__asyncjs__*,env.emscripten_fiber_swap and instrumentation collapses to
the genuine suspend chains.
2. JS-EH is why our wasm stacks have JS frames in the middle.
Every asyncify unwind must early-return through, and every rewind must re-establish, the
invoke sandwich layers. The async dossier's test matrix calls out "-fexceptions invoke
wrappers" as a failure axis and vcall_repro (invoke_vi→dynCall_vi) as the smoking
gun. Phases 1–2 of scripts/common/inject-dyncall-shims.sh (per-signature dynCall
bindings, empty-callback fixes) exist substantially to keep this machinery alive after
asyncify+O2.
3. JS-EH is why C++ throws can interact with the asyncify JS runtime at all.
Under JS-EH a C++ exception is a JS exception: it can tear through
Fibers.trampoline()'s do/while exactly like the throw "unwind" park does (the §3c
trampoline self-heal in inject-dyncall-shims.sh guards that), and the invoke wrapper's
catch performs stackRestore(sp) — a stack-pointer write from a JS frame — while fibers
juggle the same stack pointer. Under wasm-EH this entire interaction class disappears.
What wasm-EH does NOT change
The Asyncify correctness problems (single global Asyncify.currData, the crash/hang
family in docs/features/async/) are identical under either EH mode. Modals, clipboard, fonts,
and fiber swaps still suspend through the same register. wasm-EH is a de-bloating /
de-fragilizing move, not a concurrency fix. The arbiter is needed either way.