mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
PR-7 of the shared-runtime migration. Comment-only: no non-comment line changes,
and checks.tests passes.
This header is the canonical explanation of the one-runtime invariant, and after
the migration it described a scheme that no longer exists. It still said the
resolution was ONE PROVIDER with liblogos_core exporting types it does not own,
and still pointed at logos-basecamp/cmake/LogosSharedFromDll.cmake -- a file
deleted in logos-basecamp#348 and logos-logoscore-cli#98.
Now states where definitions actually live:
liblogos_protocol TokenManager, LogosAPIClient, the StoreRegistry
liblogos_qt_host LogosAPI
and that liblogos_core defines ZERO runtime symbols, importing them like every
other consumer.
RECORDS THE FIX THAT DID NOT WORK, because it looks obvious and someone will try
it again: hand-marking a class list with __declspec(dllexport) is not a smaller
version of the right answer. It exported 116 symbols and the Qt host runtime
still failed to link with ELEVEN undefined references across five classes, plus
free functions nobody had marked. A curated list is correct only until the next
consumer touches a symbol nobody thought of, and the failure lands in a
downstream repo far from the cause. Hence a .def GENERATED from the objects.
STATES THE IN-PROCESS / OUT-OF-PROCESS SPLIT, which is the distinction that
decides who links what and the one most likely to be "simplified" away by
someone tidying up. In-process images link the SHARED libraries; logos_host,
ui-host, module plugins and ui_qml backends keep linking the STATIC archive, and
that is CORRECT rather than a leftover: each runs in its own process, so its own
copy IS the right per-process singleton -- measured, logos_host and ui-host
define ~100 runtime symbols each and are deliberately exempt from the symbol
gates. Staying static also keeps a .lgx self-contained, since a .lgx records an
EMPTY nix closure and a shared library would not travel with it.
Says plainly not to unify the two, and why: it would couple every .lgx to the
exact runtime build it was packaged against, to fix a duplication that is not a
bug in a separate process.
Ends with what asserts any of this, since nothing did for a long time and that
is how nine images came to define the same singleton: symbol gates in
logos-basecamp, logos-logoscore-cli and logos-standalone-app, each shipped with
a negative control that plants a real duplicate and requires rejection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
187 lines
9.6 KiB
C
187 lines
9.6 KiB
C
#ifndef LOGOS_SHARED_API_H
|
|
#define LOGOS_SHARED_API_H
|
|
|
|
/**
|
|
* @file logos_shared_api.h
|
|
* @brief Marks the runtime types that must exist EXACTLY ONCE per process.
|
|
*
|
|
* Every image that links liblogos_protocol.a / liblogos_qt_host.a statically
|
|
* gets its OWN copy of the code, and therefore its own copy of every
|
|
* function-local static inside it: TokenManager::instance, the per-identity
|
|
* StoreRegistry, the host-services grant, the deferred event-subscription
|
|
* registry. The host saves a capability token into its copy, another in-process
|
|
* image reads its own empty copy, and every cross-module call is refused
|
|
* ("ModuleProxy: rejecting unauthorized call") — the package manager never
|
|
* appears in the sidebar.
|
|
*
|
|
* WHICH PLATFORMS. This was long documented as Windows-only, on the premise
|
|
* that "ELF and Mach-O interpose symbols across the whole process image set".
|
|
* That is true of ELF and FALSE of Mach-O, and the false half was measured:
|
|
*
|
|
* - PE no interposition at all. A symbol is either in a DLL's export
|
|
* table and reached through an import thunk, or it is resolved
|
|
* image-locally. Measured on the Basecamp payload: NINE images each
|
|
* defining TokenManager::instance()::instance.
|
|
* - Mach-O two-level namespace, so it behaves like PE, not like ELF. It
|
|
* appears to work only while the consumer image has NO definition of
|
|
* its own, so ld binds the undefined symbol to the provider. The
|
|
* moment any reference drags an archive member in, that image gets
|
|
* its own copy, silently. Measured in logos-basecamp: ONE reference
|
|
* to LogosAPI::forIdentity pulled logos_api.cpp.o and
|
|
* token_manager.cpp.o into the executable, which then produced 31
|
|
* refused calls against a baseline of 0.
|
|
* - ELF flat namespace, first definition wins process-wide. This one
|
|
* genuinely does collapse duplicates.
|
|
*
|
|
* WHERE THE DEFINITIONS LIVE NOW. Each type is owned by ONE shared library, and
|
|
* every in-process image imports it:
|
|
*
|
|
* liblogos_protocol TokenManager, LogosAPIClient, the StoreRegistry
|
|
* liblogos_qt_host LogosAPI
|
|
*
|
|
* liblogos_core is NOT the provider. It used to be: it absorbed both static
|
|
* archives with --whole-archive and re-published their symbols through a
|
|
* generated .def, so that one image could stand in for types it did not own.
|
|
* That scheme is gone (logos-protocol#65, logos-plugin-qt#22,
|
|
* logos-liblogos#182) and liblogos_core imports the runtime like every other
|
|
* consumer. Measured after the migration: liblogos_core defines ZERO runtime
|
|
* symbols, liblogos_protocol 78, liblogos_qt_host 23.
|
|
*
|
|
* The two fixes that look obvious are both wrong, and were both tried:
|
|
*
|
|
* - Exporting everything from one image (-Wl,--export-all-symbols) makes its
|
|
* import library a second definition of symbols the static archives also
|
|
* define, and the link dies with "multiple definition of
|
|
* `LogosAPI::LogosAPI'".
|
|
* - Hand-marking a class list with __declspec(dllexport) is not a smaller
|
|
* version of the right answer, it is a moving target: it exported 116
|
|
* symbols and the Qt host runtime still failed to link with ELEVEN
|
|
* undefined references across five classes, plus free functions nobody had
|
|
* thought to mark. A curated list is correct only until the next consumer
|
|
* touches a symbol nobody marked, and the failure lands in a downstream
|
|
* repo, far from the cause.
|
|
*
|
|
* So on Windows the export table is GENERATED from the objects
|
|
* (cmake/gen-shared-exports.sh) rather than curated, and this header resolves
|
|
* its "building the shared library" branch to nothing there, so the two
|
|
* mechanisms never compete. Off Windows a shared library exports its non-hidden
|
|
* symbols by default and there is nothing to generate.
|
|
*
|
|
* The dllimport half is the load-bearing one for consumers. It rewrites the
|
|
* reference to go through `__imp_`, so the plain symbol is never undefined and
|
|
* GNU ld never pulls an archive member that would redefine it — regardless of
|
|
* where a static archive sits on the link line. Without it the link still
|
|
* succeeds and binds to the archive, with no diagnostic at all.
|
|
*
|
|
* IN-PROCESS vs OUT-OF-PROCESS, which is the distinction that decides who links
|
|
* what, and the one most likely to be "simplified" away by someone tidying up:
|
|
*
|
|
* IN-PROCESS the app executable, liblogos_core, and anything QPluginLoader
|
|
* pulls into the app (view replica factories, legacy widget
|
|
* plugins, logos-standalone-app's third-party plugins). These
|
|
* link the SHARED libraries. A second copy here is the
|
|
* split-brain.
|
|
*
|
|
* OUT-OF-PROCESS logos_host, ui-host, module plugins and ui_qml backends.
|
|
* These keep linking the STATIC archive, and that is CORRECT, not
|
|
* a leftover. Each runs in its own process, so its own copy of
|
|
* TokenManager IS the right per-process singleton — measured,
|
|
* logos_host and ui-host define ~100 runtime symbols each and are
|
|
* deliberately exempt from the symbol gates. Staying static also
|
|
* keeps a .lgx self-contained: a .lgx records an EMPTY nix
|
|
* closure, so a shared library would not travel with it, and it
|
|
* keeps those plugins immune to ABI skew against a separately
|
|
* updated .so.
|
|
*
|
|
* Do not "unify" the two by moving modules onto the shared libraries. It would
|
|
* couple every .lgx to the exact runtime build it was packaged against, to fix
|
|
* a duplication that is not a bug in a separate process.
|
|
*
|
|
* WHAT ASSERTS ANY OF THIS. Nothing did, for a long time, which is how nine
|
|
* images came to define the same singleton. There are now symbol gates in
|
|
* logos-basecamp, logos-logoscore-cli and logos-standalone-app
|
|
* (nix/symbol-gate.nix) asserting that each type has exactly one definer across
|
|
* the in-process image set, each shipped with a negative control that plants a
|
|
* real duplicate and requires the gate to reject it.
|
|
*/
|
|
|
|
/* The primitives. Kept separate so the per-library macros below read as a
|
|
* three-state choice (export / import / neither) rather than as nested #ifdefs.
|
|
* Off Windows there is nothing to import: a shared library exports its
|
|
* non-hidden symbols by default, and consumers just reference them. */
|
|
#if defined(_WIN32)
|
|
# define LOGOS_SHARED_EXPORT __declspec(dllexport)
|
|
# define LOGOS_SHARED_IMPORT __declspec(dllimport)
|
|
#else
|
|
# define LOGOS_SHARED_EXPORT __attribute__((visibility("default")))
|
|
# define LOGOS_SHARED_IMPORT
|
|
#endif
|
|
|
|
/* logos-protocol's own single-instance types: TokenManager, LogosAPIClient,
|
|
* and the LogosResult stream operators.
|
|
*
|
|
* EXPORT while building the shared library that owns them, IMPORT while
|
|
* consuming that library, and EMPTY for the static archive — which is what
|
|
* every current consumer gets, so this is a no-op until a build opts in. */
|
|
#if defined(LOGOS_PROTOCOL_BUILDING_SHARED)
|
|
/* Building the library that owns the symbol.
|
|
*
|
|
* On Windows this resolves to NOTHING, and that is deliberate: the PE export
|
|
* table is generated from the objects (cmake/gen-shared-exports.sh), because a
|
|
* hand-marked list is a moving target -- it exported 116 symbols and the Qt host
|
|
* runtime still failed to link with eleven undefined references across five
|
|
* classes. Marking here as well would put two mechanisms on the same table for
|
|
* no gain, and __declspec(dllexport) additionally makes CMake's
|
|
* WINDOWS_EXPORT_ALL_SYMBOLS inert, so the annotation actively forecloses the
|
|
* automatic route.
|
|
*
|
|
* Off Windows the annotation is what a hidden-visibility build would need, and
|
|
* is harmless today because nothing sets -fvisibility=hidden. */
|
|
# if defined(_WIN32)
|
|
# define LOGOS_SHARED_API
|
|
# else
|
|
# define LOGOS_SHARED_API LOGOS_SHARED_EXPORT
|
|
# endif
|
|
#elif defined(_WIN32) && defined(LOGOS_SHARED_USE_DLL)
|
|
# define LOGOS_SHARED_API LOGOS_SHARED_IMPORT
|
|
#else
|
|
# define LOGOS_SHARED_API
|
|
#endif
|
|
|
|
/* logos-plugin-qt's LogosAPI, which lives in a DIFFERENT library.
|
|
*
|
|
* It needs its own macro rather than reusing LOGOS_SHARED_API, because the two
|
|
* are not the same choice in the same translation unit: while building the Qt
|
|
* host runtime shared library, LogosAPI must be EXPORTED while TokenManager —
|
|
* owned by logos-protocol — must be IMPORTED. One macro cannot say both, and on
|
|
* PE getting it wrong means the type is defined twice in the process.
|
|
*
|
|
* Off Windows this distinction is moot (both resolve to default visibility),
|
|
* which is exactly why it would go unnoticed until a Windows build. */
|
|
#if defined(LOGOS_QT_HOST_BUILDING_SHARED)
|
|
/* Building the library that owns the symbol.
|
|
*
|
|
* On Windows this resolves to NOTHING, and that is deliberate: the PE export
|
|
* table is generated from the objects (cmake/gen-shared-exports.sh), because a
|
|
* hand-marked list is a moving target -- it exported 116 symbols and the Qt host
|
|
* runtime still failed to link with eleven undefined references across five
|
|
* classes. Marking here as well would put two mechanisms on the same table for
|
|
* no gain, and __declspec(dllexport) additionally makes CMake's
|
|
* WINDOWS_EXPORT_ALL_SYMBOLS inert, so the annotation actively forecloses the
|
|
* automatic route.
|
|
*
|
|
* Off Windows the annotation is what a hidden-visibility build would need, and
|
|
* is harmless today because nothing sets -fvisibility=hidden. */
|
|
# if defined(_WIN32)
|
|
# define LOGOS_QT_HOST_API
|
|
# else
|
|
# define LOGOS_QT_HOST_API LOGOS_SHARED_EXPORT
|
|
# endif
|
|
#elif defined(_WIN32) && defined(LOGOS_SHARED_USE_DLL)
|
|
# define LOGOS_QT_HOST_API LOGOS_SHARED_IMPORT
|
|
#else
|
|
# define LOGOS_QT_HOST_API
|
|
#endif
|
|
|
|
#endif // LOGOS_SHARED_API_H
|