Skip to content

core: move BRIDGE_REGISTER_MODEL/BRIDGE_REGISTER_ACTION's registrar out of the header - #837

Merged
Yaraslaut merged 2 commits into
masterfrom
feature/791-registrar-relocation
Sep 27, 2026
Merged

Yaraslaut merged 2 commits into
masterfrom
feature/791-registrar-relocation

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Summary

BRIDGE_REGISTER_MODEL/BRIDGE_REGISTER_ACTION's registrar initialiser is an ordinary function call written where the macro is expanded, so its body — and everything it drags in (the action's glaze codecs, the dispatcher's runner closure, forms::schemaJson<A>'s schema generator) — is instantiated in every translation unit that includes a model header carrying it. A prior measurement on examples/kanban/include/kanban/models/board_model.hpp found 26.93 CPU-seconds of -O3 codegen per including TU, for codecs the overwhelming majority of those TUs never call.

  • BRIDGE_DECLARE_MODEL/BRIDGE_DECLARE_ACTION decompose the existing macros into a header-safe half (the ModelTraits<M>/ActionTraits<A> specialisation alone, free to repeat per TU) and BRIDGE_REGISTER_MODEL_SOURCE/BRIDGE_REGISTER_ACTION_SOURCE carry the registration itself, called once in the .cpp that should own it. The combined BRIDGE_REGISTER_MODEL/BRIDGE_REGISTER_ACTION are unchanged and remain the right choice when the split isn't worth it.
  • Splitting declaration from registration introduces a mistake that couldn't previously happen: a model declared but never registered anywhere. Closed with a link-time canary — BRIDGE_DECLARE_MODEL/BRIDGE_DECLARE_ACTION call a function template behind extern template, and only the SOURCE macros explicitly instantiate it — so a missing registration fails to link, instead of compiling clean and surfacing only as a runtime "unknown model type" far from the cause.
  • Retrofits examples/kanban's board_model.hpp/.cpp — the exact header the original measurement used — as proof this reaches the measured cost. Re-measured (AppleClang 17, not isolated from concurrent system load on this run, so reported as a lower bound): 20.81 CPU-s before, 7.81 CPU-s after, a 13.00 CPU-s reduction per translation unit; the compiled stub's object file shrinks from 3,331,704 to 3,232 bytes (7155 to 24 symbols), with zero glaze symbols left in the post-fix object.
  • docs/spec/core/registry.md gains "Moving a registrar out of the header", documenting the mechanism, the canary, and its real gaps (a dlopened plugin module must keep using the combined macros; the static-library archive-drop interaction is stated as inferred, not measured).

Closes #791

Test plan

  • Full core suite (morph_tests, 1602 test cases / 23513 assertions) green, both before and after a rebase onto current master.
  • Full examples/kanban ladder suite (ladder_kanban_tests, 172 test cases / 1192 assertions) green.
  • New tests/compile_checks/declare_only_no_source_link.cpp + declare_only_source_provided.cpp, wired through a new try_compile() pair in tests/CMakeLists.txt, prove the canary both directions: a declared-but-unregistered model/action fails to link, and the same program links cleanly once the SOURCE macros are added.
  • examples/kanban's ladder_kanban_lib, ladder_kanban_server, and ladder_kanban_headless all link successfully against the retrofitted board_model.hpp/.cpp.
  • Before/after CPU-second measurement on board_model.hpp (methodology matching the original issue: -fsyntax-only and -O3 -c, best of two, on a stub TU that only includes the header), corroborated by object-file size and symbol-count diffs.

🤖 Generated with Claude Code

https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc

@Yaraslaut
Yaraslaut force-pushed the feature/791-registrar-relocation branch 2 times, most recently from 12f630c to 39858e7 Compare September 26, 2026 19:57
Yaraslaut and others added 2 commits September 27, 2026 12:06
…ut of the header

BRIDGE_REGISTER_MODEL/BRIDGE_REGISTER_ACTION's registrar initialiser is an
ordinary function call written where the macro is expanded, so its body --
and everything it drags in (the action's glaze codecs, the dispatcher's
runner closure, forms::schemaJson<A>'s schema generator) -- is instantiated
in every translation unit that includes a model header carrying it. Measured
on examples/kanban/include/kanban/models/board_model.hpp (morph#791): 26.93
CPU-seconds of -O3 codegen per including TU, for codecs the overwhelming
majority of those TUs never call.

BRIDGE_DECLARE_MODEL/BRIDGE_DECLARE_ACTION decompose the existing macros into
a header-safe half (the ModelTraits<M>/ActionTraits<A> specialisation alone,
free to repeat per TU) and BRIDGE_REGISTER_MODEL_SOURCE/
BRIDGE_REGISTER_ACTION_SOURCE carry the registration itself, called once in
the .cpp that should own it. The combined BRIDGE_REGISTER_MODEL/
BRIDGE_REGISTER_ACTION macros are unchanged and remain the right choice when
the split isn't worth it.

Splitting declaration from registration introduces a mistake that couldn't
previously happen: a model declared but never registered anywhere. Closed
with a link-time canary -- BRIDGE_DECLARE_MODEL/BRIDGE_DECLARE_ACTION call a
function template behind `extern template`, and only the SOURCE macros
explicitly instantiate it -- so a missing registration fails to *link*,
instead of compiling clean and surfacing only as a runtime "unknown model
type" far from the cause. Proven both ways by
tests/compile_checks/declare_only_no_source_link.cpp/
declare_only_source_provided.cpp via a new try_compile() pair in
tests/CMakeLists.txt.

Retrofits examples/kanban's board_model.hpp/.cpp -- the exact header morph#791
measured -- as the proof this reaches the measured cost. Re-measured (AppleClang
17, not isolated from concurrent system load so reported as a lower bound):
20.81 CPU-s before, 7.81 CPU-s after, a 13.00 CPU-s reduction per translation
unit; the compiled stub's object file shrinks from 3,331,704 to 3,232 bytes
(7155 to 24 symbols), with zero glaze symbols left in the post-fix object.

docs/spec/core/registry.md gains "Moving a registrar out of the header",
documenting the mechanism, the canary, and its one real gap (a `dlopen`ed
plugin module, which must keep using the combined macros).

Closes #791

Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc
…_compile

A try_compile() scratch project cannot link core-cpp's compiled modules
(established just above, for the MORPH_CLIENT_ONLY guard, when morph moved
onto core-cpp v0.5.0). BRIDGE_REGISTER_ACTION_SOURCE routes through
registerActionExecutorOnce's definition in bridge.hpp, which pulls in
TimeoutScheduler and, through it, core::net -- so the "must link" probe needs
the same real, linked-executable treatment as that guard's own positive
probes, via morph_guard_probe_deps. The "must fail to link" probe stays a
try_compile(): it never reaches Bridge's registration machinery, so it needs
nothing beyond MORPH_VETTED_HMAC_GUARD_INCLUDE_DIRS.

Also drops a stale try_run()-based client_only_runtime_throw check that had
survived, unchanged, as merge-conflict context from before core-cpp v0.5.0 --
duplicating the add_executable()-based check already added above.
@Yaraslaut
Yaraslaut force-pushed the feature/791-registrar-relocation branch from 61d726c to 95d2b49 Compare September 27, 2026 10:06
@codecov

codecov Bot commented Sep 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit ac56129 into master Sep 27, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: BRIDGE_REGISTER_ACTION must live in the model's header, and costs 26.93 CPU-s of codegen in every TU that includes it

1 participant