Summary
The registration macros emit three registrars per action. Two of them are only meaningful on
a host that actually owns the models:
registerModelOnce — its factory calls ModelFactory::create<Model>()
registerActionOnce — its runner calls Model::execute(...) on a live holder
Both force the linker to resolve the model's constructor and execute bodies in every
translation unit that merely #includes the model header.
The third, registerActionExecutorOnce, routes through BridgeHandler → Bridge::executeVia
→ IBackend::execute, which is fully abstract. That path needs the action's JSON codecs and
the execute declaration (for ActionTraits::Result) — never its definition.
Why it matters
A pure client — one that dispatches every action to a remote peer and never constructs a model
locally — cannot link. It pulls in model implementations it will never call, and those
implementations routinely depend on a platform stack the client target does not have
(a database driver, a native UI framework, an OS-specific API). For a browser/WASM build
those dependencies do not exist at all, so the link simply cannot be satisfied.
This is already the documented contract elsewhere: IBackend::registerModelWithContext
describes its factory argument as "local path only", and a remote backend's registerModel
ignores it outright because model construction is delegated to the server. The link-time
dependency just doesn't match that contract.
Suggested fix
A macro that suppresses the two server-side registrars:
#ifdef MORPH_CLIENT_ONLY
# define MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, NAME)
# define MORPH_DETAIL_REGISTER_ACTION_LOCAL(M, A, NAME)
#else
# define MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, NAME) /* registerModelOnce<M> */
# define MORPH_DETAIL_REGISTER_ACTION_LOCAL(M, A, NAME) /* registerActionOnce<M, A> */
#endif
so a client links against model headers without their implementations, while the executor
registrar — the one the remote path actually uses — is still emitted.
Caveats worth documenting
- It must be carried on the interface of the morph target, not set per-consumer: the macro
changes which registrars a model header emits, so two translation units disagreeing about it
would violate ODR.
- It must never be defined for a process that hosts models. A server or a local-mode build
would silently register nothing and fail at runtime with "unknown model type" — worth
calling out explicitly, since the failure is far from the cause.
Happy to open a PR if the shape looks right. The naming is a straw man — if you'd prefer this
expressed as a CMake option on the target rather than a bare macro, that works equally well.
Summary
The registration macros emit three registrars per action. Two of them are only meaningful on
a host that actually owns the models:
registerModelOnce— its factory callsModelFactory::create<Model>()registerActionOnce— its runner callsModel::execute(...)on a live holderBoth force the linker to resolve the model's constructor and
executebodies in everytranslation unit that merely
#includes the model header.The third,
registerActionExecutorOnce, routes throughBridgeHandler→Bridge::executeVia→
IBackend::execute, which is fully abstract. That path needs the action's JSON codecs andthe
executedeclaration (forActionTraits::Result) — never its definition.Why it matters
A pure client — one that dispatches every action to a remote peer and never constructs a model
locally — cannot link. It pulls in model implementations it will never call, and those
implementations routinely depend on a platform stack the client target does not have
(a database driver, a native UI framework, an OS-specific API). For a browser/WASM build
those dependencies do not exist at all, so the link simply cannot be satisfied.
This is already the documented contract elsewhere:
IBackend::registerModelWithContextdescribes its factory argument as "local path only", and a remote backend's
registerModelignores it outright because model construction is delegated to the server. The link-time
dependency just doesn't match that contract.
Suggested fix
A macro that suppresses the two server-side registrars:
so a client links against model headers without their implementations, while the executor
registrar — the one the remote path actually uses — is still emitted.
Caveats worth documenting
changes which registrars a model header emits, so two translation units disagreeing about it
would violate ODR.
would silently register nothing and fail at runtime with "unknown model type" — worth
calling out explicitly, since the failure is far from the cause.
Happy to open a PR if the shape looks right. The naming is a straw man — if you'd prefer this
expressed as a CMake option on the target rather than a bare macro, that works equally well.