mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
master
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b505ee95eb |
fix(generator): widen BOTH rejection detectors to a CLOSED SET of codes
The two emitted detectors — logosDispatchRejection (QVariant, Qt surface) and
logosDispatchRejectionJson (nlohmann, lp surface) — each matched the single
literal "dispatch_failed". Providers have been answering a wrong argument COUNT
with "invalid_args" all along: this repo's own cdylib dispatch emits it
(experimental/lidl_gen_cdylib.cpp:805) and so does logos-rust-sdk's
args::invalid_args. Nothing detected it. The refusal therefore arrived as a
VALUE and the return table erased it — `_result.toList()` on that map is `[]`,
`.toString()` is "", `.toLongLong()` is 0 — so a caller could not tell "you sent
me the wrong number of arguments" from "the provider returned nothing".
Measured on the untyped surface, where the erasure is visible:
logosctl call test_basic_module isPositive (missing required argument)
-> exit 0, status:"ok", result {"code":"invalid_args", ...}
The set is now {dispatch_failed, invalid_args, unknown_method}, in ONE
kRejectionCodes array. Both emitters build their condition text from it, so the
Qt and Qt-free twins cannot drift apart — which is what two hand-written copies
of the same literal were always going to do.
"unknown_method" is listed before any provider emits it, on purpose. An unknown
method is currently answered with a bare null, byte-identical to a legitimate
null return (logos-protocol logos_protocol.h says so outright), and closing that
is a provider-contract change across the SDKs. Detectors go first because
widening one is backwards-compatible on its own — nothing emits the code, so
nothing changes — whereas a new provider code shipped against narrow detectors
would arrive at consumers as DATA: the same silent-success bug, freshly minted.
The set stays CLOSED. NOT "any three-key object with a code": a method may
legitimately return a three-string map, and an `any` return certainly can, so a
shape-only match would let user data impersonate a refusal. Every guard above
the compare — exactly three keys, all three present, all three strings — is
untouched.
WHY FIVE COPIES AND NOT ONE. The other four are logos-qt-sdk's byte-identical
lidl_gen_qt_consumer.cpp, logos-rust-sdk's args::as_dispatch_rejection, and
logos-logoscore-cli's core_service/call_envelope.cpp. Two shared homes were
considered, both rejected here and both recorded at kRejectionCodes: a shared
EMITTER in share/lidl-frontend (the channel exists — it already ships
lidl_emit_common to logos-qt-generator) collapses 2 of 5 and turns four
independently landable fixes into an ordered stack; a runtime predicate in
logos-protocol is the principled end state, and is how the analogous CONVERSION
duplication was actually solved, but it trades a text-level duplication for a
build-level version coupling — the emitted body is self-contained today, so a
wrapper compiles against whatever protocol its module pins. Each repo instead
holds the vocabulary in one named place, so a drift is visible.
Tests: each code asserted present in the emitted condition on both surfaces,
plus the negatives that keep the match closed — exactly three comparisons and
no more, the shape guards still emitted, and the compare still ahead of
`return true`. 316/316 ctest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9c05d4ad11 |
feat(lp): emit the AsyncResult twin, and fix the LpClient create race (#142)
* feat(lp): emit the AsyncResult twin, and fix the LpClient create race
Two related changes to the Qt-free (ApiStyle::Lp) consumer surface.
1. logos::LpClient::ensure() published its lazily-created lp_client through a
plain pointer with no synchronization. Two threads reach a dep's FIRST call
concurrently more often than the lazy-init shape suggests: a
concurrency:"multi" module dispatches handlers on concurrent QThreads, and
any module running a worker of its own (an HTTP handler, a chain-sync pump)
races that worker against the dispatch thread. So this was a data race, and
it leaked whichever client lost.
A mutex around the body is the obvious fix and the wrong one: for a Qt-affine
transport lp_client_create marshals construction onto the Qt main thread and
BLOCKS there, so a worker holding the lock would wait for the main thread
while the main thread, reaching the same ensure() from an inbound call, waits
for the lock — trading a data race for a deadlock. Construct outside any lock
and publish with a CAS instead; the loser destroys its own client, which
lp_client_destroy permits from any thread. A failed create is not latched.
2. `<name>AsyncResult` is now emitted for the lp surface, matching the Qt one.
It was withheld for a reason that belonged to the transport rather than the
emitter: lp_invoke_async used to hard-code `cb(1, ...)`, so an AsyncResult
over it would have reported ok() for a call to a module that was not even
loaded — an error channel that lies is worse than none. logos-protocol#40
fixed that, and the new logos::LpClient::invokeAsyncResult surfaces the
failure in C++.
The generated twin also folds a provider REJECTION: a provider that ran and
refused answers {"code": "dispatch_failed", ...} as its RESULT, not as a
transport error, so the decode would otherwise erase it into a default value.
The lp SYNC path folds it too now, as the Qt sync path already did — with no
qWarning fallback, since a Qt-free wrapper has no logger to fall back to.
lp `<name>Async` is deliberately unchanged: its callback takes the value
alone, exactly as on the Qt side.
Also unblocks logos-qt-sdk's LpBridge::invokeAsyncResult, which keeps a private
second lp_client only because logos::LpClient had no error-carrying async.
Verified: sdk tests 309/309 (incl. 9 new LpClient and 10 new generator tests),
generator-cli green, and the generated wrapper compiles under -Wall -Wextra
-Werror. The concurrency test was checked against the pre-fix header as a
control: 8 threads, 8 clients created, 7 leaked, threads disagreeing on which
client was the module's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): stub lp_string_free, and keep the path that needs it live
The sdk_tests link failed on GCC/Linux with an undefined reference to
lp_string_free from logos::LpClient::getMethods(), while linking cleanly on
macOS/clang.
The stub set was genuinely missing lp_string_free. It went unnoticed because the
lp_get_methods stub returned NULL, which made getMethods()'s free call
unreachable: lp_get_methods is a non-inline extern "C" function DEFINED IN THE
SAME TU as the test, so clang may inline it, prove the pointer null and delete
the call — no reference, no link error. GCC kept the call, and the linker wanted
the symbol.
Adding the stub alone would fix the link and leave the trap: the free path would
still be dead, so the next compiler that keeps the call decides whether this
builds. So lp_get_methods now returns a real heap allocation, which is the ABI's
actual contract ("every char* RETURNED by this library is owned by the caller;
free it with lp_string_free"), and lp_string_free frees it and counts. The
single-threaded test asserts the count, so the ownership rule is pinned rather
than merely satisfied.
Verified by reproducing the failure on macOS with -O0 (which stops clang folding
the call away): the pre-fix file fails with exactly `"_lp_string_free",
referenced from: logos::LpClient::getMethods()`, and the fixed one links and
runs 9/9. Full check: 308/308.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f3369faca4 |
feat(generator): async callers can see the error, sync callers can set a deadline (#132)
The two consumer surfaces had complementary holes:
sync : T foo(params…, logos::CallError* err = nullptr) error yes, timeout NO
async: void fooAsync(params…, cb, Timeout = Timeout()) timeout yes, error NO
so an async caller could not tell a failed remote call from a provider that
legitimately returned 0 / "" / false — the exact ambiguity the sync path's
CallError* was added to resolve — and a sync caller could not say how long it
was willing to wait, even though the transport overload the generator already
calls takes both.
Both fixes are additive:
T foo(params…, logos::CallError* err = nullptr, Timeout timeout = Timeout());
void fooAsync(params…, std::function<void(T)> cb, Timeout timeout = Timeout()); // unchanged
void fooAsyncResult(params…, std::function<void(logos::AsyncResult<T>)> cb,
Timeout timeout = Timeout()); // new
logos::AsyncResult<T> (new, Qt-free, cpp/logos_async_result.h) is {value, error}
plus ok(); AsyncResult<void> carries only the error so every fooAsyncResult has
the same callback shape. The name is distinct rather than an overload because
std::function<void(AsyncResult<T>)> next to std::function<void(T)> is ambiguous
for a generic lambda.
Applied to both emitters that produce this surface — legacy/generator_lib.cpp
(the module-builder path) and experimental/lidl_gen_client.cpp (`--lidl
--module-only`, from a published contract) — since a consumer can reach either
for the same contract.
The Qt-free (ApiStyle::Lp) surface gets the sync timeout (spelled `int
timeout_ms`; `Timeout` lives behind a Qt header) but NOT fooAsyncResult:
logos-protocol's lp_invoke_async hard-codes `cb(1, …)`, so an AsyncResult there
would report ok() on a failed call. Measured, not assumed. See the note in
makeHeaderLp.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|