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>
This commit is contained in:
Dario Lipicar
2026-08-03 10:19:08 -03:00
committed by GitHub
co-authored by Claude Opus 5
parent d972fe207a
commit f3369faca4
14 changed files with 716 additions and 134 deletions
+18 -12
View File
@@ -329,16 +329,22 @@ Fixture files in `tests/experimental/fixtures/`:
single-`_bytes`-field record is refused under both spellings. It used to read `f.type`,
which refused `? _bytes: tstr` and let `_bytes: ?tstr` through — the same declaration,
two answers. `?bstr` is unaffected either way: the tag lives in the value, not the slot.
- **A provider REJECTION reaches an async consumer callback only as a log line.** A
provider that refuses a call answers the canonical
`{"code":"dispatch_failed", "message":…, "origin":…}` object as its RESULT, not as a
transport error, and the Qt return table would convert it like any other value —
- **A provider REJECTION reaches `…Async`'s callback only as a log line** (but
`…AsyncResult`'s callback gets it properly). A provider that refuses a call answers the
canonical `{"code":"dispatch_failed", "message":…, "origin":…}` object as its RESULT, not
as a transport error, and the Qt return table would convert it like any other value —
erasing it (`_result.toList()` on that map is `[]`). The Qt consumer emitter therefore
detects it and folds it into the `logos::CallError` out-parameter the sync wrapper
already carries, so `mod.echoUintList(v, &err)` can tell a rejection from an empty
return. The generated `…Async` overload has no such channel — its callback is
`std::function<void(T)>`, and adding an error parameter would change the generated
public surface (which logos-qt-sdk's `qt-generator --backend consumer` veneer mirrors
1:1) — so an async rejection is reported with `qWarning` and the callback still
receives the default-converted value. Giving async an error channel is an API change,
not a code-generation fix.
detects it (`logosDispatchRejection`, emitted once per wrapper) and folds it into the
error channel of every surface that HAS one:
- **sync** — the `logos::CallError*` out-parameter, so `mod.echoUintList(v, &err)` can
tell a rejection from an empty return;
- **`…AsyncResult`** — `logos::AsyncResult<T>::error`, so `r.ok()` is false and
`r.error.code == "dispatch_failed"` exactly as on the sync path.
The historical **`…Async`** overload is the one exception: its callback is
`std::function<void(T)>`, and adding an error parameter would change a generated public
surface (which logos-qt-sdk's `qt-generator --backend consumer` veneer mirrors 1:1). It
is left untouched, so there an async rejection is reported with `qWarning` and the
callback still receives the default-converted value. `…AsyncResult` exists precisely
because giving async an error channel was an API addition rather than a code-generation
fix — a caller that needs to SEE the rejection uses it.