Files
logos-protocol/cpp
Dario LipicarandClaude Opus 5 2e3344accc feat(shared-api): make liblogos_protocol.dll a usable single provider (#65)
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>
2026-08-20 22:18:42 -03:00
..