mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
* fix(events): the event bridge converts through the canonical helper
setEventListenerStdBridge adapts the universal event callback (name + JSON
string) to the Qt EventCallback (name + QVariantList). It is the event-path
counterpart of callMethodStdBridge, but it did the conversion itself:
callMethodStdBridge -> logos::nlohmannToQVariant (canonical)
setEventListenerStdBridge -> QJsonDocument::fromJson
+ QJsonValue::toVariant (Qt's parser)
Two consequences, both measured by the LIDL conformance matrix as M6:
* a uint64 above int64max degraded to a double. Qt 6 backs QJsonValue with
QCborValue, so integers up to int64 DID survive — only values with no
integral representation there fell back to double. echoUint(2^64-1) was
exact while uintEvent(2^64-1) arrived as 1.8446744073709552e+19: same
value, same process, one hop later.
* canonical tagged bytes {"_bytes": ...} were not decoded, arriving as a
QVariantMap where the method path yields a QByteArray. This never showed up
end-to-end because the undecoded map round-trips to JSON and the python
client decodes the tag itself — but a C++ or QML event subscriber got a map.
Both now go through logos::nlohmannArgsToQVariantList, which the generated
cdylib emitTrampoline already used. Numbers and bytes no longer depend on
whether a value left the module as a return or as an event.
Not the residue of the codec convergence, despite how M6 was originally
registered. #29 converged six copies of the VALUE codec; this was a seventh
conversion inside an ADAPTER, which that scope never touched. It is also not on
the providers' own path — a Qt provider stores its callback verbatim and a
cdylib provider already converted correctly. The one live caller is the
logoscore daemon's CoreServiceImpl, which forwards every watched module event;
that is why C++ and Rust providers measured identically.
Why it survived: the bridge appeared in the test suite once, in
test_universal_provider_dispatch.cpp, purely to satisfy the pure virtual. No
test asserted anything about an event payload. The method path got 15 contract
tests in #29; the event path got none.
tests: 11 new cells pin the bridge directly — uint64 past int64max, 2^53+1,
int64::min, large integers nested in containers, tagged bytes at top level and
at depth, plus the shapes that already worked (multi-param order, double staying
double, null elements, empty payload, the non-array raw-string fallback) so a
future rewrite cannot quietly drop them. 210/210.
verified: logos-cpp-sdk, logos-qt-sdk, logos-liblogos and logos-logoscore-cli
all green against this build; the conformance matrix goes 156 -> 158 pass with
M6's two cells retired, and the ext table stays 40/40.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(events): pin the signedness rule the convergence brings with it
nlohmannArgsToQVariantList classifies every non-negative integer as unsigned, so
a LIDL `int` event argument now arrives as ULongLong where it used to be
LongLong. That matches what nlohmannToQVariant (the method path) and the cdylib
emitTrampoline already did — the surfaces now agree — but it is an observable
metatype change that nothing asserted.
Pinned in both directions (non-negative -> ULongLong, negative -> LongLong) so
it stays a decision rather than a side effect. Value-level reads are unaffected.
212/212.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(plain): RpcValue can represent a uint64 above int64max
The plain (tcp/tcp_ssl) wire squeezed every unsigned value through int64_t, so a
LIDL `uint` above int64max wrapped — independently in each direction:
outbound qvariant_rpc_value.cpp QMetaType::ULongLong -> int64_t(...)
inbound json_mapping.cpp is_number_unsigned -> get<int64_t>()
Neither wraps loudly: .get<int64_t>() past int64max returns -1 with no
exception. Two peers both running this code agreed on -1, so nothing looked
broken from inside — and no plain-tier test used an integer outside int32 range.
Measured over real tcp before the fix:
echoUint(2^63) -> -9223372036854775808
echoUint(2^64-1) -> -1
This was never a wire-format constraint. Both codecs carry uint64 natively (CBOR
emits major type 0, `1b ff..ff`) and the envelope's own `id` field already
crossed this wire as uint64_t. Only RpcValue *payloads* could not represent it.
RpcValue gains a uint64_t alternative, used through `makeInteger()` and ONLY for
values above int64max — the sole case where int64_t loses information. Anything
broader would change the representation of every non-negative integer already on
this wire, and since std::variant equality compares the alternative index it
would break comparisons against int64-built values, to fix nothing. Small
unsigned values keep crossing as signed, pinned by a test so the rule stays
visible.
Also fixes an off-by-one in the QJsonValue::Double -> int64 guard while here:
double(int64max) rounds UP to exactly 2^63, so `d <= double(int64max)` admitted
2^63 and then ran int64_t(d) out of range — undefined behaviour, saturating on
arm64 and INT64_MIN on x86-64. Now a strict `<` against 2^63.
tests: 14 new. Both codecs round-trip 2^64-1 flat and nested; negatives stay
signed; the Qt boundary is exact in both directions; the narrow representation
rule and the 2^63 guard are pinned. 226/226.
verified end-to-end, cross-process, with a negative control: the new 64-bit
boundary cases in logos-logoscore-py fail on the pinned protocol over tcp with
exactly the values above, and all 68 pass with this build — on local, tcp and
tcp_ssl alike.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
73 lines
3.0 KiB
C++
73 lines
3.0 KiB
C++
#include "logos_provider_interface.h"
|
|
#include "logos_json_convert.h"
|
|
#include <QDebug>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LogosProviderObject — universal virtual defaults
|
|
// ---------------------------------------------------------------------------
|
|
|
|
nlohmann::json LogosProviderObject::callMethodStd(const std::string& /*methodName*/,
|
|
const nlohmann::json& /*args*/)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<LogosMethodMetadata> LogosProviderObject::getMethodsStd()
|
|
{
|
|
return {};
|
|
}
|
|
|
|
void LogosProviderObject::setEventListenerStd(UniversalEventCallback /*callback*/)
|
|
{
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// LogosProviderObject — bridge helpers (Qt-free providers delegate here)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
QVariant LogosProviderObject::callMethodStdBridge(const QString& methodName, const QVariantList& args)
|
|
{
|
|
nlohmann::json jArgs = nlohmann::json::array();
|
|
for (const QVariant& a : args)
|
|
jArgs.push_back(logos::qvariantToNlohmann(a));
|
|
|
|
nlohmann::json result = callMethodStd(methodName.toStdString(), jArgs);
|
|
return logos::nlohmannToQVariant(result);
|
|
}
|
|
|
|
QJsonArray LogosProviderObject::getMethodsStdBridge()
|
|
{
|
|
return logos::methodsToJsonArray(getMethodsStd());
|
|
}
|
|
|
|
void LogosProviderObject::setEventListenerStdBridge(EventCallback callback)
|
|
{
|
|
setEventListenerStd([callback](const std::string& eventName, const std::string& data) {
|
|
if (!callback) return;
|
|
QVariantList qData;
|
|
// Parse with nlohmann and convert with the SAME helper the method path
|
|
// uses (callMethodStdBridge above), so a value does not depend on
|
|
// whether it left the module as a return or as an event.
|
|
//
|
|
// This used QJsonDocument::fromJson + QJsonValue::toVariant. Qt 6 backs
|
|
// QJsonValue with QCborValue, so integers up to int64 survived — but a
|
|
// uint64 above int64max has no integral representation there and fell
|
|
// back to double: 18446744073709551615 arrived as 1.8446744073709552e+19,
|
|
// exact on the method path and rounded one hop later. The Qt parser also
|
|
// has no notion of the canonical {"_bytes": ...} tag, so byte payloads
|
|
// arrived as a QVariantMap and only survived because that map round-trips
|
|
// to a consumer that decodes the tag itself.
|
|
//
|
|
// parse(..., nullptr, false) is the non-throwing form: malformed input
|
|
// yields a discarded value and takes the raw-string fallback below,
|
|
// which is the behaviour QJsonDocument gave for unparseable data.
|
|
const nlohmann::json payload = nlohmann::json::parse(data, nullptr, false);
|
|
if (!payload.is_discarded() && payload.is_array()) {
|
|
qData = logos::nlohmannArgsToQVariantList(payload);
|
|
} else {
|
|
qData.append(QString::fromStdString(data));
|
|
}
|
|
callback(QString::fromStdString(eventName), qData);
|
|
});
|
|
}
|