mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
PR-1+PR-2 of the shared-runtime migration, squashed: they were raised
separately and the second replaced the first's mechanism, so the split was
history rather than review value.
WHY. The runtime types that must exist EXACTLY ONCE per process (TokenManager,
LogosAPIClient, the per-identity StoreRegistry) are moving from "absorbed into
liblogos_core by whole-archive and re-exported through a generated .def" to
"owned by the shared library that defines them". Every image that links a static
archive gets its own copy of every function-local static inside it, so the host
writes a capability token into one store and another in-process image reads an
empty one -- with no build diagnostic.
Three things, and the order they were discovered in is the order they matter:
1. THE CMAKE PACKAGE. logos_protocol_shared was built and installed but
deliberately kept OUT of the export set: it existed only for FFI callers that
dlopen the lp_* C ABI, and those never link it. In-process C++ consumers do,
and a consumer cannot link what find_package() does not hand it. Now exported
as logos-protocol::logos_protocol_shared, with the INSTALL_INTERFACE include
dirs the static target already had, and with ARCHIVE DESTINATION -- on Windows
a shared library's import library (.dll.a) is the ARCHIVE artifact, so
omitting it installs no import library at all and the failure is invisible on
ELF and Mach-O, which have none.
2. THE EXPORT TABLE IS GENERATED, NOT HAND-MARKED. The first attempt annotated
the classes with __declspec(dllexport). That exported 116 symbols and the Qt
host runtime STILL failed to link against it, with ELEVEN undefined
references across five classes -- LogosProviderObject and its vtable,
ModuleProxy, ModuleHandshakeProxy, LogosTransportFactory -- plus free
functions such as logos::qvariantToNlohmann. 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.
cmake/gen-shared-exports.sh is adapted from logos-liblogos, which generated
the same table one layer up. The mechanism is unchanged because the reasons
for it are unchanged; this moves it down to the library that owns the
symbols. It cannot be shared as a file: logos-liblogos depends on
logos-protocol, not the other way round.
logos_shared_api.h therefore resolves its "building the shared library"
branch to NOTHING on Windows, so the .def and the annotations never compete.
The macro keeps its import half, which is what stops a consumer pulling the
archive member that would redefine the symbol.
30 exports on master -> 116 hand-marked -> 360 generated.
3. VTABLES AND TYPEINFO ARE CARVED OUT OF THE COMDAT FILTER. The last undefined
symbol was the vtable for LogosProviderObject. PE HAS NO WEAK SYMBOLS --
COMDAT is the mechanism for weak and inline linkage -- so GCC emits a vtable
into .rdata$_ZTV... even when the class has a key function and the vtable is a
single strong definition. The section name cannot tell "one definition nobody
duplicates" from "every TU emits its own", so the filter dropped it. The
filter's reasoning does not apply to vtables: a consumer of a class WITH a key
function emits a .refptr and needs ours; a class WITHOUT one emits its own
copy and never references ours, so exporting is inert. This never mattered
while liblogos_core absorbed both archives -- definition and consumer landed
in one image and the reference never crossed a boundary.
WHY PROTOCOL NEEDS A .def WHEN THE QT HOST DOES NOT. The shared qt-host DLL
exports 2799 symbols with no .def at all, because it carries no dllexport marks
and GNU ld auto-exports everything. Protocol cannot rely on that: LP_API's
dllexport on the lp_* C ABI disables auto-export for the whole target. ANY single
dllexport turns the automatic path off -- which is also why CMake's
WINDOWS_EXPORT_ALL_SYMBOLS was measured as completely inert here.
TWO MACROS, NOT ONE. LOGOS_QT_HOST_API is added for logos-plugin-qt's LogosAPI,
which lives in a different library. While building the Qt host shared library
LogosAPI must NOT be dllimport while TokenManager must be, and one macro cannot
say both in the same translation unit. Off Windows the distinction is moot --
both resolve to default visibility -- which is exactly why getting it wrong would
go unnoticed until a Windows build.
Also corrects this file's own premise, the origin of the false claim that "ELF
and Mach-O give this for free. Both formats interpose symbols across the whole
process image set." True of ELF, false of Mach-O, whose two-level namespace gives
no interposition -- measured in logos-basecamp, where one reference to
LogosAPI::forIdentity dragged logos_api.cpp.o into the executable and produced 31
refused calls against a baseline of 0.
VERIFIED.
aarch64-darwin static archive symbol tables IDENTICAL (14257 lines), exactly
ONE byte differing in 5.4MB -- 351 -> 352 in __.SYMDEF's ar
header, build metadata, not content
logos-protocolTargets.cmake names both targets
checks.tests PASS, .#default PASS
x86_64-linux checks.tests PASS
x86_64-mingw export table 30 -> 360, all 30 lp_* preserved, 0 removed
import library liblogos_protocol.dll.a now installed
logos-plugin-qt#22 links against it, and its PE layering is
correct: defines LogosAPI 25, defines TokenManager 0 and
LogosAPIClient 0, imports from liblogos_protocol.dll
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
144 lines
7.1 KiB
C
144 lines
7.1 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.
|
|
*
|
|
* The consumers empty their static archives on every platform anyway
|
|
* (logos-basecamp/cmake/LogosSharedFromDll.cmake), so the invariant is ONE rule
|
|
* everywhere rather than three — and nix/symbol-gate.nix can assert it
|
|
* uniformly instead of encoding a per-platform exception.
|
|
*
|
|
* The obvious fixes are both wrong, and the wrongness is not obvious, so:
|
|
*
|
|
* - Exporting everything from liblogos_core (-Wl,--export-all-symbols) makes
|
|
* its import library a second definition of symbols that the static
|
|
* archives also define, and the link dies with "multiple definition of
|
|
* `LogosAPI::LogosAPI'". See the note in logos-liblogos/src/CMakeLists.txt.
|
|
* - Exporting nothing (a C-API-only narrowing) links, and silently gives
|
|
* every image its own statics. That is the bug above.
|
|
*
|
|
* The resolution is ONE PROVIDER, and the macro below is how a symbol is
|
|
* assigned to one. On Windows the in-process consumers additionally compile
|
|
* with LOGOS_SHARED_USE_DLL so their references become __declspec(dllimport).
|
|
*
|
|
* The dllimport is the load-bearing half, not the export. It rewrites the
|
|
* reference to go through `__imp_`, so the plain symbol is never undefined and
|
|
* GNU ld never pulls the archive member that would have redefined it —
|
|
* regardless of where the static archive sits on the link line. Without it the
|
|
* link still succeeds and binds to the archive, with no diagnostic at all.
|
|
*
|
|
* Note that logos_host, ui-host and the module plugins do NOT opt in. They are
|
|
* separate processes that do not load the provider, so they keep their own —
|
|
* correct, per-process — statics.
|
|
*/
|
|
|
|
/* 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
|