mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
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>
79 lines
3.1 KiB
C++
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
|