mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 20:11:07 +00:00
lp_result_cb has always been documented as carrying an outcome —
"ok != 0 -> `json` is the result JSON value; ok == 0 -> `json` is the
canonical error object" — and the synchronous twin lp_invoke has always
honoured it (LP_ERR_UNAVAILABLE + out_error_json). lp_invoke_async did
not: it subscribed with the VALUE-ONLY invokeRemoteMethodAsync overload
and called back `cb(1, json, user_data)` with ok hard-coded to 1, so a
call to a module that cannot be acquired reached the callback as a
SUCCESS carrying a default-constructed value.
The fix is to pass a TWO-argument lambda, which is invocable only as
LogosAPIClient::AsyncResultErrorCallback and so binds to the
CallError-aware overload that already exists next to the value-only one.
The failure is then rendered with the same makeErrorJson the sync path
uses, so both entry points report the same event in the same shape.
The ABI is unchanged. lp_result_cb's (ok, json, user_data) signature
already reserves ok == 0 for exactly this; nothing had to grow a new
entry point, and every in-tree consumer already branches on `ok`
(logos-rust-sdk's async_call_trampoline even parses `message` out of the
canonical error object — code written against a contract the
implementation never kept).
Regression test: a matched pair over a REAL transport (plain TCP), not
the mock.
FAILING async call -> ok=0 {"code":"object_unavailable", ...}
SUCCEEDING async call -> ok=1 7
The first fails on the unfixed tree (ok=1, json "null"); the second
passes on both, so an over-eager "report failure everywhere" fix cannot
sneak through.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
288 lines
13 KiB
C
288 lines
13 KiB
C
#ifndef LOGOS_PROTOCOL_H
|
|
#define LOGOS_PROTOCOL_H
|
|
|
|
/* ===========================================================================
|
|
* logos_protocol.h — the public, language-neutral C ABI of logos-protocol.
|
|
*
|
|
* This is the ONE seam every Logos SDK builds on. The data model is
|
|
* JSON-in-strings: method arguments are a JSON array, results are a JSON
|
|
* value, event payloads are a JSON array — all UTF-8 `const char*`.
|
|
*
|
|
* Ownership:
|
|
* - Every `char*` RETURNED by this library is heap-allocated and owned by
|
|
* the caller; free it with lp_string_free() (safe on NULL).
|
|
* - Every `const char*` PASSED IN is borrowed for the duration of the call.
|
|
*
|
|
* Bytes encoding: binary data crossing this ABI is encoded inside JSON as
|
|
* {"_bytes": "<base64url>"}
|
|
* (a single-key object). This is lossless for arbitrary bytes, including
|
|
* embedded NUL. It matches the plain-wire encoding (json_mapping.cpp) and is
|
|
* the canonical representation at this boundary.
|
|
*
|
|
* Error shape: structural failures report one canonical JSON object through
|
|
* `out_error_json` / error callbacks:
|
|
* {"code": "<machine_code>", "message": "<human text>", "origin": "<module>"}
|
|
*
|
|
* Threading / event-loop contract:
|
|
* - Callbacks may arrive on an internal protocol thread — never assume
|
|
* they run on your own thread.
|
|
* - Handles are thread-safe per-handle: calls on one handle may be made
|
|
* from any thread; the library marshals to the handle's owner thread
|
|
* internally where required.
|
|
* - Qt-free transports (plain tcp/tcp_ssl, mock) are serviced by the
|
|
* library's own workers — no caller event loop is needed.
|
|
* - The Qt Remote Objects transport (the current default inside module
|
|
* processes) ADDITIONALLY requires a running Qt event loop in the
|
|
* process. Every Logos module process has one (logos_host runs it).
|
|
* Standalone non-Qt consumers must use the plain transport.
|
|
* A client on that transport is created on — and owned by — the Qt main
|
|
* thread no matter which thread calls lp_client_create(), because its
|
|
* node and socket are only serviced by that thread's loop. Calls from
|
|
* other threads marshal onto it and block until it answers.
|
|
* - lp_invoke() blocks the calling thread until the result arrives or the
|
|
* timeout elapses (timeout_ms <= 0 selects the default, currently 20s).
|
|
*
|
|
* Cancellation / lifetime:
|
|
* - After lp_client_destroy() / lp_unsubscribe() RETURNS, no further
|
|
* callbacks fire for that handle; pending async results are dropped.
|
|
* `user_data` may be freed only after that point, never before.
|
|
* - Both are safe to call from ANY thread, including a worker that happens
|
|
* to drop the last reference to a client. lp_client_destroy() defers the
|
|
* underlying teardown to the client's owner thread when called elsewhere,
|
|
* so the handle may outlive the call by an event-loop turn — the
|
|
* no-callbacks guarantee above holds regardless.
|
|
*
|
|
* Versioning: this library carries the logos-protocol semantic version —
|
|
* the single number that governs Logos load/call compatibility. Two
|
|
* participants interoperate iff they share the same MAJOR. MINOR is
|
|
* additive/back-compatible; PATCH never affects compatibility.
|
|
* =========================================================================== */
|
|
|
|
#define LOGOS_PROTOCOL_VERSION_MAJOR 0
|
|
// 0.2: per-module concurrent dispatch ("multi"). Additive/back-compatible — a
|
|
// multi module returns a deferred-completion sentinel from callMethod and pushes
|
|
// the result as a __logos_call_complete__ event (see logos_async_dispatch.h);
|
|
// the provider/host ABI is UNCHANGED, so same-MAJOR hosts (incl. 0.1 daemons)
|
|
// load and forward multi modules without modification. A pre-0.2 *consumer*
|
|
// would see the raw sentinel rather than awaiting it — graceful, not a crash.
|
|
#define LOGOS_PROTOCOL_VERSION_MINOR 2
|
|
#define LOGOS_PROTOCOL_VERSION_PATCH 0
|
|
#define LOGOS_PROTOCOL_VERSION_STRING "0.2.0"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Return codes (negative = failure). Functions returning int use these.
|
|
* ------------------------------------------------------------------------- */
|
|
#define LP_OK 0
|
|
#define LP_ERR_INVALID_ARG (-1)
|
|
#define LP_ERR_UNSUPPORTED (-2) /* provider surface: exercised in a later phase */
|
|
#define LP_ERR_INTERNAL (-3)
|
|
#define LP_ERR_UNAVAILABLE (-4) /* target module/object could not be acquired */
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Version
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** Version string "MAJOR.MINOR.PATCH" of the linked logos-protocol.
|
|
* Returns a static string — do NOT free. */
|
|
const char* lp_protocol_version(void);
|
|
|
|
/** MAJOR component of the linked logos-protocol version. Equal majors are
|
|
* compatible; unequal majors are not. */
|
|
int lp_protocol_abi_major(void);
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Memory
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** Free a string returned by this library. Safe to call with NULL. */
|
|
void lp_string_free(char* s);
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Process-global mode / transport defaults
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** Set the process-wide communication mode: "remote" (IPC, default),
|
|
* "local" (in-process registry) or "mock" (in-memory, for tests).
|
|
* Returns LP_OK or LP_ERR_INVALID_ARG. */
|
|
int lp_set_mode(const char* mode);
|
|
|
|
/** Current mode as "remote" | "local" | "mock". Static string — do not free. */
|
|
const char* lp_get_mode(void);
|
|
|
|
/** Set the process-global default transport from a JSON object, e.g.
|
|
* {"protocol":"local"}
|
|
* {"protocol":"tcp","host":"127.0.0.1","port":6001,"codec":"json"}
|
|
* {"protocol":"tcp_ssl","host":"...","port":6443,"codec":"cbor",
|
|
* "ca_file":"...","cert_file":"...","key_file":"...","verify_peer":true}
|
|
* Returns LP_OK or LP_ERR_INVALID_ARG on parse failure. */
|
|
int lp_set_default_transport(const char* transport_json);
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Consumer: clients, invoke, subscribe
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
typedef struct lp_client lp_client;
|
|
typedef struct lp_subscription lp_subscription;
|
|
|
|
/** Result callback for lp_invoke_async.
|
|
* ok != 0 → `json` is the result JSON value; ok == 0 → `json` is the
|
|
* canonical error object. `json` is only valid for the duration of the
|
|
* callback — copy it if you need it longer. */
|
|
typedef void (*lp_result_cb)(int ok, const char* json, void* user_data);
|
|
|
|
/** Event callback for lp_subscribe. `data_json` is a JSON array (the event
|
|
* payload), valid only for the duration of the callback. */
|
|
typedef void (*lp_event_cb)(const char* event_name, const char* data_json,
|
|
void* user_data);
|
|
|
|
/**
|
|
* Create a client for calling `target_module` on behalf of `origin_module`.
|
|
*
|
|
* `target_transport_json` / `capability_transport_json`: JSON object as for
|
|
* lp_set_default_transport(), or NULL to use the process default. The
|
|
* capability transport is used by the automatic `requestModule` token-fetch
|
|
* flow (this library dials `capability_module` transparently the first time
|
|
* a target requires a token — every language gets that flow for free).
|
|
*
|
|
* Owner thread: for a Qt-affine transport (Qt Remote Objects / local mode) the
|
|
* client is constructed on the Qt main thread — blocking this call until that
|
|
* thread runs it — because its node and socket are only serviced there. Any
|
|
* thread may call this. For the Qt-free transports (tcp / tcp_ssl / mock) the
|
|
* calling thread becomes the owner thread, as before.
|
|
*
|
|
* Returns NULL on invalid arguments.
|
|
*/
|
|
lp_client* lp_client_create(const char* target_module,
|
|
const char* origin_module,
|
|
const char* target_transport_json,
|
|
const char* capability_transport_json);
|
|
|
|
/** Destroy a client. After this returns, no further callbacks fire for the
|
|
* client or its subscriptions. */
|
|
void lp_client_destroy(lp_client* client);
|
|
|
|
/**
|
|
* Call `method` on the client's target module, blocking until the result
|
|
* arrives or the timeout elapses.
|
|
*
|
|
* `args_json`: JSON array of arguments (NULL means "[]").
|
|
* `timeout_ms <= 0` selects the default timeout.
|
|
*
|
|
* On LP_OK: *out_result_json (if non-NULL) receives the result JSON value
|
|
* (may be "null" — today's protocol does not distinguish "no result" from
|
|
* a failed call at this level; that matches existing behavior).
|
|
* On failure: *out_error_json (if non-NULL) receives the canonical error
|
|
* object. Both out-strings are owned by the caller (lp_string_free).
|
|
*/
|
|
int lp_invoke(lp_client* client,
|
|
const char* method,
|
|
const char* args_json,
|
|
int timeout_ms,
|
|
char** out_result_json,
|
|
char** out_error_json);
|
|
|
|
/**
|
|
* Asynchronous variant of lp_invoke. Returns LP_OK if the call was
|
|
* dispatched; `cb` then fires exactly once with the result (from the
|
|
* client's owner thread). Safe to call from any thread.
|
|
*
|
|
* `cb` carries the same outcome the sync twin splits across its return code
|
|
* and out-params: ok != 0 → `json` is the result JSON value; ok == 0 → `json`
|
|
* is the canonical error object lp_invoke would have written to
|
|
* out_error_json (e.g. code "object_unavailable" when the target module is
|
|
* not loaded). A LP_OK return therefore means "dispatched", never "succeeded"
|
|
* — the outcome is only known in the callback.
|
|
*
|
|
* Argument/handle validation still fails synchronously with
|
|
* LP_ERR_INVALID_ARG and `cb` is NOT called in that case.
|
|
*/
|
|
int lp_invoke_async(lp_client* client,
|
|
const char* method,
|
|
const char* args_json,
|
|
int timeout_ms,
|
|
lp_result_cb cb,
|
|
void* user_data);
|
|
|
|
/**
|
|
* Subscribe to `event_name` emitted by the client's target module.
|
|
* `cb` fires once per event with the payload as a JSON array.
|
|
* Returns NULL on failure (e.g. the target object cannot be acquired).
|
|
*/
|
|
lp_subscription* lp_subscribe(lp_client* client,
|
|
const char* event_name,
|
|
lp_event_cb cb,
|
|
void* user_data);
|
|
|
|
/** Cancel a subscription. After this returns the callback will not fire
|
|
* again (already-running invocations are allowed to finish first). */
|
|
void lp_unsubscribe(lp_subscription* sub);
|
|
|
|
/** Introspect the target module's methods/events as a JSON array (the
|
|
* same shape `lm` prints). Caller frees via lp_string_free. NULL on
|
|
* failure. */
|
|
char* lp_get_methods(lp_client* client);
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Tokens
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** Get the stored token for `module_name`. Returns NULL when absent;
|
|
* caller frees via lp_string_free. */
|
|
char* lp_token_get(const char* module_name);
|
|
|
|
/** Store a token for `module_name`. */
|
|
int lp_token_save(const char* module_name, const char* token);
|
|
|
|
/** Deliver a module token to the client's target (the consumer-side
|
|
* `informModuleToken`). Returns LP_OK when the target accepted it. */
|
|
int lp_inform_module_token(lp_client* client,
|
|
const char* auth_token,
|
|
const char* module_name,
|
|
const char* token);
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Provider (GROUNDWORK — defined and compiled in this version, fully
|
|
* exercised when module authoring lands on the common cdylib module-impl
|
|
* C ABI. Until then lp_provider_register/emit return LP_ERR_UNSUPPORTED.)
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
typedef struct lp_provider lp_provider;
|
|
|
|
/** Dispatch a method call. Return a heap string (result JSON value) that the
|
|
* library frees with lp_string_free; return NULL to signal failure. */
|
|
typedef char* (*lp_dispatch_cb)(const char* method, const char* args_json,
|
|
void* user_data);
|
|
|
|
/** Return the module's method/event metadata as a JSON array (heap string,
|
|
* freed by the library via lp_string_free). */
|
|
typedef char* (*lp_getmethods_cb)(void* user_data);
|
|
|
|
/** Accept a token delivered by another module. Return LP_OK to accept. */
|
|
typedef int (*lp_token_cb)(const char* module_name, const char* token,
|
|
void* user_data);
|
|
|
|
lp_provider* lp_provider_create(const char* module_name,
|
|
const char* transport_set_json);
|
|
void lp_provider_destroy(lp_provider* provider);
|
|
int lp_provider_register(lp_provider* provider,
|
|
lp_dispatch_cb dispatch,
|
|
lp_getmethods_cb get_methods,
|
|
lp_token_cb on_token,
|
|
void* user_data);
|
|
int lp_provider_emit_event(lp_provider* provider,
|
|
const char* event_name,
|
|
const char* data_json);
|
|
int lp_provider_save_token(lp_provider* provider,
|
|
const char* module_name,
|
|
const char* token);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* LOGOS_PROTOCOL_H */
|