The fail-fast only tested <root>/cpp/logos_protocol.h, but the LP_SRC
selection right below (and the error message itself) support the
installed export layout <root>/include/cpp as well. Pointing
LOGOS_PROTOCOL_ROOT at an installed export tripped the FATAL_ERROR
before that fallback could apply.
Caught by Copilot review on #82.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transport/token/IPC layer (transports incl. QRO + plain TCP/TLS,
consumer core LogosAPIClient/LogosAPIConsumer with the capability
auto-requestModule flow, ModuleProxy, token manager, QVariant<->JSON
conversion, the abstract LogosProviderObject interface) now lives in the
logos-protocol repo behind the versioned lp_* C ABI.
This SDK keeps the typed C++ developer layer (LogosAPI, provider base
classes + Qt provider glue, module context, code generator) and still
compiles the protocol sources INTO liblogos_sdk.a from the flake input,
so the installed artifact (archive symbols, include/ + include/cpp
layouts, cmake config) stays byte-compatible: existing consumers need
no changes. Public headers are unchanged; logos_provider_object.h keeps
its name and now re-exports the abstract interface from
logos_provider_interface.h.
Transport/protocol component tests moved to logos-protocol with the
code; the remaining sdk/generator/experimental suites are unchanged
(432/432 green against the local protocol checkout).
* Marshal inter-module calls to the owner thread
Logos inter-module calls go over Qt Remote Objects, whose replicas only
work on the thread that created them (the module's main/event-loop thread).
A module that makes calls from a worker thread — e.g. an embedded HTTP
server serving /metrics — would otherwise hang on replica acquisition.
Make LogosAPIClient transparently marshal to its owner thread when called
off-thread (guarded so same-thread calls run directly with no overhead):
- LogosAPI::getClient creates the client/consumer/replicas on the owner thread
- LogosAPIClient::invokeRemoteMethod / requestObject / onEvent run there too
New header logos_thread_marshal.h (runOnOwnerThread). No new data members —
ABI-safe for statically-linked plugins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add regression test for worker-thread inter-module calls
A provider records the thread its method runs on; a consumer calls it from
a worker thread via LogosAPIClient::invokeRemoteMethod. The call must execute
on the owner (main/event-loop) thread, not the worker thread.
Fails without the marshaling change (the call runs on the worker thread —
0x..d80d0 vs owner 0x..c53e0, "executed on the worker thread instead of the
owner thread"); passes with it (511/511).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address review: async marshaling, helper constraints, test ownership
- invokeRemoteMethodAsync now also marshals to the owner thread (non-blocking
QueuedConnection) — the async path acquires a replica too, so calling it from
a worker thread previously re-introduced the off-thread bug.
- runOnOwnerThread: document the return-type constraints (void or
default-constructible, non-reference) and static_assert against references.
- test: declare the provider before its LogosAPI so the ModuleProxy (which
holds a raw pointer to it) is torn down first — removes the leak and the
inaccurate comment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* doctest: call a module from a worker thread (HTTP server)
Adds cpp-sdk-worker-thread-http.test.yaml: builds a sensor_module callee and
an http_module caller that embeds a libmicrohttpd server, runs them in
logoscore, starts the server, and curls it. The HTTP handler calls
sensor_module.readTemperature() from the server's worker thread — which only
works because the SDK marshals the cross-module call onto the module's owner
thread. The module stays pure C++.
Wired into doctests/run.sh and the doctests CI workflow. Validated locally
(23/23 steps pass): `curl` returns `temperature 42`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add per-event documentation + getPluginEvents introspection
Mirror the per-method documentation pipeline for events. Events
(declared in a universal module's logos_events: section) now carry a
description parsed from their /// doc comments, and are introspectable
at runtime via a new getPluginEvents framework call.
- lidl_ast: EventDecl gains a description field.
- impl_header_parser: capture the event's doc comment (previously
discarded) and an optional metadata.json events[].description.
- lidl_gen_provider: generated universal provider emits
getEvents() override, mirroring getMethods() (name/signature/
parameters/description; no returnType/isInvokable — events are void).
- logos_provider_object: default-empty virtual getEvents() so the
legacy provider path and QtProviderObject inherit empty.
- module_proxy / qt_provider_object: intercept getPluginEvents next to
the getPluginMethods special-case.
- docs: spec + README event-documentation notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add unit tests for event documentation + getEvents generation
Address review feedback (#71): cover the event-introspection paths that
previously only had method-side tests.
- impl_header_parser test: assert metadata.json events[].description is
parsed; new documented_events fixture asserts `///` doc-comment
capture on a logos_events: block (multi-line joined with \n,
adjacent-only, plain // ignored).
- lidl_gen_provider test: assert the generated dispatch contains
getEvents() emitting each event's name/signature/parameters and an
escaped description, and that events carry no returnType/isInvokable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fold event introspection into getMethods() to keep the provider ABI stable
The previous approach added a getEvents() virtual to LogosProviderObject,
which inserted a new vtable slot and shifted every later slot — an ABI
break that would misdispatch virtual calls whenever an old and new
host/module were mixed across the in-process plugin boundary.
Instead, report events INSIDE the existing getMethods() call: it now
returns the module's whole interface, with each entry tagged
type "method" or "event" (events omit returnType/isInvokable). The
provider vtable is therefore byte-for-byte unchanged, so old/new hosts
and modules stay binary-compatible — a new host reading an old module
sees no event entries (zero events), and an old host reading a new
module just ignores the "type" field (cosmetic). An entry with no
"type" is treated as a method.
- logos_provider_object.h: remove the getEvents() virtual; document that
getMethods() carries both, and why.
- generator (lidl_gen_provider): emit events as type "event" entries
inside getMethods(); tag methods type "method"; no getEvents() output.
- module_proxy / qt_provider_object: getPluginMethods()/getPluginEvents()
are now type-filtered views of getMethods(), plus a new
getPluginInterface() returning the whole list. (These are name-
dispatched Q_INVOKABLEs, not vtable surface — adding them is safe.)
- tests: generator asserts events fold into getMethods() tagged "event";
ModuleProxy asserts the three filtered views; parser tests unchanged.
- docs: spec/project/docs/README updated, incl. an ABI rationale note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: marshal provider events onto the source thread
ModuleProxy's event listener emitted eventResponse directly on whatever thread
the module fired the event from (its worker/FFI thread). QtRemoteObjects then
serialized and sent the event from that foreign thread, racing the source
socket against a method reply being sent from the source thread, which silently
dropped the reply.
This is why a method that emits an event mid-call never returns to the caller
(e.g. delivery_module start(), which emits connectionStateChanged as the node
connects) while a method that emits nothing (createNode) returns fine.
Marshal the emission onto the ModuleProxy's own thread via a queued invocation
so events and method replies are serialized on the single thread
QtRemoteObjects expects to own the source.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: generalize the threading comment
* fix: use AutoConnection so same-thread emits stay synchronous
QueuedConnection deferred every emission, breaking same-thread callers that
emit-then-assert and crashing when a queued lambda outlived the object.
AutoConnection invokes synchronously when already on the source thread and only
queues cross-thread emissions (the actual fix); passing 'this' as context
cancels a queued call if the object is destroyed first.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
overload with method that don't use QString
overload with method that don't use QString
overload with method that don't use QString
overload with method that don't use QString
* refactor: abstract connection/transport; and clearly separate qt remote obj and qt local into separate implementations
* abstract qt remote registry
* add mock implementation; these serves to further test the abstraction but also useful for testing modules later
* use LogosObject instead of QObject
* abstract provider side
* updates to use new api
* re-add async api back
---------
Co-authored-by: Logos Workspace <logos@workspace.local>
Remove logging of method arguments. It may contain private keys,
DB passwords, auth tokens, and token values. Log only method
names and argument counts for debugging.
- https://github.com/status-im/infra-logos/issues/1
* Add LogosResult
* Add documentation for complex types
* Add get type util function
* Add more shorthand functions
* Add bool support
* Provide more shorthand functions
* Throw exception on bad access
* Fix typo in doc