pcbjam/docs/features/editor-unification/04-part2-single-app-merge.md
Istvan Matejcsok c6716e6b60 feat(wasm): unify editor builds — footprint/symbol editors via runtime --frame
The footprint and symbol editors are no longer separate WASM bundles: the frontend loads the parent pcbnew/eeschema bundle and passes --frame=fpedit / --frame=symedit (TOOL_BUNDLE + TOOL_FRAME -> Module.arguments in boot). Drops the two duplicate build+deploy targets and their wrapper scripts + vestigial embind; adds low-level harnesses (footprint_editor.html, symbol_editor.html) and a runtime-frame spec. Bumps the kicad submodule to the runtime --frame launcher.

The frame-runtime spec is listed in PCBNEW_FAMILY_SPECS so CI routes it to the chromium-ci (V8) project — its footprint case boots the pcbnew module, which OOMs SpiderMonkey on x86 CI. Includes the editor-unification dossier (research docs 01-04 + the as-built implementation record 05).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:56:02 +02:00

142 lines
8.7 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.

# 04 — Part 2: fuse the editors into one app
> **Verdict: feasible and well-bounded, but moderate work — and a product decision, not a dedup
> win.** Merging "the four editors" means linking the **two engines** (pcbnew kiface + eeschema
> kiface) into one WASM image and picking the view at runtime. The collision surface is small
> and concentrated; the real cost is bundle size. Prereqs:
> [`02-kiface-architecture.md`](02-kiface-architecture.md), [`03`](03-part1-library-editor-unification.md).
## What "the four" actually is
Board + Footprint + Schematic + Symbol = `FACE_PCB` (pcbnew kiface) + `FACE_SCH` (eeschema
kiface) = **two engines in one static image**. Extending to all 7 GUI tools adds `FACE_GERBVIEW`,
`FACE_PL_EDITOR`, `FACE_PCB_CALCULATOR` = 5 kifaces — same mechanism, more faces.
This is fundamentally different from Part 1. Part 1 is "one engine, many frames" — already
supported, you just expose the selector. Part 2 is "many engines, one image," which has **no
existing template**: upstream gets multi-kiface only by loading DLLs at runtime, the WASM build
has no `dlopen`, the `kicad` project manager isn't built for WASM (`kicad/CMakeLists.txt:113`),
and the WASM `KIWAY` explicitly assumes exactly one kiface (`kiway.cpp:223-258`). So Part 2 is a
real porting task, not a flag flip.
## The symbol-collision surface (when two kifaces link into one image)
From most to least trouble:
### 1. `Kiface()` — the load-bearing item (moderate)
Each kiface defines a global accessor `KIFACE_BASE& Kiface()` (declared
`include/kiface_base.h:134`; defined `pcbnew.cpp:600`, `eeschema.cpp:452`,
`gerbview.cpp:152`, `pl_editor.cpp:141`, `pcb_calculator.cpp:76`). Linking two kifaces gives two
definitions of `Kiface()` — a duplicate symbol.
Worse, it is not just a name clash; it is a **semantic** one. `Kiface()` is called ~97 times
(pcbnew 48 calls / 21 files; eeschema 49 / 20 files), **including 5 files in shared `common/`**
that are compiled **once** into libkicommon and linked into every app:
- `common/eda_base_frame.cpp` (e.g. `:1274 return Kiface().KifaceSettings();`, `:1280`, `:1286`)
- `common/gestfich.cpp`
- `common/design_block_tree_model_adapter.cpp:49`
- `common/dialogs/dialog_color_picker.cpp`
- `common/tool/common_control.cpp`
That shared code calls `Kiface()` expecting "the one active editor's kiface" — but with two
kifaces linked, it can't statically know whether the active frame is PCB or schematic. So
`Kiface()` must become a **runtime dispatch on the active editor** (a "current active kiface"
pointer keyed off the active frame's `FACE_T`). It's mostly process/editor-level config —
`KifaceSettings()`, `KifaceSearch()`, `GetHelpFileName()`, `IsSingle()` — so a single dispatching
`Kiface()` backed by an active-editor pointer is feasible, but it is a behavioral change touching
~100 call sites including shared code. **This is what lifts Part 2 from "small" to "moderate."**
> ⚠️ **`--allow-multiple-definition` is not a fix here.** The static kiface link already passes
> `LINKER:--allow-multiple-definition` (see `../symbol-editor/0001-symbol-editor-port.md`). That
> would let two `Kiface()` definitions *link* — by silently taking the first — which means shared
> `common/` code would always resolve to (say) pcbnew's kiface even while the schematic editor is
> active. That is the bug, not the fix. `Kiface()` needs real per-active-editor dispatch.
### 2. `KIFACE_GETTER` / `KIFACE_1` (trivial)
Every kiface exports the same `extern "C"` getter — `KIFACE_GETTER` is a macro expanding to the
unmangled symbol `KIFACE_1` (`include/kiway.h:110-115`), defined identically in each kiface
(`pcbnew.cpp:605`, `eeschema.cpp:457`, …), all returning `&kiface`. Two in one image = duplicate
`KIFACE_1`.
Fix is mechanical: the macro is unconditional at `kiway.h:111`, so guard it with `#ifndef
KIFACE_GETTER` and pass a per-target `-DKIFACE_GETTER=<distinct>` (the same `COMPILE_DEFINITIONS`
machinery that already sets `TOP_FRAME`). The `extern "C"` declaration at `kiway.h:541/543`
follows the macro automatically. A combined launcher then declares both distinct externs and
registers each face:
```cpp
extern "C" KIFACE* pcbnew_kiface_getter(int*,int,PGM_BASE*);
extern "C" KIFACE* eeschema_kiface_getter(int*,int,PGM_BASE*);
Kiway.set_kiface( KIWAY::FACE_PCB, pcbnew_kiface_getter(&v, KIFACE_VERSION, this) );
Kiway.set_kiface( KIWAY::FACE_SCH, eeschema_kiface_getter(&v, KIFACE_VERSION, this) );
```
### 3. Relax the WASM "exactly one kiface" convention (small)
`kiway.cpp:223-258` enforces single-kiface registration by *convention*, not storage —
`set_kiface` already writes into a per-`FACE_T` array (`include/kiway.h:476-481`) and `KiFACE()`
reads per slot. Registering several faces is just calling `set_kiface` N times. Update the comment
and the single-getter assumption to match the combined launcher above.
### Non-issues (verified)
- **`Pgm()` / `program` singleton** (`common/pgm_base.cpp:1051-1058`; `single_top.cpp:137,180`)
— there is exactly one shared instance, which is *correct* for a multi-kiface app. No collision.
- **The `static <X>::IFACE kiface(...)` instances** (`pcbnew.cpp:592`, `eeschema.cpp:445`, in named
namespaces `PCB`/`SCH`) — file-static, internal linkage, no external symbol. No collision.
- Beyond `KIFACE_1` and `Kiface()`, the kifaces emit no other global non-member symbols at file
scope; the lower libs (libkicommon, libgal, …) are linked once and shared. A one-shot trial
link is the definitive duplicate-symbol audit, but the *by-design* collisions are just those two.
## The real cost: bundle size
This is the strategic consideration, and it cuts against our current delivery model.
- **Today:** 7 separate bundles, each user lazy-loads only the tool they open. Total deployed
≈ 572 MB raw — but massively duplicated (wx + common + boost replicated in all 7).
- **Merged:** a combined image pays for shared code **once**. A 4-editor (pcbnew + eeschema)
binary ≈ pcbnew's 146 MB (already carries wx/common + the PCB-only OCC) + eeschema's *unique*
code (~3040 MB; OCC is PCB-only, wx/common/boost are shared) ≈ **~180190 MB**. All-7 ≈
~200230 MB. *(Estimates — a trial link is the only way to know precisely.)*
- **The catch:** that ~190 MB is downloaded by **every** user — even one who only wants the
calculator. It works *against* the per-tool lazy loading we do now, and a merged binary can't
be lazily sub-loaded: `wasm-split` / `dlopen` are already assessed as a dead end for us (see
[`../perf/bundle-size.md`](../perf/bundle-size.md) and the bundle-composition research). So the
tradeoff is "one shared binary, zero duplication, cross-probing possible" vs. "larger first
load for single-tool users."
## When Part 2 is worth it
- **Worth it** if the product goal is a unified, project-based "KiCad in the browser" where a
user has a project open and flips between schematic and PCB — and you want desktop-style
**cross-probing** (select a component in the schematic, highlight it on the board). A combined
image holds the board editor *and* schematic editor live under one `KIWAY`, which is exactly how
desktop KiCad's single process works; the inter-editor `KIWAY` express messaging would light up
for free. This is essentially porting the upstream project-manager model to one static image.
- **Not worth it** if the goal is the fastest possible first load for a single tool. Then keep
the tools as separate lazy-loaded bundles and just take [Part 1's](03-part1-library-editor-unification.md)
free dedup.
## Suggested sequencing if pursued
1. Ship [Part 1](03-part1-library-editor-unification.md) first (free; builds the runtime-frame
launcher this reuses).
2. Spike a **2-kiface trial link** (pcbnew + eeschema) with the getter renamed (§2) and
`--allow-multiple-definition` *off*, to enumerate the true duplicate-symbol set and confirm
it's just `Kiface()`. Measure the combined size.
3. Convert `Kiface()` to active-editor dispatch (§1) — the bulk of the work; validate the shared
`common/` call sites resolve correctly with both editors live.
4. Combined launcher registers both faces (§2/§3); JS opens any `FRAME_T` against the one bundle
(and optionally two frames at once for cross-probing).
5. Only then consider folding in gerbview / pl_editor / calculator (3 more faces, same recipe).
## Bottom line
The engineering is bounded and the scary part (the getter) is trivial; the genuine work is one
well-understood refactor (`Kiface()` → runtime dispatch). But Part 2 trades per-tool download size
for a unified binary, so decide it on **product** grounds — do we want a single cross-probing
KiCad-in-the-browser? — not as a duplication cleanup. The duplication cleanup is
[Part 1](03-part1-library-editor-unification.md), and it's free.