Skip to content

Registration macros force clients to link model implementations they never call #27

Description

@Yaraslaut

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 BridgeHandlerBridge::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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions