Files
logos-protocol/cpp/logos_protocol.h
T
Dario Lipicar 9de4165ab6 Qt split + module authoring groundwork: LogosProviderPlugin + the common module-impl C ABI (#3)
* Extract the Logos protocol layer from logos-cpp-sdk

Transports (plain TCP/TLS, qt_local, qt_remote/QRO, mock), token manager,
consumer core (LogosAPIClient/LogosAPIConsumer incl. the capability
auto-requestModule flow), ModuleProxy, the abstract LogosProviderObject
interface, and the canonical QVariant<->JSON conversion — now behind the
language-neutral lp_* C ABI (logos_protocol.h) carrying the protocol
semver (LOGOS_PROTOCOL_VERSION_*, lp_protocol_version()).

Bytes crossing the ABI use the lossless {"_bytes": base64url} tagging
(NUL-safe), matching the plain wire encoding.

Provider lp_* surface is compiled groundwork; serving lands with module
authoring.

* Move LogosProviderPlugin into logos_provider_interface.h

Plugin-loading tools (logos-cpp-generator's introspection mode, lm, the
hosts) need only qobject_cast<LogosProviderPlugin*>() + the abstract
LogosProviderObject — both framework-internal. Hosting the detection
interface here keeps those tools off the developer-facing logos-qt-sdk
layer. Same iid (org.logos.LogosProviderPlugin); header-only, ABI-neutral.

* Define the common module-impl C ABI (logos_module_impl.h)

ONE cdylib contract for module implementations in every language:
dispatch / get_methods / set_context / set_emit_callback / accept_token
/ get_protocol_version / string_free. The C++ and Rust SDKs emit these
exports around their respective impls; the uniform generated Qt glue
(and later a no-Qt host) talks to the cdylib only through this ABI.
JSON data model and tagged bytes form match the lp_* consumer ABI; the
protocol-version handshake complements the build-time metadata stamp.

* json convert: integers stay integers across the C ABI

QJsonValue::fromVariant degrades every numeric to double, so Int/UInt/
LongLong/ULongLong QVariants serialized as 5.0 — and a strict consumer on
the other side of the C ABI (a generated dispatch reading an int param)
rejects or zeroes them. Surfaced by the first cdylib-authored module
whose inbound args cross qvariantToNlohmann; the dlopen smoke harness
fed hand-written int JSON and never exercised this edge.

* call-error channel: surface {code,message,origin} for unacquirable targets

invokeRemoteMethod could not distinguish a failed call from a void/null
result — lp_invoke returned LP_OK with a null JSON result even when the
target module was never reached, and generated typed wrappers silently
defaulted (0 / empty string). Additive err-out overloads on
LogosAPIConsumer/LogosAPIClient fill a std-only logos::CallError
(logos_call_error.h, new LogosCallError exception for the generated
wrappers to throw); lp_invoke now honors its documented contract for
this class of failure: LP_ERR_UNAVAILABLE + canonical error JSON.
First detectable code: object_unavailable (requestObject failure) —
the struct is the extension point for transport-level statuses.

* call-error: drop the exception type — the error channel is the out-param

Per review, generated wrappers expose CallError as an optional trailing
out-parameter instead of throwing; the struct is the whole contract.

* ci: build + run the protocol test suite

On every pull request (unfiltered — stacked PRs included), master pushes,
and manual dispatch. The repo shipped without CI; its 111-test suite only
ran locally and through the workspace gate.

* consumer: typed requestModule for the capability flow

Port of logos-cpp-sdk master f5a127dd ('use updated capability module',
cpp-sdk#85, Iuri Matias) — the touched files (logos_api_client.cpp,
logos_api_consumer.{h,cpp}) moved into this repo in the P1 extraction.
The capability auto-requestModule path now calls a typed std::string
helper on the consumer (which acquires the capability object directly)
instead of a stringly invokeRemoteMethod round-trip. 111/111 tests.

* ci: DeterminateSystems nix installer (macOS runners)

cachix/install-nix-action fails on the macOS runners with
eDSRecordAlreadyExists (pre-existing nix build users); the org's
macOS-bearing workflows use the DeterminateSystems installer.
2026-06-12 19:39:57 -03:00

259 lines
11 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.
* - 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.
*
* 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
#define LOGOS_PROTOCOL_VERSION_MINOR 1
#define LOGOS_PROTOCOL_VERSION_PATCH 0
#define LOGOS_PROTOCOL_VERSION_STRING "0.1.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).
*
* The calling thread becomes the client's owner thread; with the Qt Remote
* Objects transport it must run a Qt event loop. 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 */