Files
logos-cpp-sdk/cpp/logos_async_result.h
Dario LipicarandClaude Opus 5 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>
2026-08-03 10:19:08 -03:00

79 lines
3.1 KiB
C++

#ifndef LOGOS_ASYNC_RESULT_H
#define LOGOS_ASYNC_RESULT_H
// The async counterpart of logos::CallError's sync out-parameter.
//
// The sync generated wrapper takes an optional `logos::CallError*` precisely
// "to distinguish a failed remote call from a legitimately default-valued
// result". The async wrapper had no such channel: `fooAsync` hands the callback
// a bare T, and a failed call is indistinguishable from a provider that
// legitimately returned 0 / "" / false. AsyncResult<T> is that missing channel
// — the value and the error travel together, so `fooAsyncResult`'s callback can
// check `.ok()` before trusting `.value`.
//
// This mirrors logos-rust-sdk, where BOTH surfaces already carry the error
// (lidl-gen/src/rustgen.rs: `-> Result<T, LogosError>` on the sync wrapper and
// `FnOnce(Result<T, LogosError>)` on the async one). C++ was the outlier.
//
// Deliberately Qt-FREE, exactly like logos_call_error.h next to which it
// conceptually lives: it is named in the signatures of BOTH generated surfaces
// — the Qt-typed one (ApiStyle::Qt) and the Qt-free lp one (ApiStyle::Lp, used
// by cdylib/universal modules whose translation units must not see Qt). Only
// std types and logos::CallError may appear here.
//
// (It lives in logos-cpp-sdk rather than in logos-protocol's
// logos_call_error.h only because that is where the generator that emits it
// lives; ${LOGOS_CPP_SDK_ROOT}/include is on the include path of every module
// build — see logos-plugin-qt/cmake/LogosModule.cmake and its module-builder
// twin, which add it unconditionally.)
#include "logos_call_error.h"
namespace logos {
/**
* @brief An async call's outcome: the decoded value plus the call error.
*
* `error.ok()` (surfaced as `ok()`) is the ONLY reliable success test — a
* failed call leaves `value` default-constructed, which for most return types
* is also a perfectly legal success value.
*
* dep.echoIntAsyncResult(7, [](logos::AsyncResult<qlonglong> r) {
* if (!r.ok()) { qWarning() << r.error.code.c_str(); return; }
* use(r.value);
* });
*
* Aggregate — `AsyncResult<T>{v, err}` and designated-ish brace init both work.
*/
template <typename T>
struct AsyncResult {
T value{};
CallError error;
bool ok() const { return error.ok(); }
explicit operator bool() const { return error.ok(); }
};
/**
* @brief The void specialization: an error channel with no value.
*
* A `void`-returning method could equally have been given a plain
* `std::function<void(logos::CallError)>` callback. It is spelled
* AsyncResult<void> instead so that EVERY generated `fooAsyncResult` takes
* `std::function<void(logos::AsyncResult<R>)>` for its own return type R with
* no special case — forwarding/proxy code (and the generator itself) can write
* the callback type from the return type mechanically, and every call site
* reads `if (!r.ok())` regardless of what the method returns.
*/
template <>
struct AsyncResult<void> {
CallError error;
bool ok() const { return error.ok(); }
explicit operator bool() const { return error.ok(); }
};
} // namespace logos
#endif // LOGOS_ASYNC_RESULT_H