mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
818a3c6490810bda6b9fbb0899cd252addf9388a
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c7444bc29a |
Follow-ups to #100: lp-consumer bstr decode, Qt-free cdylib event types, binary-event coverage (#102)
* cdylib events: Qt-free types, and drop the unused bytes encoder Three follow-ups to the bstr event fix, all in the cdylib events sidecar -- a Qt-FREE translation unit: - An `any`/map event parameter was emitted as a bare QVariant/QVariantMap, which does not compile there. Spell those as their nlohmann aliases (LogosMap / LogosList) and pull in <logos_json.h> when they appear. - std::vector<std::vector<uint8_t>> fell through the impl-header parser's unknown-type fallback to `any`, so the cdylib gate admitted it and the generator then emitted QVariant. Parse it as `[bstr]` so the gate rejects it with a message naming the offending parameter. - The bytes encoder was emitted into every module's sidecar, leaving an unused static function (-Wunused-function) wherever no event carries binary data. Emit it only when a bstr event parameter exists. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * lp consumer: decode bstr into std::vector<uint8_t> The Qt-free (`lp`) consumer wrappers -- what every universal C++ module gets for its dependencies -- had no QByteArray in their type tables, so a `bstr` event parameter, method argument, or return degraded to QVariant and then to LogosMap. A consumer subscribing to a binary event was handed the raw tagged JSON object {"_bytes": "<base64url>"} instead of the bytes, with no generated decode. Teach the tables about QByteArray (-> std::vector<uint8_t>) and marshal it through the canonical tagged form in both directions: logos::bytesToJson on the way out, logos::jsonToBytes on the way in. Those live in logos_json.h -- Qt-free and protocol-free, so the generated wrappers and module code can share them. The Qt apiStyle already did this via QByteArray::toBase64/fromBase64. Without this, a subscriber written the obvious way -- onBinaryReady([](const std::string&, const std::vector<uint8_t>& payload) {...}) -- compiles (nlohmann::json has an implicit conversion operator) and then throws at runtime on every event, so the callback body silently never runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * tests: cover binary event payloads by value, not just by source text The regression test for #99 asserts on generated source text, so it stays green against an encoder that emits the wrong bytes. Add the value-level half: - tests/sdk/test_logos_json_bytes.cpp exercises the canonical tagged-bytes codec against the RFC 4648 vectors, the URL-safe alphabet, every len%3 tail group, embedded NULs and high bytes, a 109,447-byte payload (the size from #99), and the lenient/padded decode paths. - tests/experimental/test_lidl_gen_cdylib.cpp additionally pins the Qt-free spelling of JSON event payloads, the rejection of [bstr], and the omission of the bytes encoder from modules whose events carry no binary data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * doctests: prove a binary event payload survives the round trip Neither doc-test covered bytes-in-an-event -- the gap #99 fell through. The generator round-trip carried `bstr` only as a method argument and return, and the composition doc-test, which is the one that actually runs two modules under logoscore and subscribes to an event, carried only a string. So a generator that dropped every bstr event argument kept both of them green. - cpp-sdk-module-composition: greeter_module gains a `blobReady(label, payload)` event and an `emitBlob(size)` method; orchestrator_module subscribes and reports the length AND a checksum of what it received. Length alone would not catch a corrupted payload -- a wrong alphabet round-trips to the same size. - cpp-sdk-generator-roundtrip: sensor_module gains a `capture(id, frame: bstr)` event, and a new step shows the generated event body encoding it through lidlBytesToJson rather than pushing it raw. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * logos_json.h: include <cstddef> for size_t The tagged-bytes codec uses size_t but relied on it arriving transitively through the other includes. Include <cstddef> directly so the header is self-contained. (Copilot review, PR #102.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
f032cf291e |
fix(header-parser): join multi-line method declarations before parsing (#91)
* fix(header-parser): join multi-line method declarations before parsing * strip qualifiers/attributes before return-type matching (#92) --------- Co-authored-by: Álex <alex93cabeza@gmail.com> |
||
|
|
1bc101df1f |
feat: cpp-generator consumes logos-lidl; delete embedded frontend (#89)
* feat: cpp-generator consumes logos-lidl; delete embedded frontend The canonical LIDL frontend now lives in logos-lidl. cpp-generator links it and keeps only the C++/Qt-specific parts (impl-header parsing, the gen_client/ gen_cdylib backends, the Qt type-name mapping). - Delete the embedded lidl_lexer/parser/serializer/validator/ast. - Add experimental/lidl_compat.h: brings logos-lidl's std AST into the global scope the backends use (via `using`), a qs() std::string→QString helper, a QTextStream<<std::string overload, and name-compatible shims (lidlParse/ lidlSerialize/lidlValidate) so the emission code keeps compiling. - Re-point impl_header_parser, lidl_gen_client (+ Doxygen /// docs on the generated client methods), lidl_gen_cdylib, lidl_emit_common, and legacy/ main at lidl::ModuleDecl. - CMake: C++17 + find_package(logos-lidl) + link logos-lidl::logos_lidl. - bin.nix: distribute only the shared C++/Qt backend helpers (compat + impl_header_parser + emit_common) under share/lidl-frontend, not the frontend. - tests: drop the 4 frontend test files (covered by logos-lidl now); the backend tests link logos-lidl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pin logos-lidl to the C-ABI commit + lock it The logos-lidl input was declared in flake.nix but missing from flake.lock, so override chains that don't reach the nested input (the doctest harness building a scaffolded module) couldn't resolve it. Pin the branch rev and lock it so the component is self-contained. Re-point at master once logos-lidl lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-point logos-lidl to merged master (#5) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
676154070c |
codegen: Qt-free outbound — ApiStyle::Lp typed wrappers over the lp_* C ABI (#88)
* ci: drop doctest chain pins — the qt-split chain is fully merged logoscore-cli and module-builder masters now contain the chain; the temporary --release-for pins (added so stacked-branch CI could resolve compatible cross-repo revs) default back to latest releases. * codegen: Qt-free outbound — ApiStyle::Lp typed wrappers over the lp_* C ABI Adds a third generator flavor (ApiStyle::Lp, --api-style lp) whose typed dependency wrappers + LogosModules umbrella call the logos-protocol C ABI directly via a new header-only logos::LpClient, instead of LogosAPIClient. This lets a module make outbound typed calls and event subscriptions with NO Qt in its translation units — Qt stays confined to the QRO transport (inside logos-protocol) and the generated plugin glue. - cpp/logos_lp_client.h: header-only logos::LpClient (lazy lp_client_create on a baked origin; invoke / invokeAsync / subscribe; std<->nlohmann JSON; CallError out-param) + RAII logos::LpSubscription (unsubscribes on drop) + json<->std helpers. The C++ analog of rust-sdk PluginProxy. - generator: makeHeaderLp/makeSourceLp emit the Lp wrappers; the Lp umbrella drops the LogosAPI ctor and bakes this module name as the lp_client origin (LogosModules() default-constructible). Qt/Std emission is byte-unchanged (dispatch added at the top of makeHeader/makeSource). Verified: generator builds; generated wrappers + umbrella compile to .o with ONLY cpp-sdk + logos-protocol headers + nlohmann (no Qt); cpp-sdk tests pass. * cdylib: wire the Qt-free typed dependency surface (modules()) into the impl When a cdylib module declares dependencies, the generated exports now include the Lp umbrella (logos_sdk.h) and construct LogosModules() + maybeSetLogosModules on the impl just before onContextReady — so the author can call modules().<dep>... and subscribe to dep events from a Qt-free cdylib. Guarded on module.depends so dependency-less cdylib modules are byte-unchanged. The umbrella + dep wrappers themselves are produced by the --general-only --api-style lp generation; feeding the dep .lidl files into that during the module build is the remaining build-system wiring (module-builder + plugin-qt dep resolution). * cdylib: wire modules() unconditionally (deps come from metadata, not the .lidl) The umbrella wiring was guarded on the .lidl module.depends, but a cdylib module declares its dependencies in metadata.json#dependencies — the .lidl contract.depends is typically empty — so modules() was left null and a typed outbound call segfaulted. Always include the generated logos_sdk.h umbrella and maybeSetLogosModules(impl, new LogosModules()) before onContextReady; the overload is a no-op for context-less impls and the umbrella codegen emits an (empty) logos_sdk.h for every cdylib, so this is safe in all cases. * fix(headers): ship logos_lp_client.h in the include/cpp source-export root A cdylib module's generated dep wrapper includes "logos_lp_client.h" and, transitively, "logos_result.h". The wrapper is compiled with the cpp-sdk source-export include root (include/cpp), so logos_lp_client.h must sit beside logos_result.h there — a quoted include resolves siblings relative to the including file's directory. Previously logos_lp_client.h shipped only at the top-level include/ (the CMake-export layout via cpp/ CMakeLists.txt), so it pulled in include/logos_result.h while the impl's logos_module_context.h pulled include/cpp/logos_result.h. Those are two distinct realpaths under the symlinkJoin, so #pragma once could not dedup them and StdLogosResult was redefined. Install every std header into both roots so a single TU only ever sees one logos_result.h. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): route logos_module_accept_token into the protocol TokenManager The generated module-impl export stored accepted tokens in a process-local std::map (g_tokens) that nothing ever read, so a cdylib module's OUTBOUND lp_client (modules().<dep>...) never saw the capability_module bootstrap token the host delivers at load. The automatic requestModule flow then ran unauthenticated: capability_module rejected requestModule, no per-target token was issued, and the cross-module call was rejected (returning a default-constructed result, e.g. 0). Forward the token into lp_token_save, which writes the same TokenManager::instance() singleton the cdylib's lp_client reads. The capability/token handshake now completes and typed Qt-free outbound calls return real results. Drop the dead g_tokens map + mutex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lidl): exclude LogosModuleContext hooks from header-derived contracts --header-to-lidl parses an impl class's public methods. An impl commonly overrides onContextReady() (and could redeclare a context accessor) in its own public section, so the derived LIDL would include onContextReady / modules / modulePath / instanceId / instancePersistencePath. Those are framework plumbing, not API methods — and feeding them to the cdylib backend breaks cdylib-eligibility (e.g. the inherited accessors' Qt-free return-type check), which is exactly what header-first universal modules now hit. Skip the reserved LogosModuleContext names in the parser so both the Qt --from-header path and the cdylib --header-to-lidl path emit clean, API-only contracts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): support the full std type set in header-derived contracts Routing core universal modules through the cdylib backend surfaced gaps between the cdylib subset and what the std apiStyle handled — a universal module that built under std must also build as a header-first cdylib. - lidl parser: restore the return-shape flags (resultReturn / jsonReturn) from the parsed return TypeExpr, so a header -> .lidl -> cdylib round-trip (the universal path, needed to feed the Qt glue) preserves the semantics the impl-header parser sets from C++ types (StdLogosResult -> result; LogosMap/LogosList -> json). Without this the cdylib codegen/eligibility mis-handled result / map / list returns. - cdylib eligibility + dispatch: `void` is not a lidlBuiltinType, so the parser yields it as a Named "void" (header path uses empty name) — treat both as void in the eligibility check and the dispatch (was relying on lidlTypeToQt=="void", which didn't match Named "void" -> generated an `auto result = <void call>`). - typeSupported: accept `any` (both directions), `void`/`result` (returns), arrays-of-any, and Map ({k:v}/LogosMap) — the Qt-free-via-nlohmann set. Verified: a probe with void / LogosMap / LogosList / StdLogosResult / const returns is cdylib-eligible and dispatches correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lidl): carry method/event descriptions across the .lidl round-trip Header-first universal modules go header -> .lidl -> cdylib backend. The impl-header parser captures /// and /** */ doc comments into method/event descriptions, but the .lidl serializer emitted only the signature, so the descriptions were dropped — introspection (lm methods / --json, getMethods) then showed no docs (regressing the wrap-external-lib + tutorial doctests). Serialize each method/event's description as a trailing `description "..."` clause (escaped for the string literal; the lexer already decodes \\ \" \n \t) and parse it back in parseMethodDef/parseEventDef. Module description now escaped too. Verified: /// docs survive header -> .lidl -> getMethods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lp): bound-interface wrappers are handles over umbrella-owned state The Lp interface (bind_<iface>) wrapper owned its LpClient + RAII subscriptions BY VALUE, so the idiomatic transient handle — modules().bind_calculator(p).fibonacciAsync(...) modules().bind_calculator(p).onVersionReady(...) — tore the client/subscription down when the temporary died, cancelling the async callback and the event subscription. (Sync calls completed before the temporary's destruction, so they worked; the Qt/std flavor works because its handle is thin over a LogosAPI-owned persistent client.) Make the Lp Bound wrapper a THIN, copyable handle over `State { LpClient client; vector<LpSubscription> subs; }` that the LogosModules umbrella OWNS per provider (std::map<provider, unique_ptr<State>>) for the module's lifetime. bind_<iface>(p) creates/looks up the State and returns a handle to it, so a transient handle's async/event registrations outlive it. Concrete (Static) dep wrappers are unchanged — they're already persistent umbrella members, so by-value ownership is fine there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): lenient bytes-param decode (string / array / tagged) A universal module's bstr (std::vector<uint8_t>) PARAM arrived empty when the caller sent a plain string rather than the tagged {"_bytes": base64url} form — lidlBytesFromJson only accepted the tagged object, so byteArraySize("12345") and byteArraySize(b"\x01..") both saw 0 bytes (the return direction already worked). The std path was lenient (a QString or QByteArray arg both became bytes). Accept all three forms: a plain JSON string (raw UTF-8 bytes), an array of byte values, and the tagged {"_bytes"} form (base64url). Return direction unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): a number arg to a bytes param decodes as its decimal text byteArraySize("12345") arrives as a JSON number (the logoscore CLI's type auto-detection turns the string "12345" into int 12345), and the Qt path gives QVariant(int)->QByteArray "12345" (5 bytes). The cdylib bstr decode returned 0 for a number. Treat a JSON number as its decimal text bytes (j.dump()), matching the Qt behaviour, so a bare-number arg to a bytes param round-trips identically. Verified: byteArraySize 12345 -> 5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bb6d87b6ec |
fix: parse declarations on same line as logos_events: / access specifier (#76) (#81)
* fix: parse declarations on same line as logos_events/access specifier (#76) The impl header parser updated its section state and immediately broke out of line processing when it matched `logos_events:` (or `public:`/`private:`), discarding any declaration on the same physical line. This meant clang-format / prettier output like logos_events : void versionReady(const std::string &version); silently dropped the event, while the newline-separated form parsed fine — the same valid C++ was handled differently based on formatting. Strip any leading section specifiers in a loop, updating the section state, then let the remainder of the line fall through to the declaration parser. Brace counting still happens once per physical line and blank-line doc-comment reset is preserved. Adds a regression test (SameLineSectionSpecifiers) with a fixture covering the exact prettier form from the issue, a follow-on same-line event, the newline form alongside it, and the symmetric inline `public:` method case. Fixes #76 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: attach doc comments to same-line logos_events/access-specifier decls Address review feedback: the first pass cleared pendingDoc on every specifier match, so a `///` comment above a collapsed `logos_events : void foo();` did not attach to the event. In the collapsed form there is nowhere else to put the doc comment, so this left documentation formatting-dependent — the same bug class as #76, one level up. Only clear pendingDoc for a *bare* specifier (a section boundary, matching Qt `signals:` semantics); when a declaration shares the line, keep the pending doc so the declaration parser attaches it. Extend the fixture with a `///`-documented same-line event and assert the description is captured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b62ac2017 |
Per-event documentation + getPluginEvents introspection (#71)
* 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> |
||
|
|
760916e97b |
Parse method doc comments into per-method description (#70)
* parse adjacent method comments to populate description * preserve line breaks in method descriptions Join doc-comment lines with newlines instead of spaces (markers stripped, leading/trailing blank lines dropped, interior blanks kept), and escape \n when emitting the description into the generated getMethods(). Both codegen paths updated; docs corrected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * don't count braces inside comment lines (impl-header parser) A brace in a doc/line comment (e.g. `/// returns { ... }`) no longer affects class-scope tracking, which previously could make the parser think the class ended early and drop later declarations. Addresses review feedback on #70. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c71089abe9 | remove legacy emitEvent (#69) | ||
|
|
8bdbd13848 |
Extend universal modules with module context (#61)
* extend universal modules with module context * implement module calls and events for universal modules * pr comments |
||
|
|
f7c855b110 | add logos result type (#55) | ||
|
|
1468180b25 | add support for new types (#50) | ||
|
|
d633575677 |
add IDL parser & generator (wip) (#33)
* add IDL parser & generator (wip) * fix fixtures issues affecting tests * fix fixtures issues affecting tests |