mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-27 12:01:15 +00:00
lp_client_create() made the CALLING thread the client's owner thread. Callers reach it through a lazily-created wrapper (the generated bind_<iface>() -> LpClient::ensure()), so the first thread to make an outbound call captured the whole transport for the life of the process. For the qt_remote transport that thread also ends up owning the QRemoteObjectNode and its QLocalSocket, which are only serviced by a thread running a Qt event loop. A module whose first call came from a worker — an HTTP handler, a timer thread — bound its transport to a thread that only pumps events while it is already blocked inside a call. Replica acquisition then never completed: every requestObject() burned its full 20s timeout and returned nullptr, and since a failed acquire yields an empty result the data loss was silent. openmetrics-module hit exactly this: one GET /metrics took 40s (2 x 20s) and came back missing a module, /health went unanswered behind the wedged libmicrohttpd thread, and the follow-up stop RPC failed. Construct the client on the Qt main thread when the transport needs a Qt event loop, so the per-call marshal that already exists (logos::runOnOwnerThread) lands somewhere that can actually service it. This is the anchor the Qt path always had — LogosAPI::getClient marshals construction to the LogosAPI's thread — given to the lp path. Plain (tcp/tcp_ssl) and mock transports are Qt-free and thread-agnostic, so they keep the calling thread: a worker-thread consumer stays off the main thread's back. LogosTransportFactory::needsQtEventLoop() carries that rule next to the createConnection resolution it mirrors. When there is nothing to anchor to (a Qt-affine transport with no QCoreApplication) we now warn instead of letting it surface as a mute timeout. Tests: a worker thread creates an lp client over qt_remote and calls a provider published on the main thread; passes in ~0.15s, and with the construction hop reverted fails after 24.8s / 49.9s — the acquire timeouts themselves. Plus a truth table for needsQtEventLoop. 183/183 protocol tests pass. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
278 lines
13 KiB
C
278 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.
|
|
*/
|
|
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 */
|