fix(generator): the Qt consumer must not flatten a rejection into an empty value - #129
Open
dlipicar wants to merge 2 commits into
Open
fix(generator): the Qt consumer must not flatten a rejection into an empty value#129dlipicar wants to merge 2 commits into
dlipicar wants to merge 2 commits into
Conversation
…empty value
A provider that REJECTS a call answers the canonical
{"code":"dispatch_failed", "message":..., "origin":...} object as its RESULT,
not as a transport error — the same object from every provider flavour
(logos-qt-sdk dispatchFailedVariant, the generated cdylib dispatch, the Rust
provider's args::dispatch_failed).
The generated Qt consumer wrapper converted it like any other value, which
ERASES it. `_result.toList()` on that map is `[]`, `.toString()` is "",
`.toLongLong()` is 0 — so `echoUintList([1, -1, 3])`, answered `dispatch_failed`
by the provider, reached the caller as `[]`: the whole list, not the bad
element, and indistinguishable from "the provider returned nothing". Losing the
error is a consumer bug whatever the provider did.
Reported through the channel this surface already uses for a failed call: the
`logos::CallError*` out-parameter every generated sync method carries (today
"object_unavailable" when the target cannot be acquired). No signature changes,
no return value changes — a caller that passes `err` now sees
code="dispatch_failed" with the provider's diagnostic and origin; a caller that
does not gets the same default it always got, plus the qWarning the wrapper
already emits for a failed call.
* the detector is emitted once per wrapper, in an anonymous namespace, and
matches EXACTLY (three string fields, that code) for the same reason
logos_rpc_status.h's isUnauthorizedSentinel matches exactly: an `any` or map
return carrying user data must never false-match.
* the result is now captured for `void` returns too — a void method can be
rejected, and the rejection object is the only place that says so.
* the async overload's callback takes the value alone and has no error
parameter; giving it one would change the generated public surface (which
logos-qt-sdk's veneer mirrors 1:1), so an async rejection is logged rather
than delivered. Recorded in cpp-generator/docs/project.md.
Scope: CONSUMER side only. The provider half of registry entry Q1 (a Qt-typed
provider cannot validate the ELEMENT of a typed numeric array, because a C++
signature spells [uint] and [any] alike as QVariantList) is explicitly out of
scope and unchanged. The lp wrapper is untouched — its generated output is
byte-identical before and after.
…slation unit
`logos_sdk.cpp` textually `#include`s every generated `<dep>_api.cpp`, so a
module with more than one dependency compiles several copies of the detector
into ONE translation unit:
test_fullapi_rust_api.cpp:15:6: error: redefinition of 'logosDispatchRejection'
Internal linkage covers the separate-TU case; only the preprocessor covers this
one. Found by building test_fullapi_qtproxy (3 wrappers: two concrete deps plus
the bound `full_api` interface) against this generator — a single-wrapper
contract cannot reach it.
📊 cpp-sdk doc-test reportThe real accounts module, run through a logoscore daemon with the whole stack built against this commit of the C++ SDK — rendered alongside the commands actually run and their output (updated each run, commit Pages can take a minute to update after the run finishes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A Qt consumer flattened a provider rejection into an empty value, so the caller
never saw the error.
A provider that REJECTS a call answers the canonical
{"code":"dispatch_failed", "message":…, "origin":…}object as its RESULT, not as atransport error. Every provider flavour produces the same object — logos-qt-sdk's
dispatchFailedVariant, the generated cdylib dispatch, the Rust provider'sargs::dispatch_failed.The generated Qt consumer wrapper (
legacy/main.cpp→generateInterfaceWrappers→generator_lib.cppmakeSource, i.e. the path every real module builds through:logos-plugin-qt
buildPlugin.nixrunslogos-cpp-generator --metadata … --general-only --api-style qt) converted it like anyother value, which erases it:
So
echoUintList([1, -1, 3]), answereddispatch_failedby the provider, reached thecaller as
[]— the whole list, not the bad element, and indistinguishable from "theprovider returned nothing". That is registry entry Q1's consumer half
(logos-test-modules
conformance/known.json).The fix
Report it through the channel this surface already uses for a failed call — the
logos::CallError*out-parameter every generated sync method carries (todayobject_unavailablewhen the target module cannot be acquired). The envelope maps 1:1onto
logos::CallError's{code, message, origin}.errnowsees
code="dispatch_failed"with the provider's diagnostic and origin; a caller thatdoes not gets exactly the value it always got, plus the
qWarningthe wrapper alreadyemits for a failed call. The generated header is byte-identical.
exactly — three string fields and that code — for the same reason
logos_rpc_status.h'sisUnauthorizedSentinelmatches exactly: anany/map returncarrying user data must never false-match.
voidreturns too: a void method can be rejected, andthe rejection object is the only place that says so.
function in an anonymous namespace, and such a contract's wrapper stays byte-identical).
Scope
Consumer side only. Q1's provider half — a Qt-typed provider cannot validate the
ELEMENT of a typed numeric array, because a C++ signature spells
[uint]and[any]alike as
QVariantList— is explicitly out of scope and untouched; the project decidedQt-typed PROVIDER hardening is no longer a goal.
The lp wrapper is untouched:
makeSourceLpis unchanged and its generated output isbyte-identical before and after (verified below).
Verified by running
1. Real contract + real generated consumer + real provider. Generated
full_api_api.{h,cpp}fromtest-fullapi-qtproxy-module/interfaces/full_api.lidlwiththe pre-fix and post-fix generators (
--general-only --api-style qt), compiled eachverbatim into the same harness, and drove the prebuilt
test_fullapi_cppcdylibthrough its own C ABI (
logos_module_dispatch) — only the transport hop is stubbed. Theprovider's real answer:
echoUintList([1,-1,3], &err)→err.code''dispatch_failederr.message''expected unsigned integer at arg0[1], got numbererr.origin''test_fullapi_cpp[](indistinguishable)[]plus a non-ok errerr: logged[1,2,3]clean + round-tripscodeis not a rejection, value survivesechoInt(3)→ 3, no errorPre-fix: 7 failing checks. Post-fix: 0. The controls pass on both arms, so the
harness is not simply "red before, green after".
2. Unit tests. Six new
MakeSourceTestcases. The four positive ones fail on theunfixed generator (
88 QtEmitsRejectionDetector,89 QtSyncFoldsRejectionIntoCallError,90 QtVoidReturnStillCapturesResult,91 QtAsyncLogsRejection) and pass after;nix build .#testsis 224/224 green with the fix.3. Negative control. Regenerating the same contract with
--api-style lpgivesbyte-identical output before and after. The comparison is not vacuous:
qtvslpoutput differs on the same generator. (Note for anyone repeating this:
--lidlmoderoutes to
experimental/lidl_gen_client.cppand ignores--api-style— a qt-vs-lpdiff run that way is byte-identical for the wrong reason and cannot fail.)
4. Built through the real build path.
nix build .#test_fullapi_qtproxyinlogos-test-modules with
--override-input logos-module-builder/logos-cpp-sdk <this branch>. This caught a defect none of the checks above could:logos_sdk.cpptextually
#includes every generated<dep>_api.cpp, so a module with more thanone dependency compiles several copies of the detector into ONE translation unit —
error: redefinition of 'logosDispatchRejection'. Internal linkage does not help there;the second commit adds a preprocessor guard, with a unit test that concatenates two
generated wrappers the way the umbrella does. A single-wrapper contract cannot reach
this, which is why generating from one contract and compiling it was not enough.
5. Real daemon, real transport, real modules.
test_fullapi_qtproxy,test_fullapi_cppandtest_fullapi_rustbuilt from logos-test-modules with--override-input logos-module-builder/logos-cpp-sdk <this branch>, loaded into alogoscoredaemon over LocalSocket/QtRO:The same three modules built without the override (pristine
mastergenerator), samedaemon, same call: the log shows the identical wrapper call sequence
(
LogosAPIConsumer: Calling invokeRemoteMethod … "echoUintList"→RemoteLogosObject::callMethod) and then nothing — 0 occurrences ofremote call failed. So the new line is the fix, not the harness.The CLI still sees
[]in both arms because the proxy's forwarding method callsp.echoUintList(v)without anerrout-param and has nowhere to put one in itsQVariantListreturn — see the Q1 follow-up below.Also checked: the built plugin binary carries the new code — 99
…Async: remote call failedstrings post-fix vs 0 pre-fix, and 231 vs 132 warning strings overall.Both providers produce the same envelope and both are picked up: the Rust provider
answers
{"code":"dispatch_failed","message":"expected integer at arg0[1], got number","origin":"test_fullapi_rust"}and the fixed consumer reports it identically.Follow-ups (not in this PR)
the rejection, but the matrix cells
hostile/[uint]/negative-element,hostile/[uint]/fractional-element,hostile/[int]/fractional-elementstill read[]through
test_fullapi_qtproxy, because the proxy's forwarding methods callp.echoUintList(v)without anerrout-param and have nowhere to put it in theirown return type. Making those cells answer
dispatch_failedis a test-modules change(propagate the
CallError), not a generator one.qt-generator --backend consumeremits the same shape of bug:return logos::qt::fromWire<QVariantList>(_r);with_errcarrying only transportfailures. Same fix, different repo.
cpp-generator/docs/project.md); that needs an API change, not a codegen fix.