From 2e3344acccfba14cf2bd51019c62b7290dad10ca Mon Sep 17 00:00:00 2001 From: Dario Lipicar Date: Thu, 20 Aug 2026 22:18:42 -0300 Subject: [PATCH] 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 --- cmake/gen-shared-exports.sh | 152 ++++++++++++++++++++++++++++++++++++ cpp/CMakeLists.txt | 81 ++++++++++++++++++- cpp/logos_shared_api.h | 150 +++++++++++++++++++++++++++-------- 3 files changed, 346 insertions(+), 37 deletions(-) create mode 100755 cmake/gen-shared-exports.sh diff --git a/cmake/gen-shared-exports.sh b/cmake/gen-shared-exports.sh new file mode 100755 index 0000000..34eb1b7 --- /dev/null +++ b/cmake/gen-shared-exports.sh @@ -0,0 +1,152 @@ +#!/bin/sh +# Generate the PE module-definition file that makes liblogos_protocol.dll the +# single provider of the shared C++ runtime (TokenManager, LogosAPIClient, +# ModuleProxy, LogosProviderObject, the transports, and the lp_* C ABI). +# +# Adapted from logos-liblogos/cmake/gen-shared-exports.sh, which generated the +# same table one layer up when liblogos_core absorbed both static archives and +# re-exported them. The migration to real shared libraries moves that +# responsibility down to the library that OWNS the symbols; the mechanism is +# unchanged because the reasons for it are unchanged. It cannot be shared as a +# file: logos-liblogos depends on logos-protocol, not the other way round. +# +# WHY a generated .def rather than __declspec(dllexport) in the headers. +# Measured, not assumed: annotating the classes by hand exported 116 symbols and +# the Qt host runtime still failed to link against it with ELEVEN undefined +# references spanning five classes -- LogosProviderObject (and its vtable), +# ModuleProxy, ModuleHandshakeProxy, LogosTransportFactory -- plus free +# functions such as logos::qvariantToNlohmann. A curated list is a moving target: +# it 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'"'"'s WINDOWS_EXPORT_ALL_SYMBOLS is not the alternative. It goes inert the +# moment a target contains any __declspec(dllexport), so it and the annotations +# are mutually exclusive -- measured here as 116 exports both with and without +# it. Hence: the .def owns the PE export table, and logos_shared_api.h resolves +# its "building the shared library" branch to NOTHING on Windows so the two +# mechanisms never compete. +# +# WHY the whole archive and not a curated member list: ld chooses archive +# members by object file, for reasons that have nothing to do with our symbols. +# Consumers link an EMPTY stand-in archive and take everything from the DLL, +# which only works if the DLL really does provide everything. A partial export +# set turns into an undefined reference in a downstream repo. +# +# WHY COMDAT symbols are filtered out: they come from inline functions and +# templates in headers, so every consumer TU emits its own copy regardless of +# what the DLL exports. Exporting them turns the import library into a STRONG +# definition that then collides with the consumer'"'"'s own copy. They are also +# precisely the symbols an export cannot deduplicate -- a function-local static +# inside an inline function stays per-image on PE no matter what. Do not +# "simplify" this filter away. +# +# Usage: gen-shared-exports.sh [...] + +set -eu + +NM="$1"; shift +OUT="$1"; shift + +TMP="${OUT}.tmp" +: > "$TMP" + +while [ "$#" -gt 0 ]; do + archive="$1"; members="$2"; shift 2 + [ -f "$archive" ] || { echo "gen-shared-exports: missing archive $archive" >&2; exit 1; } + "$NM" -A --defined-only "$archive" | awk -v members="$members" -v arch="$archive" ' + BEGIN { + if (members == "*") { all = 1 } + else { n = split(members, a, ","); for (i = 1; i <= n; i++) want[a[i]] = 1 } + } + { + # nm -A on an archive prints ":: ". + split($1, p, ":") + mem = p[2]; typ = $2; name = $3 + + # COMDAT sections are named ".text$" / ".rdata$" / + # etc. Record the mangled tail so the symbol itself can be dropped. + if (name ~ /^\.[a-z]+\$/) { + tail = name; sub(/^\.[a-z]+\$/, "", tail) + # EXCEPT vtables and typeinfo (_ZTV / _ZTI / _ZTS). + # + # 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 therefore cannot distinguish + # "one definition that nobody duplicates" from "every TU emits its + # own", and dropping all of them drops these too. + # + # The reasoning above does not apply to them. A consumer of a class + # WITH a key function does not emit a copy: it emits a .refptr, an + # external reference, and needs ours. Measured here -- the Qt host + # runtime emits .refptr._ZTV19LogosProviderObject and failed to link + # with an undefined reference to the vtable for LogosProviderObject + # while this filter dropped the only definition. A class WITHOUT a + # key function emits its own copy in every TU and never references + # ours, so exporting it is inert rather than colliding. + # + # This did not matter while liblogos_core absorbed both archives: + # definition and consumer landed in ONE image and the reference + # never crossed a boundary. Splitting the runtime into real shared + # libraries is what made class vtables cross-image symbols. + if (tail !~ /^_ZT[VIS]/) comdat[mem SUBSEP tail] = 1 + next + } + if (!all && !(mem in want)) next + # T=text D=data R=rodata B=bss, all uppercase == external linkage. + # Weak (V/W) is COMDAT by another name; lowercase is file-local and + # cannot be exported at all (that includes the function-local statics + # themselves — we export the ACCESSOR so callers reach ours). + if (typ != "T" && typ != "D" && typ != "R" && typ != "B") next + # Itanium-mangled C++ (^_Z), PLUS the logos-protocol C ABI (^lp_). + # + # The ^_Z test alone keeps toolchain bookkeeping such as + # qt_version_tag_6_11_used — which every image legitimately defines — + # out of the export table. But `lp_*` is `extern "C"`, so it is + # UNMANGLED and the same test silently dropped the entire C ABI. + # + # That was invisible until B5: before it, only C++ callers reached the + # shared runtime. B5 re-emits every Qt-typed dependency wrapper as a + # VENEER over the lp path, so a consumer that compiles such a wrapper — + # logos-basecamp compiles package_manager_api.cpp into its own exe — + # now calls lp_invoke / lp_client_create / lp_token_save directly. With + # them absent from the .def the exe cannot import them, and the link + # fails with plain `undefined reference to 'lp_invoke'`. + # + # Exporting is the correct fix rather than letting the consumer link + # liblogos_protocol.a itself: lp_token_save and friends operate on the + # TokenManager singleton, so a static copy in the exe would reinstate + # exactly the split-brain token store this whole .def scheme exists to + # prevent. Module plugins are separate processes and keep their own + # per-image copy by design (see logos_module_grant_host_services). + # + # The prefix is deliberately tight: `lp_` only, not "anything unmangled". + if (name !~ /^_Z/ && name !~ /^lp_/) next + seenmem[mem] = 1 + k++; recmem[k] = mem; rectyp[k] = typ; recnam[k] = name + } + END { + if (!all) for (m in want) if (!(m in seenmem)) { + printf("gen-shared-exports: %s has no member %s\n", arch, m) > "/dev/stderr" + bad = 1 + } + if (bad) exit 1 + for (i = 1; i <= k; i++) { + if ((recmem[i] SUBSEP recnam[i]) in comdat) continue + print recnam[i] (rectyp[i] == "T" ? "" : " DATA") + } + }' >> "$TMP" +done + +# DATA matters: a data export reached without the DATA keyword hands the +# consumer the CONTENTS of the slot instead of its address, which corrupts at +# runtime rather than at link. The keyword is derived from nm's type letter +# above, never written by hand. +{ + echo "EXPORTS" + sort -u "$TMP" +} > "$OUT" +rm -f "$TMP" + +count=$(grep -c . "$OUT" || true) +echo "gen-shared-exports: wrote $OUT ($((count - 1)) symbols)" diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 260502a..5d6ea80 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -168,9 +168,23 @@ install(TARGETS logos_protocol # purely for OUT-OF-PLUGIN callers that bind lp_* at runtime via dlopen/FFI # (e.g. logos-js-sdk's koffi.load, logos-rust-sdk's callerBuildSupport) — # the role liblogos_module_client previously filled. In-plugin code keeps -# linking the STATIC `logos_protocol` archive through the EXPORT set below, -# so this target is intentionally NOT exported and NOT installed into the -# CMake package; `find_package(logos-protocol)` is unaffected. +# linking the STATIC `logos_protocol` archive through the EXPORT set below. +# +# This target is EXPORTED as of this commit, where it was previously installed +# but deliberately kept out of the CMake package. The reason it now has to be is +# a second class of consumer: IN-PROCESS C++ images that link it as the single +# provider of the runtime types which must exist exactly once per process +# (TokenManager, LogosAPIClient, the per-identity StoreRegistry). Linking one +# shared library is what replaces the whole-archive + generated-.def scheme, in +# which liblogos_core absorbed both static archives and re-exported them. A +# consumer cannot link what find_package() does not hand it. +# +# OUT-OF-PROCESS consumers keep linking the STATIC archive, and that is not a +# transitional state: module plugins and ui_qml backends run in their own +# processes, so their own copy of TokenManager is the CORRECT per-process +# singleton. 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 +# keeps those plugins immune to ABI skew against a separately-updated .so. add_library(logos_protocol_shared SHARED ${PROTOCOL_SOURCES}) target_link_libraries(logos_protocol_shared PUBLIC @@ -188,12 +202,21 @@ target_link_libraries(logos_protocol_shared PUBLIC # leaves LP_API empty so consumers linking it need no import library. target_compile_definitions(logos_protocol_shared PRIVATE LOGOS_PROTOCOL_BUILDING_SHARED) +# The INSTALL_INTERFACE half mirrors the static target exactly. Without it an +# exported target carries no include directories at all, and the failure is a +# confusing one: the target imports fine and the link succeeds, then the +# consumer fails to COMPILE on `#include "token_manager.h"`. target_include_directories(logos_protocol_shared PUBLIC $ $ $ $ $ + $ + $ + $ + $ + $ ) # Same basename as the archive (liblogos_protocol.{so,dylib}); the LIBRARY @@ -203,9 +226,61 @@ set_target_properties(logos_protocol_shared PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib" ) +# ARCHIVE DESTINATION is NOT redundant here. On Windows a shared library's +# import library (.dll.a) is the ARCHIVE artifact and the DLL itself is RUNTIME, +# so without an ARCHIVE destination the import library is never installed and a +# consumer that find_package()s this target gets an imported SHARED target whose +# IMPORTED_IMPLIB does not exist. It links fine on ELF and Mach-O, where there is +# no import library at all, and fails only on Windows. +# PE export table for the shared library, generated from the objects. +# +# Not needed off Windows: ELF and Mach-O export a shared library's non-hidden +# symbols by default, which is exactly why the shortfall this replaces was +# invisible until a Windows link. +if(WIN32) + # CMAKE_NM is normally set by the toolchain file; fall back to the + # cross-prefixed binary so a plain `cmake -DCMAKE_TOOLCHAIN_FILE=...` still + # generates. A missing nm must fail HERE, loudly, rather than silently + # emitting an empty .def -- an empty export table links clean and fails in a + # downstream repo as an undefined reference. + set(_lp_nm "${CMAKE_NM}") + if(NOT _lp_nm) + find_program(_lp_nm NAMES "${CMAKE_CXX_COMPILER_TARGET}-nm" x86_64-w64-mingw32-nm nm) + endif() + if(NOT _lp_nm) + message(FATAL_ERROR + "nm not found, so liblogos_protocol.dll cannot be given an export " + "list. Windows requires one: PE exports nothing that is not named, " + "and a consumer linking the empty stand-in archive would then fail " + "with undefined references to the entire C++ runtime.") + endif() + + # Generated from the STATIC archive, which is built from the same sources. + # "*" means every member: ld chooses archive members for reasons unrelated to + # our symbols, so a partial set is a downstream undefined reference. + set(_lp_def "${CMAKE_CURRENT_BINARY_DIR}/logos_protocol_shared_exports.def") + add_custom_command( + OUTPUT "${_lp_def}" + COMMAND ${CMAKE_COMMAND} -E env sh + "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/gen-shared-exports.sh" + "${_lp_nm}" "${_lp_def}" + "$" "*" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../cmake/gen-shared-exports.sh" logos_protocol + COMMENT "Generating liblogos_protocol export definition" + VERBATIM + ) + add_custom_target(logos_protocol_shared_exports DEPENDS "${_lp_def}") + add_dependencies(logos_protocol_shared logos_protocol_shared_exports) + target_link_options(logos_protocol_shared PRIVATE "${_lp_def}") + set_property(TARGET logos_protocol_shared APPEND PROPERTY LINK_DEPENDS "${_lp_def}") +endif() + install(TARGETS logos_protocol_shared + EXPORT logos-protocolTargets + ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin + INCLUDES DESTINATION include ) install(EXPORT logos-protocolTargets diff --git a/cpp/logos_shared_api.h b/cpp/logos_shared_api.h index 8392f3b..da85faa 100644 --- a/cpp/logos_shared_api.h +++ b/cpp/logos_shared_api.h @@ -5,36 +5,51 @@ * @file logos_shared_api.h * @brief Marks the runtime types that must exist EXACTLY ONCE per process. * - * ELF and Mach-O give this for free. Both formats interpose symbols across the - * whole process image set, so when liblogos_core.{so,dylib} exports - * TokenManager::instance() every other image in the process — the host binary, - * the UI plugin — binds to that one definition and the function-local - * `static TokenManager instance;` inside it is genuinely a singleton. + * 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. * - * PE has no interposition. A symbol is either in a DLL's export table and - * reached through an import thunk, or it is resolved image-locally; there is no - * "first definition wins across the process" rule. So on Windows every image - * that links liblogos_protocol.a / liblogos_qt_sdk.a statically gets its OWN - * copy of the code, and therefore its own copy of every function-local static - * inside it. Measured on the Basecamp payload: nine images each define - * TokenManager::instance()::instance. The host saves a capability token into - * its copy, the UI plugin 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 liblogos_qt_sdk.a - * also defines, and the link dies with "multiple definition of + * 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 (today's C-API-only narrowing) links, and silently - * gives every image its own statics. That is the bug above. + * - 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: liblogos_core.dll exports these types - * explicitly (a generated .def, see logos-liblogos/cmake/gen-shared-exports.sh) - * and the in-process consumers — LogosBasecamp.exe and main_ui.dll — compile - * with LOGOS_SHARED_USE_DLL so their references become `__declspec(dllimport)`. + * 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 @@ -42,20 +57,87 @@ * 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. * - * Everything here is deliberately a no-op unless a consumer opts in: - * - * - Off Windows the macro is empty; ELF/Mach-O already do the right thing. - * - Inside logos-protocol / logos-qt-sdk / liblogos_core the macro is empty, - * so the static archives are compiled byte-identically to before and the - * export side is driven purely by the .def at liblogos_core's link. - * - logos_host.exe, ui-host.exe and the module plugin DLLs do not define - * LOGOS_SHARED_USE_DLL. They are separate processes that do not load - * liblogos_core, so they keep their own (correct, per-process) statics. + * 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. */ -#if defined(_WIN32) && defined(LOGOS_SHARED_USE_DLL) -# define LOGOS_SHARED_API __declspec(dllimport) + +/* 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