pcbjam/docs/features/editor-unification/04-part2-single-app-merge.md
Istvan Matejcsok c5b4561495 docs+test(wasm): Part 2 as-built record; cross-face probe spec
- docs/features/editor-unification/06-part2-implementation.md — the merged
  kicad_editor as-built record: symbol-collision surgery (incl. the 21-symbol ODR
  audit), embind dispatcher, wiring, and validation results (frame-runtime 4/4,
  full suite 67 passed / 1 known pre-existing 3d-viewer drag failure fixed on main
  by 7630c7e, collab 10/10, cross-face probe green). README status/index updated;
  dangling perf/bundle-size.md links → perf/README.md.
- tests/kicad/xface-probe.spec.ts — permanent cross-face guard: a --frame=sch
  session opens Preferences and asserts the PCB engine's pages ("PCB Editor",
  "Footprint Editor") exist — i.e. KiFACE(FACE_PCB) lazy-starts inside a schematic
  session (absent on single-kiface bundles). Routed to BIG_MODULE_SPECS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:56:02 +02:00

8.7 KiB
Raw Blame History

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, 03.

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.

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:

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/README.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 free dedup.

Suggested sequencing if pursued

  1. Ship Part 1 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, and it's free.