Files
logos-plugin-qt/cpp/logos_provider_object.cpp
T
Dario Gabriel LipicarandClaude Opus 5 c459e47787 feat(glue): pull the caller from the host image and push it across the C ABI
Everything under this has merged — the protocol resolves the caller and
opens a scope (#70), both backends define logos_module_set_call_caller
(cpp-sdk #147, rust-sdk #47) — and nothing connected the two, so
currentCaller() returned Unknown everywhere. This is the wire.

Per dispatch, all on the dispatch thread: pull the caller document back out
of the HOST image, push it into the module image, dispatch, pop.

WHY THE PULL IS AN invokeMethod AND NOT A CALL
The host binary and the module plugin EACH define LogosAPI — meta-object
included — with their own statics at distinct addresses and no undefined
reference to the other's. Mach-O is TWOLEVEL, PE has no interposition. A
direct logosAPI()->currentCallerJson() binds to the PLUGIN copy and reads
the PLUGIN thread-local: empty, forever, silently, on macOS and Windows.
invokeMethod resolves through metaObject()/qt_metacall, which are virtual
and whose vptr the HOST constructor wrote, so it lands in host code on the
calling thread. Same channel initLogos and aboutToUnload already use.

IN MULTI THE PULL IS BEFORE THE CAPTURE
callMethod is entered on the dispatch thread and captures by value into
QThread::create, so the pull happens there and the JSON rides along; the
push/pop triple moves verbatim into the worker. Pulling inside the worker
also compiles, and every multi call would read Unknown because that thread
never had a scope. A test asserts the source ORDER, and it was driven red
by making exactly that mistake.

The invokable's name was not chosen here: logos-protocol master already
names currentCallerJson in three places, including a CMakeLists comment
saying the header ships so this file can answer it.

Two checks beyond the wire, because the by-name test alone left gaps:

  * test-glue-compiles — NOTHING in this repo compiled the emitted glue.
    test-qt-host-generator.cpp documents a bug that escaped through exactly
    that hole. Both branches now build as a real Qt plugin with the C ABI
    stubbed and -Wl,--no-undefined, so a missing symbol fails at link
    rather than at a user's dlopen.
  * test-caller-invokable — six runtime assertions, including that a scope
    open on one thread is INVISIBLE on another, which is the actual
    justification for the multi placement.

The contract test strips comments before its negative assertions (both
files legitimately NAME the forbidden calls in prose) and guards that with
a positive control, so the negatives cannot pass vacuously.

Guard is MAJOR-aware expanded arithmetic. Dropping only the MAJOR > 0 arm
still passes a grep, so the test resolves at 1.0 and asserts the call
survives; that mutation was driven red too.

11 checks green on x86_64-linux, the three new ones green on
aarch64-darwin, and the Windows mingw cross builds.

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

117 lines
5.3 KiB
C++

#include "logos_provider_object.h"
#include "logos_api.h"
#include "token_manager.h"
// LOGOS_PROTOCOL_VERSION_{MAJOR,MINOR} for the guard below — by name, so an
// undefined macro cannot quietly turn the guard FALSE and leave every module
// with a permanently unknown caller.
#include "logos_protocol.h"
#include <QDebug>
#include <QMetaObject>
#include <QString>
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 || \
(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 6))
// Only for logos::callerUnknownJson() — the fail-closed document. NOT for
// logos::currentInboundCallerJson(): calling that here would read THIS image's
// thread-local, which is the bug this whole file exists to avoid. See below.
#include "logos_caller_scope.h"
#endif
// The LogosProviderObject universal-interface defaults and Std bridges moved
// to logos-protocol (logos_provider_interface.cpp) together with the abstract
// interface. What remains here is LogosProviderBase — the base the generated
// Qt plugin glue (<name>_cdylib_glue.cpp) derives from — because it talks to
// LogosAPI, which layers above the protocol. It used to be hand-derived by
// `interface: "provider"` module code; that interface is gone, and module code
// now derives from logos-cpp-sdk's Qt-free LogosModuleContext instead.
// ---------------------------------------------------------------------------
// LogosProviderBase
// ---------------------------------------------------------------------------
void LogosProviderBase::init(void* apiInstance)
{
m_logosAPI = static_cast<LogosAPI*>(apiInstance);
qDebug() << "[LogosProviderObject] LogosProviderBase::init called";
onInit(m_logosAPI);
}
bool LogosProviderBase::informModuleToken(const QString& moduleName, const QString& token)
{
if (!m_logosAPI) {
qWarning() << "[LogosProviderObject] informModuleToken: LogosAPI not available";
return false;
}
TokenManager* tokenManager = m_logosAPI->getTokenManager();
if (!tokenManager) {
qWarning() << "[LogosProviderObject] informModuleToken: TokenManager not available";
return false;
}
qDebug() << "[LogosProviderObject] Saving token for module:" << moduleName;
tokenManager->saveToken(moduleName, token);
return true;
}
// THE PULL. Everything about the four lines below is deliberate; see the
// declaration in logos_provider_object.h and logos_caller_scope.h in
// logos-protocol for the measurement behind it.
//
// invokeMethod, NOT m_logosAPI->currentCallerJson(). This translation unit is
// compiled into the MODULE image, which links its own copy of LogosAPI —
// meta-object included — with its own function-local statics at its own
// addresses and no undefined reference to the host's. A direct call binds to
// THIS image's copy and reads THIS image's caller thread-local, which no
// CallerScope ever wrote. It is silently empty forever on macOS and Windows,
// and correct on Linux, so a green Linux run proves nothing about it.
// invokeMethod resolves through metaObject()/qt_metacall — virtual, vptr
// written by the HOST's constructor — and therefore lands in host code.
//
// BY NAME, so this string and the Q_INVOKABLE in logos_api.h are one
// contract with no compiler between them. tests/test-caller-contract.nix is
// what holds them together; without it a rename is a silent, permanent
// Unknown across the whole fleet.
//
// DirectConnection, because the answer is per-THREAD. The scope is open on
// the thread the dispatch was delivered on, and this call must read that
// thread's slot rather than hop to LogosAPI's owner thread. (Qt also refuses
// Q_RETURN_ARG on a queued connection, so the alternative is a runtime
// warning and a permanent Unknown, not a build failure.)
std::string LogosProviderBase::currentCallerJson() const
{
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 || \
(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 6))
QString caller;
if (m_logosAPI
&& QMetaObject::invokeMethod(m_logosAPI, "currentCallerJson",
Qt::DirectConnection,
Q_RETURN_ARG(QString, caller))
&& !caller.isEmpty()) {
return caller.toStdString();
}
// No LogosAPI yet, a host too old to carry the invokable, or an empty
// answer. Fail closed to the ONE document logos-protocol produces for
// "could not name the caller" — never to an empty string, which on the
// module-impl C ABI is not a weaker identity but a different operation.
return logos::callerUnknownJson();
#else
// Below protocol 0.6 there is no caller surface at either end: no
// logos_module_set_call_caller for the glue to push into, and no
// callerUnknownJson() to name. Empty is the absence of the whole mechanism,
// and the glue's push is guarded on this same expression, so nothing reads
// it.
return std::string();
#endif
}
void LogosProviderBase::emitEvent(const QString& eventName, const QVariantList& data)
{
if (m_eventCallback) {
qDebug() << "[LogosProviderObject] emitEvent:" << eventName;
m_eventCallback(eventName, data);
} else {
qWarning() << "[LogosProviderObject] emitEvent: no listener set for" << eventName;
}
}