pcbjam/docs/features/wasm-exceptions/01-background-two-eh-models.md

76 lines
4.3 KiB
Markdown
Raw Normal View History

# 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++ `throw` compiles 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 an **`invoke_<sig>` wrapper** — a
JS function that does `stackSave()`, calls back into the wasm table inside a JS
`try/catch`, and on catch runs `stackRestore(sp)` + `_setThrew(1)` so the wasm side can
run destructors and re-dispatch. The generated `pcbnew.js` contains **1,049 `invoke_`
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`/`throw` are **wasm instructions** (the exception-handling proposal;
standardized, supported in all modern browsers — note the legacy encoding vs the newer
`exnref`/`try_table` encoding, controlled by Emscripten's `WASM_LEGACY_EXCEPTIONS`,
default legacy as of this writing).
- No `invoke_*` wrappers exist at all. Throws never enter JS. The non-throwing path of a
`try` is ~zero-cost in modern engines. `SUPPORT_LONGJMP=wasm` rides 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 12 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.