Files
Dario Gabriel LipicarandClaude Opus 5 6c24fcb132 feat(caller): resolve who is calling, and declare the export that carries it
A module can now learn which module is calling it. Not via the LIDL — this
is not part of any module's interface, and the callee already has the
identity from the token the call carried; the only question was surfacing
it. So it is ambient: logos::currentCaller(), no declared parameter, no
contract change, no per-method opt-in.

WHAT THIS PR CONTAINS
  * LogosCaller — Unknown | HostAnchor | Module{name, instance?} |
    Derived{parent, leaf} | Operator{name} — std-typed and Qt-free.
  * CallerScope, an RAII save/restore around a thread-local STACK. Not a
    slot: A calling B calling back into A on one thread must nest, and an
    exception thrown from a handler must still pop.
  * resolveCaller, replacing the bool fold in ModuleProxy. It reads the
    INBOUND store #69 made direction-pure — the only store that may
    legitimately name a caller.
  * logos_module_set_call_caller DECLARED, and MINOR 5 -> 6.

WHY AMBIENT, AND WHY IT MUST CROSS AN IMAGE BOUNDARY
LogosProviderObject::callMethod is a vtable slot, and this codebase avoids
vtable changes on purpose. But the deeper reason is measured, not stylistic:
nm on real binaries shows the host and the module plugin EACH define
ModuleProxy::callRemoteMethod and TokenManager::instance, each with its own
function-local static at a distinct address, and neither with a single
undefined reference to the other's. Mach-O is TWOLEVEL; PE has no
interposition. A thread_local opened host-side is NOT the one a handler
reads. Since --backend qt is now refused outright, every module is a cdylib
and the C ABI push is the only path, not a fallback.

The pull is only safe through QMetaObject::invokeMethod on the host's
LogosAPI, because metaObject()/qt_metacall are virtual and the vptr was
written by the host's constructor — LogosAPI is duplicated across images
too, meta-object included, so a direct call would bind to the plugin's copy
and read the plugin's TLS, silently empty forever. A dynamic property
cannot carry it either: one process-global slot, so two overlapping
concurrency:"multi" calls from different callers would clobber each other.

Nothing here is spelled "verified". capability_module checks only that an
asserted name EXISTS as a key, so the strongest honest word is token-bound.
HostAnchor carries no name because core and capability_module hold one
token VALUE under two keys by construction. Unknown is the fail-closed
value and is always in-band, never spelled by absence.

The constant-time fold survives: the matched key is accumulated into a
fixed-width buffer with no data-dependent branch, verified at the
instruction level (csel, not a branch) with the comparison count invariant.

THE BUMP IS SAFE BECAUSE THE BACKENDS WENT FIRST
logos-protocol only DECLARES this ABI; every backend owes the definition,
and that gap shipped twice. logos-cpp-sdk#147 and logos-rust-sdk#47 already
define logos_module_set_call_caller, gated on >= 0.6 and therefore inert
until this lands. Verified on x86_64-linux: with this tree as the protocol,
BOTH backends at master pass their ABI checks and define the export;
manifest reports 0.6.0 with 11 exports. No repo is red at any point.

Rule 6 is now normative on a point the two backends had silently diverged
on — a present-but-unreadable "instance" is dropped and the module still
identified — each having pinned its own answer with a passing test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:28:00 -03:00

384 lines
15 KiB
CMake

cmake_minimum_required(VERSION 3.14)
project(LogosProtocol)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
# Find Qt packages — Qt is an implementation detail of the qt_remote (QRO)
# and qt_local transports plus the Qt value types used at the QRO boundary.
# The PUBLIC C ABI (logos_protocol.h) is Qt-free.
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core RemoteObjects)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects)
# Plain-C++ transport dependencies (no Qt).
#
# Boost.System is header-only, but Boost 1.87 only EXPORTS the Boost::system
# imported target when the component is requested -- hence the explicit
# COMPONENTS below. Boost 1.89 then removed the compiled boost_system library
# outright, so there is no boost_systemConfig.cmake at all and requesting it as
# REQUIRED hard-fails ("Could not find a package configuration file provided by
# boost_system"). Ask for it optionally and fall back to Boost::headers, which
# carries the same header-only error_code either way.
find_package(Boost REQUIRED)
find_package(Boost QUIET OPTIONAL_COMPONENTS system)
if(TARGET Boost::system)
set(LOGOS_BOOST_SYSTEM Boost::system)
else()
set(LOGOS_BOOST_SYSTEM Boost::headers)
endif()
message(STATUS "Boost.System target: ${LOGOS_BOOST_SYSTEM} (Boost ${Boost_VERSION})")
find_package(OpenSSL REQUIRED)
find_package(nlohmann_json REQUIRED)
set(PROTOCOL_SOURCES
logos_protocol.h
logos_module_impl.h
logos_call_error.h
logos_shared_api.h
logos_protocol.cpp
logos_types.cpp
logos_types.h
logos_api_client.cpp
logos_api_client.h
logos_api_consumer.cpp
logos_api_consumer.h
logos_subscription_state.h
module_proxy.cpp
module_proxy.h
# WHO is calling the dispatch on this thread. Produced by
# ModuleProxy::authorize, consumed by the host-side invokable the
# generated glue reaches by name.
logos_caller_scope.cpp
logos_caller_scope.h
token_manager.cpp
token_manager.h
logos_mode.h
logos_instance.h
logos_socket_paths.h
logos_socket_paths.cpp
plugin_registry.h
logos_object.h
logos_thread_marshal.h
logos_provider_interface.cpp
logos_provider_interface.h
logos_json_convert.cpp
logos_json_convert.h
logos_transport.h
logos_transport.cpp
logos_transport_config.h
logos_transport_config_json.h
logos_transport_config_json.cpp
logos_transport_factory.cpp
logos_transport_factory.h
logos_registry.h
logos_registry_factory.h
logos_registry_factory.cpp
implementations/qt_local/local_transport.cpp
implementations/qt_local/local_transport.h
implementations/qt_remote/remote_transport.cpp
implementations/qt_remote/remote_transport.h
implementations/qt_remote/qt_remote_registry.h
implementations/qt_remote/qt_remote_registry.cpp
implementations/mock/mock_store.cpp
implementations/mock/mock_store.h
implementations/mock/mock_transport.cpp
implementations/mock/mock_transport.h
implementations/mock/mock_registry.h
implementations/mock/logos_mock.h
# Plain-C++ wire stack (no Qt)
implementations/plain/rpc_value.h
implementations/plain/rpc_message.h
implementations/plain/rpc_message.cpp
implementations/plain/wire_codec.h
implementations/plain/json_mapping.h
implementations/plain/json_mapping.cpp
implementations/plain/json_codec.h
implementations/plain/json_codec.cpp
implementations/plain/cbor_codec.h
implementations/plain/cbor_codec.cpp
implementations/plain/rpc_framing.h
implementations/plain/rpc_framing.cpp
implementations/plain/incoming_call_handler.h
implementations/plain/rpc_connection.h
implementations/plain/rpc_server.h
implementations/plain/rpc_server.cpp
implementations/plain/io_context_pool.h
implementations/plain/io_context_pool.cpp
implementations/plain/qvariant_rpc_value.h
implementations/plain/qvariant_rpc_value.cpp
implementations/plain/plain_logos_object.h
implementations/plain/plain_logos_object.cpp
implementations/plain/plain_transport_host.h
implementations/plain/plain_transport_host.cpp
implementations/plain/plain_transport_connection.h
implementations/plain/plain_transport_connection.cpp
)
add_library(logos_protocol STATIC ${PROTOCOL_SOURCES})
# Link Qt + plain-C++ transport deps. Boost.Asio is header-only
# *except* for `boost::system::error_code` (and friends), which lives
# in libboost_system — see the matching note in logos-cpp-sdk.
# Boost.Asio's IOCP backend calls the Winsock EXTENSION functions AcceptEx and
# GetAcceptExSockaddrs, which live in mswsock rather than ws2_32. Without this
# the plain TCP transport compiles cleanly and then fails at link with
# "undefined reference to `AcceptEx'" out of
# win_iocp_socket_service_base::start_accept_op. ws2_32 supplies core Winsock
# and is listed explicitly rather than relied upon arriving via Qt::Network.
if(WIN32)
set(LOGOS_PROTOCOL_PLATFORM_LIBS ws2_32 mswsock)
else()
set(LOGOS_PROTOCOL_PLATFORM_LIBS "")
endif()
target_link_libraries(logos_protocol PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::RemoteObjects
Boost::headers
${LOGOS_BOOST_SYSTEM}
OpenSSL::SSL
OpenSSL::Crypto
nlohmann_json::nlohmann_json
${LOGOS_PROTOCOL_PLATFORM_LIBS}
)
target_include_directories(logos_protocol PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_local>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_remote>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/mock>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/plain>
$<INSTALL_INTERFACE:include>
$<INSTALL_INTERFACE:include/implementations/qt_local>
$<INSTALL_INTERFACE:include/implementations/qt_remote>
$<INSTALL_INTERFACE:include/implementations/mock>
$<INSTALL_INTERFACE:include/implementations/plain>
)
set_target_properties(logos_protocol PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)
install(TARGETS logos_protocol
EXPORT logos-protocolTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
# Shared variant of the same sources, exporting the language-neutral lp_*
# C ABI (extern "C", default visibility) as a loadable library. This exists
# 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.
#
# 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
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::RemoteObjects
Boost::headers
${LOGOS_BOOST_SYSTEM}
OpenSSL::SSL
OpenSSL::Crypto
nlohmann_json::nlohmann_json
${LOGOS_PROTOCOL_PLATFORM_LIBS}
)
# Only the SHARED build marks the lp_* C ABI dllexport; the static archive
# 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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_local>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/qt_remote>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/mock>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/implementations/plain>
$<INSTALL_INTERFACE:include>
$<INSTALL_INTERFACE:include/implementations/qt_local>
$<INSTALL_INTERFACE:include/implementations/qt_remote>
$<INSTALL_INTERFACE:include/implementations/mock>
$<INSTALL_INTERFACE:include/implementations/plain>
)
# Same basename as the archive (liblogos_protocol.{so,dylib}); the LIBRARY
# install below lands it in $out/lib next to liblogos_protocol.a.
set_target_properties(logos_protocol_shared PROPERTIES
OUTPUT_NAME logos_protocol
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}"
"$<TARGET_FILE:logos_protocol>" "*"
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
FILE logos-protocolTargets.cmake
NAMESPACE logos-protocol::
DESTINATION lib/cmake/logos-protocol
)
include(CMakePackageConfigHelpers)
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/logos-protocolConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/logos-protocolConfig.cmake"
INSTALL_DESTINATION lib/cmake/logos-protocol
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/logos-protocolConfigVersion.cmake"
VERSION 0.1.0
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/logos-protocolConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/logos-protocolConfigVersion.cmake"
DESTINATION lib/cmake/logos-protocol
)
# Install headers. Moved headers keep the exact relative paths they had when
# they shipped from logos-cpp-sdk, so downstream `#include "token_manager.h"`
# (etc.) keep resolving unchanged through propagated include dirs.
install(FILES
logos_protocol.h
logos_module_impl.h
# The canonical LIDL <-> JSON codec. Generated module glue includes it, so it
# has to ship with the library's headers, not just in the source export.
logos_codec.h
logos_call_error.h
logos_rpc_status.h
# Windows single-provider marker. token_manager.h / logos_api_client.h /
# logos_types.h include it, and so does logos-qt-sdk's logos_api.h, so it
# has to ship even though nothing in this library's own sources needs it.
logos_shared_api.h
logos_types.h
logos_api_client.h
logos_api_consumer.h
# Named by logos_api_client.h and logos_api_consumer.h alike; the client only
# forward-declares the consumer, so the enum cannot live in the consumer header.
logos_subscription_state.h
module_proxy.h
# Included by logos-plugin-qt's logos_api.cpp to answer
# LogosAPI::currentCallerJson(), so it has to ship with the headers.
logos_caller_scope.h
token_manager.h
logos_mode.h
logos_instance.h
logos_socket_paths.h
plugin_registry.h
logos_object.h
logos_thread_marshal.h
logos_provider_interface.h
logos_json_convert.h
logos_transport.h
logos_transport_config.h
logos_transport_config_json.h
logos_transport_factory.h
logos_registry.h
logos_registry_factory.h
DESTINATION include
)
install(FILES
implementations/plain/rpc_value.h
implementations/plain/rpc_message.h
implementations/plain/wire_codec.h
implementations/plain/json_codec.h
implementations/plain/rpc_framing.h
DESTINATION include/implementations/plain
)
install(FILES
implementations/qt_local/local_transport.h
DESTINATION include/implementations/qt_local
)
install(FILES
implementations/qt_remote/remote_transport.h
implementations/qt_remote/qt_remote_registry.h
DESTINATION include/implementations/qt_remote
)
install(FILES
implementations/mock/mock_store.h
implementations/mock/mock_transport.h
implementations/mock/mock_registry.h
implementations/mock/logos_mock.h
DESTINATION include/implementations/mock
)