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>
logos-protocol
The Logos protocol layer: transports, token exchange, and the
language-neutral lp_* C ABI (cpp/logos_protocol.h) that every Logos
SDK builds on.
Extracted from logos-cpp-sdk so that non-C++ SDKs (Rust, …) consume the
same transports, capability/token flow and wire behavior through one stable,
versioned boundary instead of re-wrapping the C++/Qt SDK.
What lives here
- Public C ABI —
cpp/logos_protocol.h: consumer surface (lp_client_*,lp_invoke[_async],lp_subscribe, tokens,lp_get_methods), provider groundwork (lp_provider_*), the trust-root surface (lp_grant_host_servicesand the two functions it gates), per-identity token stores (lp_token_isolate_identity,lp_token_get_for,lp_token_save_for,lp_token_reset_identity,lp_token_identity_is_isolated), and the protocol version (LOGOS_PROTOCOL_VERSION_*,lp_protocol_version(),lp_protocol_abi_major()). JSON-in-strings data model; bytes cross the boundary as{"_bytes":"<base64url>"}(lossless, NUL-safe). - Transports — plain TCP / TCP+TLS (Boost.Asio + OpenSSL + nlohmann,
Qt-free),
qt_local, in-memory mock, and Qt Remote Objects (qt_remote— the only Qt-bearing transport). - Consumer core —
LogosAPIClient/LogosAPIConsumerincluding the automaticcapability_module.requestModuletoken-fetch flow (behind the protocol boundary: every language gets it for free). - Provider-side plumbing —
ModuleProxy(auth gate the transports publish) and the abstractLogosProviderObjectinterface (logos_provider_interface.h). - Token manager, transport/registry factories, mode config (remote/local/mock), and the canonical QVariant↔JSON conversion used at the QRO boundary.
Per-identity token stores
TokenManager::instance() is the image's store. In a host that loads
several modules in one image it is also an ambient ring: the host writes
name -> that module's root auth token for every module it loads, and a client
presents a cached token before it ever mints one — so any module in that image
can reach any other with authority it was never granted, and no requestModule
appears in the log. Per-module origin strings do not change that, because
origin was never consulted on the path taken.
TokenManager::forIdentity(origin) makes origin select the store instead of
merely labelling the caller, and isolateIdentity(origin) is how a host opts a
name in (lp_token_isolate_identity and friends from C). Both are additive and
inert by default: until a name is isolated, forIdentity() returns the same
object instance() returns, so a host that knows nothing about this is
unchanged. A private store is seeded with the trust-root bootstrap (core,
capability_module) so first-call requestModule still works, and with nothing
else.
This is a second axis, not a replacement for the per-image split: a module
cdylib links its own copy of this library and therefore has its own
instance(), which stays correct as-is.
logos-cpp-sdk layers the typed C++ developer API (LogosAPI, module
context, code generator, provider base classes) on top of this repo.
Versioning
This repo carries the logos-protocol semver — the single number that governs Logos load/call compatibility. Two participants (modules, hosts, SDKs in any language) interoperate iff they share the same MAJOR. MINOR is additive/back-compatible; PATCH never affects compatibility. SDKs must re-expose the version of the protocol they linked (never mint their own).
Building
# Via workspace
ws build logos-protocol
# Standalone
nix build
# Tests
nix build .#tests
Layering invariant
logos-protocol depends only on Qt / Boost / OpenSSL / nlohmann_json — it
must NEVER depend on logos-cpp-sdk, logos-qt-sdk, logos-rust-sdk, liblogos
or logos-lidl. Everything points inward.