mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-31 05:51:08 +00:00
Groundwork for making logos_protocol and logos_qt_host proper shared libraries
for the in-process image set, replacing the whole-archive + generated .def +
empty-archive-shim scheme. On its own this changes no behaviour: every current
consumer still resolves the macro to nothing.
WHAT WAS MISSING. The macro had only two states -- __declspec(dllimport) under
_WIN32 && LOGOS_SHARED_USE_DLL, and empty otherwise. There was no export half at
all, because the export side was delegated to a .def generated at
liblogos_core's link (logos-liblogos/cmake/gen-shared-exports.sh). A shared
library cannot use a .def belonging to a different library, so the types need to
be able to mark themselves.
Now three-state, mirroring LP_API's existing shape in logos_protocol.h:
EXPORT while building the shared library that owns the symbol, IMPORT while
consuming it, EMPTY for the static archive.
TWO MACROS, NOT ONE. LOGOS_QT_HOST_API is added alongside for logos-plugin-qt's
LogosAPI, which lives in a DIFFERENT library. It cannot reuse 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 defines the type twice in the process.
Nothing uses LOGOS_QT_HOST_API yet; it is defined here so the split is stated
where the mechanism lives rather than discovered later, and so the Qt host
change is a mechanical switch. Off Windows the distinction is moot -- both
resolve to default visibility -- which is exactly why it would go unnoticed
until a Windows build.
Also corrects this file's own premise, which is where the false claim
originated: "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 each), same size, and
EXACTLY ONE byte differs in the whole 5.4MB file -- 351 -> 352
in __.SYMDEF's ar header, i.e. build-environment metadata, not
content. This is the no-behaviour-change claim, measured.
shared library 748 exported / 78 runtime symbols, unchanged. Expected: no
-fvisibility=hidden anywhere in this build, so a Mach-O shared
library already exported them by default.
checks.tests PASS
NOT VERIFIED LOCALLY: Windows, which is the only platform where this changes
anything -- without dllexport the symbols are simply absent from the PE export
table. packages.x86_64-windows requires an x86_64-linux builder, so it rests on
CI here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
5.5 KiB
C
110 lines
5.5 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)
|
|
# define LOGOS_SHARED_API LOGOS_SHARED_EXPORT
|
|
#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)
|
|
# define LOGOS_QT_HOST_API LOGOS_SHARED_EXPORT
|
|
#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
|