# 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 (~30–40 MB; OCC is PCB-only, wx/common/boost are shared) ≈ **~180–190 MB**. All-7 ≈
  ~200–230 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`](../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](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.
