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>
75 lines
3.4 KiB
Markdown
75 lines
3.4 KiB
Markdown
# 04 — The KiCad catch-block audit
|
||
|
||
## Why an audit
|
||
|
||
Binaryen's asyncify cannot (yet) handle a suspension that begins while execution is inside
|
||
a wasm catch handler (see 05). Any C++ `catch` whose handler (directly or transitively)
|
||
opens a modal dialog, touches the async clipboard, etc., is therefore illegal under
|
||
`-fwasm-exceptions` + ASYNCIFY=1 today. KiCad's standard error pattern is exactly that:
|
||
|
||
```cpp
|
||
catch( const IO_ERROR& ioe )
|
||
{
|
||
DisplayErrorMessage( this, ioe.What() ); // → ShowModal → Asyncify suspension
|
||
}
|
||
```
|
||
|
||
## Method
|
||
|
||
[`catch_audit.py`](catch_audit.py): walks `kicad/**/*.cpp` (excluding `thirdparty/`, `qa/`),
|
||
extracts every catch block with real brace matching (string/comment aware), and classifies
|
||
each handler body:
|
||
|
||
- **direct_suspend** — contains a known suspending call (`DisplayError[Message]`,
|
||
`DisplayInfoMessage`, `wxMessageBox`, `ShowModal`, `ShowQuasiModal`, `KIDIALOG`,
|
||
`IsOK(`, clipboard ops, `wxFileDialog`, …).
|
||
- **infobar** — only `ShowInfoBar*` (non-modal, non-suspending).
|
||
- **trivial** — only rethrow / capture / format / logging. Note `wxLogError` is *safe*:
|
||
its GUI display is deferred to the idle-time log flush, outside any catch.
|
||
`PGM_BASE::HandleException` (`common/pgm_base.cpp:805`) was manually verified — it only
|
||
`wxLogError`s → benign.
|
||
- **needs_review** — calls functions not classifiable as benign; requires a transitive look.
|
||
|
||
Full output incl. all site locations: [`audit-results.txt`](audit-results.txt).
|
||
Re-run: `python3 catch_audit.py` (path to the kicad tree is hardcoded at the top).
|
||
|
||
## Results (2026-06-10, kicad @ wasm-port head)
|
||
|
||
| Category | Count |
|
||
|---|---|
|
||
| **direct_suspend** | **85** |
|
||
| needs_review | 93 (tail looks mostly benign — accessors/file ops; expect ~10–20 to become refactors on inspection) |
|
||
| trivial/safe | 458 |
|
||
| infobar-only | 0 |
|
||
| **total** | **636** |
|
||
|
||
Per app (direct/review): eeschema 37/17, pcbnew 32/29, common 9/35, cvpcb 3/1, rest
|
||
scattered. Concentrated exactly on the file-load error paths the e2e tests exercise:
|
||
`pcbnew/files.cpp`, `eeschema/files-io.cpp`, `footprint_libraries_utils.cpp`,
|
||
`symbol_library_manager.cpp`, the design-block utils.
|
||
|
||
wxWidgets adds essentially nothing (zero catch-with-dialog sites in its own code).
|
||
|
||
## The hand-refactor option (superseded by the fork, kept for the record)
|
||
|
||
Mechanical hoist per site:
|
||
|
||
```cpp
|
||
wxString err;
|
||
try { ... }
|
||
catch( const IO_ERROR& ioe ) { err = ioe.What(); } // capture only
|
||
if( !err.IsEmpty() ) DisplayErrorMessage( this, err ); // suspend OUTSIDE the catch
|
||
```
|
||
|
||
Effort: 85 hoists (~15–30 min each; error-UX paths with near-zero test coverage) ≈ 3–5
|
||
dev-days; 93 reviews ≈ 2–3 dev-days; destructor-during-unwind audit ≈ half day (cleanup
|
||
pads = `catch_all`; dialogs from destructors expected zero); **plus** permanent
|
||
upstream-sync policing (every KiCad merge adds new `catch { DisplayError }` sites —
|
||
`catch_audit.py` as a CI gate automates detection), **plus** the fork-divergence cost
|
||
against the "stay close to upstream" policy. Total ~2–3 weeks one-time + maintenance tax,
|
||
with hard-crash failure modes for any missed transitive site (asyncify asserts mode traps
|
||
deterministically — good in CI, fatal in production).
|
||
|
||
**Verdict:** with the catch-arm-hoisting fork (05) this entire refactor becomes
|
||
unnecessary — all 85 direct sites are C++ catches, which the fork makes legal. Only
|
||
suspend-inside-`catch_all`-cleanup remains forbidden, which KiCad does not do.
|