From 87abcd804422cbd733c2e658ae2f42c20162115c Mon Sep 17 00:00:00 2001 From: Dario Lipicar Date: Fri, 12 Jun 2026 22:10:16 -0300 Subject: [PATCH] Cdylib authoring: --backend cdylib emits the common C ABI wrapper + uniform Qt glue (#84) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Extract the protocol layer into logos-protocol; consume it as a flake input 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). * lock: add logos-protocol input * Make the base SDK Qt-free: move the Qt developer layer to logos-qt-sdk LogosAPI, LogosAPIProvider, LogosProviderBase/LOGOS_PROVIDER macros, the QObject provider glue (QtProviderObject) and the legacy PluginInterface (core/interface.h) move to the new logos-qt-sdk repo. The protocol sources are no longer compiled into a monolithic archive — consumers link logos-qt-sdk (which layers on logos-protocol) instead. What remains here is header-only std C++: logos_module_context.h, logos_result.h (StdLogosResult), logos_json.h — exported as the CMake INTERFACE target logos-cpp-sdk::logos_headers — plus the code generator (a build-time tool; its introspection mode now includes logos_provider_interface.h from logos-protocol, where LogosProviderPlugin moved). Mechanically verified Qt-free: the logos-cpp-lib / logos-cpp-include closures contain only nlohmann_json. Tests: 245/245 (module-context std suite + generator + experimental). * Cdylib authoring backend: --backend cdylib emits the common C ABI wrapper + uniform Qt glue From a module's LIDL contract the generator now emits: - _module_impl.cpp — the Qt-FREE logos_module_impl.h export wrapper (dispatch/get_methods/set_context/set_emit_callback/ accept_token/get_protocol_version/string_free) around the universal impl class; compiled into the module's cdylib. Tagged {"_bytes"} bytes, StdLogosResult -> {success,value,error}, context via the existing _logos_codegen_::maybeSet* SFINAE helpers. - _events_cdylib.cpp — typed logos_events: bodies marshalling into nlohmann::json (the cdylib flavor of the events sidecar). - _cdylib_glue.{h,cpp} — the UNIFORM Qt-plugin glue forwarding LogosProviderObject to the C symbols; identical regardless of the module's source language (the Rust SDK emits the same exports). - the .lidl sidecar. Qt-container types are rejected at generation time (a cdylib impl is Qt-free by definition). Verified end-to-end by dlopen smoke: typed dispatch, typed events through the emit callback, result returns, introspection, and the protocol-version handshake — and the SAME C harness passes against a Rust cdylib generated by logos-lidl-gen --provider. * fix: accept the installed source-export layout in the protocol-root check The fail-fast only tested /cpp/logos_protocol.h, but the LP_SRC selection right below (and the error message itself) support the installed export layout /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) * lock: pin logos-protocol to the qt-free-split branch head The Qt-free SDK (and the cdylib backend stacked on it) reference LogosProviderPlugin from protocol's logos_provider_interface.h, which lands on feat/qt-free-split — the P1-branch pin no longer compiles standalone. Temporary — drop when the chain PRs merge. Co-Authored-By: Claude Opus 4.8 (1M context) * lock: pin logos-protocol to the qt-free-split branch head The Qt-free SDK (and the cdylib backend stacked on it) reference LogosProviderPlugin from protocol's logos_provider_interface.h, which lands on feat/qt-free-split — the P1-branch pin no longer compiles standalone. Temporary — drop when the chain PRs merge. Co-Authored-By: Claude Opus 4.8 (1M context) * doctest: pin the logoscore runtime via its {release} placeholder The spec built logoscore-cli at bare master with only the cpp-sdk inputs overridden — master's stack cannot compile against the qt-free SDK, so the suite failed on the chain branches. With the placeholder, CI's --release-for pins expand it to the workspace's logoscore commit (and local runs without a pin still fall back to master, unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) * doctest: override the nested module builders to {release} too capability_module (via logoscore's lock) and the cloned accounts module resolve module-builder from their own locks — pre-split revs whose LogosModule.cmake still detects the SDK by logos_api.h, which the qt-free SDK no longer ships ('logos-cpp-sdk not found'). Overriding the builder itself to the workspace-pinned chain rev (keeping the nested cpp-sdk override) builds both modules with the split-aware builder. Verified end-to-end locally with the exact doctest command. Co-Authored-By: Claude Opus 4.8 (1M context) * doctest: apply the {release} + nested-builder overrides to all three specs The runtime spec got the treatment in 210eea1; the composition and worker-thread specs have the same logoscore/module build commands and failed identically (pre-split builders from the modules' own locks). All executed run: blocks now pin logoscore-cli{release} and override the nested module builders to logos-module-builder{release}; the displayed code_block: variants stay in their generic master form. Co-Authored-By: Claude Opus 4.8 (1M context) * generator: glue-only cdylib mode for non-C++ impls --lidl x.lidl --backend cdylib (no --impl-class) emits just the uniform Qt-plugin glue; the C exports come from the module's own language backend (e.g. the Rust SDK's lidl-gen --provider). This is the entry point logos-module-builder's cdylib interface uses for Rust modules. * cdylib glue: root plugin implements PluginInterface logos_host's module_initializer hard-requires PluginInterface on the root plugin before any provider detection — same bases as the qt glue (QObject, PluginInterface, LogosProviderPlugin). Caught by the first host-loaded run of a cdylib-authored module; the dlopen smoke harness exercised only the C seam. * cdylib glue: seed the cdylib's protocol stack with the host auth token The glue's init() reads the authToken property module_initializer now surfaces on the LogosAPI object and forwards it across logos_module_accept_token under the initializer's own keys (core / capability_module). Without it the cdylib's TokenManager (a separate static copy of the singleton) is empty and every outbound call — including the capability requestModule bootstrap — is rejected as unauthorized. * cdylib glue: provider derives LogosProviderBase; tokens land in BOTH stacks informModuleToken = base save (host-stack TokenManager — what ModuleProxy validates inbound calls against, incl. the grant the daemon pushes after a capability requestModule) + C-ABI forward (the cdylib's own stack, for outbound auth). init folds into onInit; the authToken property seeding stays. * lock: protocol integer-fidelity fix * codegen: typed wrappers throw on call failure; dispatch catches escapes Generated sync client wrappers call the new err-out invokeRemoteMethod overload and throw logos::LogosCallError when the call fails (e.g. the bound module is missing) — previously the empty QVariant silently degraded to the return type's default and a caller could not tell failure from a legitimate 0 / "". Both generators (legacy + LIDL), both API styles. Async paths unchanged. Generated provider dispatch (universal qt glue + LOGOS_PROVIDER) wraps the method body in a catch-all that logs and returns an invalid QVariant — an escaped exception becomes an ordinary METHOD_FAILED instead of unwinding through Qt event dispatch and killing the module process. * fix: restore a clean flake.lock after the merge conflict (protocol 176fbc8) * codegen: CallError out-param instead of throwing wrappers Per review, the generated sync wrappers expose the error channel as an optional trailing parameter — add(a, b, &err) — rather than throwing: explicit, stateless, works on temporaries, and existing call sites compile unchanged (they keep default-on-failure, now with a qWarning so failures are visible in the module log). The dispatch catch-all from the previous commit stays: it contains author exceptions, it doesn't introduce any. * generator: contract-first C++ cdylib modules from --lidl --lidl x.lidl --backend cdylib with --impl-class/--impl-header now emits the FULL set (C-ABI export wrapper + events + uniform glue) around the named hand-written Qt-free impl class — the C++ mirror of declaring the contract in .lidl and implementing the Rust trait. Without --impl-class the glue-only mode is unchanged. * glue: fire onContextReady AFTER modules()/event wiring The generated onInit set the context (which fires the impl's onContextReady hook) before constructing the LogosModules aggregate and wiring typed event emission — so an impl doing its documented one-time setup there (typed dependency calls, event subscriptions) dereferenced a null aggregate and crashed the module process (signal 11). Found by the first module to subscribe to a dependency's typed event from onContextReady. Context now goes last. * cdylib: fire the context-ready hook at module LOAD, not first dispatch The impl-exports wrapper used to fire maybeSetContext (stamp + hook) immediately inside logos_module_set_context — which the glue calls during onInit, BEFORE the auth token is seeded and BEFORE ModuleProxy wires the emit callback. A hook that made outbound calls or emitted events ran half-wired; and the Rust scaffold deferred its hook to first dispatch entirely, so a Rust module couldn't subscribe/emit/act until someone called it — a capability gap vs the universal C++ path. Two changes, applied uniformly: - The glue's onInit now seeds the auth token FIRST and forwards the context LAST (the same context-last convention as the universal onInit ordering fix). - The impl-exports gained a ready-latch (lidlTryFireContext): the context is stored on set_context and the hook fires ONCE as soon as both the context AND the emit callback have been delivered — during module registration, before the module is published for inbound calls. Hosts that never wire an emit callback still get the hook before the first dispatch (requireEmit=false fallback in logos_module_dispatch). The Rust scaffold (logos-rust-sdk lidl-gen) implements the same latch, so on_context_ready now matches C++ onContextReady semantics: subscriptions, authenticated outbound calls and typed emission all work from the hook at load time. * ci: run workflows on stacked PRs + workflow_dispatch Both workflows filtered pull_request to master-based PRs, so stacked PRs (feat/qt-free-sdk -> feat/extract-logos-protocol, feat/cdylib-authoring -> feat/qt-free-sdk) ran NO checks at all. Drop the base-branch filter for pull_request and add workflow_dispatch for manual runs. Same fix as logos-module-builder 232b8a2. * generator: --backend ui — universal authoring for UI plugin backends New emitters (lidl_gen_ui.{h,cpp}) for type=ui_qml + interface=universal modules: from the author's single clean impl class (optionally deriving LogosModuleContext), generate .rep — the view contract, one SLOT per public method (framework hooks like onContextReady excluded) _ui_interface.h — PluginInterface subclass + IID _ui_glue.{h,cpp}— plugin deriving SimpleSource + Interface + ViewPluginBase; slots forward to the impl (std<->Qt at the boundary); Q_INVOKABLE initLogos(LogosAPI*) builds LogosModules, wires modules(), stamps context (fires onContextReady — same order as the universal core glue), then setBackend(this). Typed dependency callers, typed event subscriptions and bind_ binders come from the existing umbrella pass (logos_sdk.h), which already runs for UI modules. ui-host's reflection-based initLogos call site is unchanged; legacy LogosAPI*-based UI plugins are untouched. v1 view API types: void/int/uint/float64/bool/string; logos_events: rejected for ui backends (views talk to QML via .rep, not module events). * generator: distribute the LIDL frontend for external generators First step of moving ALL Qt glue emission out of this repo into logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib impl-exports, LIDL derivation). - Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd, lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used by both generators. - The frontend set (AST, lexer, parser, serializer, validator, impl-header parser, emit-common) is installed under share/lidl-frontend/ — the qt generator compiles these sources in directly, so the two tools share one frontend without a binary ABI. * generator: Qt glue emission removed — logos-qt-generator owns it The deletion half of the generator split (counterpart: logos-qt-sdk 3b37474, builder d7a2272). This tool now emits ONLY Qt-free outputs: kept std typed wrappers + logos_sdk umbrella (--general-only), cdylib C-ABI impl-exports + typed event emitters (--lidl/--from-header --backend cdylib --impl-class), LIDL derivation/serialization (--header-to-lidl), client stubs removed universal Qt glue (--backend qt), the uniform cdylib Qt glue, the ui backend emitters — all relocated verbatim (byte-identical output verified) to logos-qt-generator; invocations here now fail with a pointer to the right tool The provider-glue golden tests travel with the emitters (to be re-homed in logos-qt-sdk's test suite); test_lidl_type_mapping stays — it covers lidl_emit_common, which both generators compile. * lock: protocol at the typed-requestModule port (3de5398) * lock: protocol at the typed-requestModule port (3de5398) * ci: chain pins for the doc-tests (drop at merge) In repo CI only cpp-sdk's {release} is the commit under test — logoscore-cli and module-builder expanded to master, which doesn't link against the chain SDK the specs override in ('Build the CLI with the SDK override' failed on every run since the stacked-PR triggers were enabled). Pin both to the extraction-chain heads; the workspace pipeline is unaffected (it pins every repo itself). * generator: distribute the LIDL frontend for external generators First step of moving ALL Qt glue emission out of this repo into logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib impl-exports, LIDL derivation). - Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd, lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used by both generators. - The frontend set (AST, lexer, parser, serializer, validator, impl-header parser, emit-common) is installed under share/lidl-frontend/ — the qt generator compiles these sources in directly, so the two tools share one frontend without a binary ABI. * docs: note the generator split (Qt glue emission lives in logos-qt-sdk) * lock: protocol#3 merged — pin advances to protocol master --------- Co-authored-by: Claude Opus 4.8 (1M context) --- README.md | 4 +- cpp-generator/CMakeLists.txt | 2 +- cpp-generator/docs/project.md | 2 +- .../experimental/lidl_gen_cdylib.cpp | 433 +++++++++++ cpp-generator/experimental/lidl_gen_cdylib.h | 41 + .../experimental/lidl_gen_provider.cpp | 732 ------------------ .../experimental/lidl_gen_provider.h | 39 - cpp-generator/main.cpp | 170 ++-- tests/experimental/CMakeLists.txt | 2 - tests/experimental/test_lidl_gen_provider.cpp | 462 ----------- tests/experimental/test_lidl_type_mapping.cpp | 2 +- 11 files changed, 593 insertions(+), 1296 deletions(-) create mode 100644 cpp-generator/experimental/lidl_gen_cdylib.cpp create mode 100644 cpp-generator/experimental/lidl_gen_cdylib.h delete mode 100644 cpp-generator/experimental/lidl_gen_provider.cpp delete mode 100644 cpp-generator/experimental/lidl_gen_provider.h delete mode 100644 tests/experimental/test_lidl_gen_provider.cpp diff --git a/README.md b/README.md index a73d99e..4b0a331 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ > repo is the **Qt-free base SDK** — header-only developer surface for > universal module implementations (`logos_module_context.h`, > `logos_result.h`, `logos_json.h`) plus the `logos-cpp-generator` code -> generator. Transports, the consumer core and the `lp_*` C ABI live in +> generator (Qt-free outputs: std typed wrappers, the `logos_sdk` umbrella, +> cdylib C-ABI impl-exports, LIDL derivation — ALL generated Qt glue comes +> from logos-qt-sdk's `logos-qt-generator`). Transports, the consumer core and the `lp_*` C ABI live in > [logos-protocol](https://github.com/logos-co/logos-protocol); the Qt > developer layer (LogosAPI, provider base classes, QObject glue) lives in > [logos-qt-sdk](https://github.com/logos-co/logos-qt-sdk). diff --git a/cpp-generator/CMakeLists.txt b/cpp-generator/CMakeLists.txt index 73329f4..f4edddf 100644 --- a/cpp-generator/CMakeLists.txt +++ b/cpp-generator/CMakeLists.txt @@ -19,7 +19,7 @@ add_executable(logos-cpp-generator experimental/lidl_serializer.cpp experimental/lidl_validator.cpp experimental/lidl_gen_client.cpp - experimental/lidl_gen_provider.cpp + experimental/lidl_gen_cdylib.cpp experimental/impl_header_parser.cpp ) diff --git a/cpp-generator/docs/project.md b/cpp-generator/docs/project.md index 280ae9d..5095039 100644 --- a/cpp-generator/docs/project.md +++ b/cpp-generator/docs/project.md @@ -18,7 +18,7 @@ cpp-generator/ │ ├── lidl_validator.h/cpp # Semantic validation │ ├── lidl_serializer.h/cpp # AST → LIDL text pretty-printer │ ├── lidl_gen_client.h/cpp # Client stub generation + helpers -│ ├── lidl_gen_provider.h/cpp # Provider glue + dispatch generation +│ ├── (Qt glue emitters live in logos-qt-sdk's logos-qt-generator) │ └── impl_header_parser.h/cpp # C++ header → ModuleDecl parser └── docs/ # This documentation ``` diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp new file mode 100644 index 0000000..e863f09 --- /dev/null +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -0,0 +1,433 @@ +#include "lidl_gen_cdylib.h" +#include "lidl_emit_common.h" + +#include + +QString lidlToPascalCase(const QString& name); +QString lidlTypeToQt(const TypeExpr& te); +bool lidlIsStdConvertible(const TypeExpr& te); + +namespace { + +// The cdylib-supported subset: std-convertible LIDL types only. +bool typeSupported(const TypeExpr& te, bool isReturn) +{ + if (te.kind == TypeExpr::Primitive) { + if (te.name == "tstr" || te.name == "bstr" || te.name == "int" + || te.name == "uint" || te.name == "float64" || te.name == "bool") + return true; + // result (StdLogosResult) and any (LogosMap/LogosList via jsonReturn) + // are fine as RETURNS — the generator routes them through nlohmann. + if (isReturn && (te.name == "result" || te.name == "any")) + return true; + return false; + } + if (te.kind == TypeExpr::Array && te.elements.size() == 1) { + const TypeExpr& e = te.elements[0]; + return e.kind == TypeExpr::Primitive + && (e.name == "tstr" || e.name == "int" || e.name == "uint" + || e.name == "float64" || e.name == "bool"); + } + return false; +} + +// json arg expression -> std-typed C++ expression +QString jsonArgToStd(const TypeExpr& te, const QString& expr) +{ + if (te.kind == TypeExpr::Primitive) { + if (te.name == "tstr") return expr + ".get()"; + if (te.name == "bstr") return "lidlBytesFromJson(" + expr + ")"; + if (te.name == "int") return expr + ".get()"; + if (te.name == "uint") return expr + ".get()"; + if (te.name == "float64") return expr + ".get()"; + if (te.name == "bool") return expr + ".get()"; + } + if (te.kind == TypeExpr::Array && te.elements.size() == 1) { + const QString inner = lidlTypeToStd(te); + return expr + ".get<" + inner + ">()"; + } + return expr; +} + +// std-typed return variable -> json expression +QString stdReturnToJson(const MethodDecl& md, const QString& var) +{ + const TypeExpr& te = md.returnType; + if (md.resultReturn) { + // StdLogosResult -> the canonical {success, value, error} object + // (same shape logos_json_convert emits for Qt LogosResult). + return "lidlResultToJson(" + var + ")"; + } + if (md.jsonReturn) { + return var; // LogosMap / LogosList are nlohmann::json already + } + if (te.kind == TypeExpr::Primitive) { + if (te.name == "bstr") return "lidlBytesToJson(" + var + ")"; + return "nlohmann::json(" + var + ")"; + } + return "nlohmann::json(" + var + ")"; +} + +void emitInterfaceJson(QTextStream& s, const ModuleDecl& module) +{ + s << "static nlohmann::json lidlInterfaceJson()\n{\n"; + s << " nlohmann::json methods = nlohmann::json::array();\n"; + for (const MethodDecl& md : module.methods) { + s << " {\n nlohmann::json obj;\n"; + s << " obj[\"name\"] = \"" << md.name << "\";\n"; + if (!md.description.isEmpty()) { + QString esc = md.description; + esc.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"); + s << " obj[\"description\"] = \"" << esc << "\";\n"; + } + QString sig = md.name + "("; + for (int i = 0; i < md.params.size(); ++i) { + sig += lidlTypeToQt(md.params[i].type); + if (i + 1 < md.params.size()) sig += ","; + } + sig += ")"; + s << " obj[\"signature\"] = \"" << sig << "\";\n"; + s << " obj[\"returnType\"] = \"" << lidlTypeToQt(md.returnType) << "\";\n"; + s << " obj[\"isInvokable\"] = true;\n"; + if (!md.params.isEmpty()) { + s << " nlohmann::json params = nlohmann::json::array();\n"; + for (const ParamDecl& pd : md.params) { + s << " params.push_back({{\"type\", \"" << lidlTypeToQt(pd.type) + << "\"}, {\"name\", \"" << pd.name << "\"}});\n"; + } + s << " obj[\"parameters\"] = params;\n"; + } + s << " methods.push_back(obj);\n }\n"; + } + for (const EventDecl& ed : module.events) { + s << " {\n nlohmann::json obj;\n"; + s << " obj[\"type\"] = \"event\";\n"; + s << " obj[\"name\"] = \"" << ed.name << "\";\n"; + if (!ed.description.isEmpty()) { + QString esc = ed.description; + esc.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n"); + s << " obj[\"description\"] = \"" << esc << "\";\n"; + } + QString sig = ed.name + "("; + for (int i = 0; i < ed.params.size(); ++i) { + sig += lidlTypeToQt(ed.params[i].type); + if (i + 1 < ed.params.size()) sig += ","; + } + sig += ")"; + s << " obj[\"signature\"] = \"" << sig << "\";\n"; + if (!ed.params.isEmpty()) { + s << " nlohmann::json params = nlohmann::json::array();\n"; + for (const ParamDecl& pd : ed.params) { + s << " params.push_back({{\"type\", \"" << lidlTypeToQt(pd.type) + << "\"}, {\"name\", \"" << pd.name << "\"}});\n"; + } + s << " obj[\"parameters\"] = params;\n"; + } + s << " methods.push_back(obj);\n }\n"; + } + s << " return methods;\n}\n\n"; +} + +} // namespace + +bool lidlCdylibSupported(const ModuleDecl& module, QString* error) +{ + for (const MethodDecl& md : module.methods) { + for (const ParamDecl& pd : md.params) { + if (!typeSupported(pd.type, /*isReturn=*/false)) { + if (error) + *error = QString("method '%1': parameter '%2' has a type outside the " + "cdylib-supported (Qt-free) subset") + .arg(md.name, pd.name); + return false; + } + } + const bool voidReturn = + md.returnType.kind == TypeExpr::Primitive && md.returnType.name.isEmpty(); + if (!voidReturn && !md.jsonReturn && !md.resultReturn + && !typeSupported(md.returnType, /*isReturn=*/true)) { + if (error) + *error = QString("method '%1': return type outside the cdylib-supported " + "(Qt-free) subset").arg(md.name); + return false; + } + } + for (const EventDecl& ed : module.events) { + for (const ParamDecl& pd : ed.params) { + if (!typeSupported(pd.type, /*isReturn=*/false)) { + if (error) + *error = QString("event '%1': parameter '%2' has a type outside the " + "cdylib-supported (Qt-free) subset") + .arg(ed.name, pd.name); + return false; + } + } + } + return true; +} + +QString lidlMakeModuleImplExports(const ModuleDecl& module, + const QString& implClass, + const QString& implHeader) +{ + QString c; + QTextStream s(&c); + + s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n"; + s << "//\n"; + s << "// The common module-impl C ABI exports (logos_module_impl.h) around the\n"; + s << "// universal impl class `" << implClass << "`. Qt-FREE: compiled into the\n"; + s << "// module's cdylib; the uniform Qt-plugin glue (or a future no-Qt host)\n"; + s << "// drives it exclusively through these symbols.\n"; + s << "#include \"" << implHeader << "\"\n"; + s << "#include \"logos_module_impl.h\"\n"; + s << "#include \"logos_protocol.h\"\n"; + s << "#include \"logos_module_context.h\"\n"; + s << "#include \"logos_result.h\"\n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n\n"; + + // -- shared statics ------------------------------------------------------ + s << "namespace {\n\n"; + s << implClass << "& lidlImpl()\n{\n static " << implClass << " impl;\n return impl;\n}\n\n"; + s << "logos_module_emit_cb g_emitCb = nullptr;\n"; + s << "void* g_emitUd = nullptr;\n"; + s << "std::mutex g_emitMutex;\n"; + s << "std::map g_tokens;\n"; + s << "std::mutex g_tokensMutex;\n"; + s << "std::mutex g_ctxMutex;\n"; + s << "bool g_ctxStored = false;\n"; + s << "std::string g_ctxPath, g_ctxId, g_ctxPersist;\n"; + s << "std::atomic g_hookFired{false};\n\n"; + + s << "char* lidlStrdup(const std::string& str)\n{\n"; + s << " char* out = static_cast(std::malloc(str.size() + 1));\n"; + s << " if (out) std::memcpy(out, str.data(), str.size() + 1);\n"; + s << " return out;\n}\n\n"; + + s << "// Canonical tagged bytes form {\"_bytes\": base64url} (see logos_protocol.h)\n"; + s << "std::string lidlB64UrlEncode(const std::vector& bytes)\n{\n"; + s << " static const char* alpha = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n"; + s << " std::string out;\n"; + s << " size_t i = 0;\n"; + s << " while (i + 3 <= bytes.size()) {\n"; + s << " uint32_t n = (uint32_t(bytes[i]) << 16) | (uint32_t(bytes[i+1]) << 8) | uint32_t(bytes[i+2]);\n"; + s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n"; + s << " out += alpha[(n >> 6) & 0x3f]; out += alpha[n & 0x3f];\n"; + s << " i += 3;\n }\n"; + s << " if (i < bytes.size()) {\n"; + s << " uint32_t n = uint32_t(bytes[i]) << 16;\n"; + s << " if (i + 1 < bytes.size()) n |= uint32_t(bytes[i+1]) << 8;\n"; + s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n"; + s << " if (i + 1 < bytes.size()) out += alpha[(n >> 6) & 0x3f];\n"; + s << " }\n return out;\n}\n\n"; + + s << "int lidlB64Idx(char ch)\n{\n"; + s << " if (ch >= 'A' && ch <= 'Z') return ch - 'A';\n"; + s << " if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26;\n"; + s << " if (ch >= '0' && ch <= '9') return ch - '0' + 52;\n"; + s << " if (ch == '-') return 62;\n if (ch == '_') return 63;\n return -1;\n}\n\n"; + + s << "std::vector lidlBytesFromJson(const nlohmann::json& j)\n{\n"; + s << " std::vector out;\n"; + s << " if (!j.is_object() || j.size() != 1 || !j.contains(\"_bytes\") || !j[\"_bytes\"].is_string())\n"; + s << " return out;\n"; + s << " const std::string s64 = j[\"_bytes\"].get();\n"; + s << " size_t i = 0;\n"; + s << " while (i + 4 <= s64.size()) {\n"; + s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]), c2 = lidlB64Idx(s64[i+2]), d = lidlB64Idx(s64[i+3]);\n"; + s << " if (a < 0 || b < 0 || c2 < 0 || d < 0) return {};\n"; + s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12) | (uint32_t(c2) << 6) | uint32_t(d);\n"; + s << " out.push_back((n >> 16) & 0xff); out.push_back((n >> 8) & 0xff); out.push_back(n & 0xff);\n"; + s << " i += 4;\n }\n"; + s << " size_t rem = s64.size() - i;\n"; + s << " if (rem == 2 || rem == 3) {\n"; + s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]);\n"; + s << " if (a < 0 || b < 0) return {};\n"; + s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12);\n"; + s << " out.push_back((n >> 16) & 0xff);\n"; + s << " if (rem == 3) {\n"; + s << " int c2 = lidlB64Idx(s64[i+2]);\n"; + s << " if (c2 < 0) return {};\n"; + s << " n |= uint32_t(c2) << 6;\n"; + s << " out.push_back((n >> 8) & 0xff);\n"; + s << " }\n }\n return out;\n}\n\n"; + + s << "nlohmann::json lidlBytesToJson(const std::vector& bytes)\n{\n"; + s << " return nlohmann::json{{\"_bytes\", lidlB64UrlEncode(bytes)}};\n}\n\n"; + + s << "nlohmann::json lidlResultToJson(const StdLogosResult& r)\n{\n"; + s << " nlohmann::json obj;\n"; + s << " obj[\"success\"] = r.success;\n"; + s << " obj[\"value\"] = r.value;\n"; + s << " obj[\"error\"] = r.error.empty() ? nlohmann::json() : nlohmann::json(r.error);\n"; + s << " return obj;\n}\n\n"; + + emitInterfaceJson(s, module); + s << "} // namespace\n\n"; + + // -- event wiring (install once, lazily) --------------------------------- + s << "static void lidlEnsureEmitWiring()\n{\n"; + s << " static std::once_flag once;\n"; + s << " std::call_once(once, []() {\n"; + s << " _logos_codegen_::maybeSetEmitEvent(lidlImpl(),\n"; + s << " [](const std::string& name, void* args) {\n"; + s << " // cdylib events sidecar marshals into nlohmann::json\n"; + s << " const nlohmann::json* payload = static_cast(args);\n"; + s << " std::lock_guard lock(g_emitMutex);\n"; + s << " if (g_emitCb) {\n"; + s << " const std::string dumped = payload ? payload->dump() : \"[]\";\n"; + s << " g_emitCb(name.c_str(), dumped.c_str(), g_emitUd);\n"; + s << " }\n"; + s << " });\n"; + s << " });\n}\n\n"; + + // The context ready-latch: stamp the context + fire onContextReady ONCE, + // as soon as the module is fully wired (context stored AND the emit + // callback delivered) — at module load, before publication. Hosts that + // never wire an emit callback still get the hook before first dispatch + // (requireEmit = false fallback). + s << "static void lidlTryFireContext(bool requireEmit)\n{\n"; + s << " lidlEnsureEmitWiring();\n"; + s << " if (g_hookFired.load(std::memory_order_acquire)) return;\n"; + s << " std::string path, id, persist;\n"; + s << " {\n"; + s << " std::lock_guard lock(g_ctxMutex);\n"; + s << " if (!g_ctxStored) return;\n"; + s << " path = g_ctxPath; id = g_ctxId; persist = g_ctxPersist;\n"; + s << " }\n"; + s << " if (requireEmit) {\n"; + s << " std::lock_guard lock(g_emitMutex);\n"; + s << " if (!g_emitCb) return;\n"; + s << " }\n"; + s << " g_hookFired.store(true, std::memory_order_release);\n"; + s << " _logos_codegen_::maybeSetContext(lidlImpl(), path, id, persist);\n"; + s << "}\n\n"; + + // -- exports ------------------------------------------------------------- + s << "extern \"C\" {\n\n"; + + s << "char* logos_module_dispatch(const char* method, const char* args_json)\n{\n"; + s << " if (!method) return nullptr;\n"; + s << " lidlTryFireContext(false);\n"; + s << " nlohmann::json args = nlohmann::json::array();\n"; + s << " if (args_json && *args_json) {\n"; + s << " args = nlohmann::json::parse(args_json, nullptr, false);\n"; + s << " if (args.is_discarded() || !args.is_array()) return nullptr;\n"; + s << " }\n"; + s << " const std::string m(method);\n"; + s << " try {\n"; + + for (const MethodDecl& md : module.methods) { + s << " if (m == \"" << md.name << "\") {\n"; + s << " if (args.size() < " << md.params.size() << ") return nullptr;\n"; + QString call = "lidlImpl()." + md.name + "("; + for (int i = 0; i < md.params.size(); ++i) { + call += jsonArgToStd(md.params[i].type, + QString("args.at(%1)").arg(i)); + if (i + 1 < md.params.size()) call += ", "; + } + call += ")"; + const bool voidReturn = lidlTypeToQt(md.returnType) == "void"; + if (voidReturn) { + s << " " << call << ";\n"; + s << " return lidlStrdup(\"true\");\n"; + } else { + s << " auto result = " << call << ";\n"; + s << " return lidlStrdup(" << stdReturnToJson(md, "result") << ".dump());\n"; + } + s << " }\n"; + } + + s << " } catch (const std::exception& e) {\n"; + s << " nlohmann::json err{{\"code\", \"dispatch_failed\"}, {\"message\", e.what()},\n"; + s << " {\"origin\", \"" << module.name << "\"}};\n"; + s << " return lidlStrdup(err.dump());\n"; + s << " }\n"; + s << " return nullptr; // unknown method\n"; + s << "}\n\n"; + + s << "char* logos_module_get_methods(void)\n{\n"; + s << " return lidlStrdup(lidlInterfaceJson().dump());\n}\n\n"; + + s << "void logos_module_set_context(const char* module_path,\n"; + s << " const char* instance_id,\n"; + s << " const char* instance_persistence_path)\n{\n"; + s << " {\n"; + s << " std::lock_guard lock(g_ctxMutex);\n"; + s << " g_ctxPath = module_path ? module_path : \"\";\n"; + s << " g_ctxId = instance_id ? instance_id : \"\";\n"; + s << " g_ctxPersist = instance_persistence_path ? instance_persistence_path : \"\";\n"; + s << " g_ctxStored = true;\n"; + s << " }\n"; + s << " lidlTryFireContext(true);\n"; + s << "}\n\n"; + + s << "void logos_module_set_emit_callback(logos_module_emit_cb cb, void* user_data)\n{\n"; + s << " {\n"; + s << " std::lock_guard lock(g_emitMutex);\n"; + s << " g_emitCb = cb;\n"; + s << " g_emitUd = user_data;\n"; + s << " }\n"; + s << " lidlTryFireContext(true);\n"; + s << "}\n\n"; + + s << "int logos_module_accept_token(const char* module_name, const char* token)\n{\n"; + s << " if (!module_name || !token) return -1;\n"; + s << " std::lock_guard lock(g_tokensMutex);\n"; + s << " g_tokens[module_name] = token;\n"; + s << " return 0;\n}\n\n"; + + s << "const char* logos_module_get_protocol_version(void)\n{\n"; + s << " return LOGOS_PROTOCOL_VERSION_STRING;\n}\n\n"; + + s << "void logos_module_string_free(char* str)\n{\n"; + s << " std::free(str);\n}\n\n"; + + s << "} // extern \"C\"\n"; + return c; +} + +QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, + const QString& implClass, + const QString& implHeader) +{ + QString c; + QTextStream s(&c); + s << "// AUTO-GENERATED by logos-cpp-generator --cdylib -- do not edit\n"; + s << "// Typed `logos_events:` bodies, cdylib flavor: marshal into\n"; + s << "// nlohmann::json and route through LogosModuleContext::emitEventImpl_\n"; + s << "// (the export wrapper forwards to the host's emit callback).\n"; + s << "#include \"" << implHeader << "\"\n"; + s << "#include \n\n"; + + for (const EventDecl& ed : module.events) { + s << "void " << implClass << "::" << ed.name << "("; + for (int i = 0; i < ed.params.size(); ++i) { + const QString stdType = lidlTypeToStd(ed.params[i].type); + if (stdType == "std::string" || stdType.startsWith("std::vector")) + s << "const " << stdType << "& " << ed.params[i].name; + else + s << stdType << " " << ed.params[i].name; + if (i + 1 < ed.params.size()) s << ", "; + } + s << ")\n{\n"; + s << " nlohmann::json args = nlohmann::json::array();\n"; + for (const ParamDecl& pd : ed.params) { + if (pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr") + s << " args.push_back(nlohmann::json{{\"_bytes\", \"\"}}); // bstr events: encode upstream\n"; + else + s << " args.push_back(" << pd.name << ");\n"; + } + s << " emitEventImpl_(\"" << ed.name << "\", &args);\n"; + s << "}\n\n"; + } + return c; +} + diff --git a/cpp-generator/experimental/lidl_gen_cdylib.h b/cpp-generator/experimental/lidl_gen_cdylib.h new file mode 100644 index 0000000..584eaf6 --- /dev/null +++ b/cpp-generator/experimental/lidl_gen_cdylib.h @@ -0,0 +1,41 @@ +#ifndef LIDL_GEN_CDYLIB_H +#define LIDL_GEN_CDYLIB_H + +#include "lidl_ast.h" +#include + +// --------------------------------------------------------------------------- +// Cdylib authoring backend — the common module-impl C ABI seam. +// +// Emits, from a module's LIDL contract: +// 1. _module_impl.cpp — the Qt-FREE C-ABI export wrapper +// (logos_module_impl.h symbols) around the universal C++ impl class. +// Compiled into the module's cdylib together with the impl. +// 2. _events_cdylib.cpp — typed `logos_events:` bodies marshalling +// into nlohmann::json (the cdylib flavor of _events.cpp). +// 3. _cdylib_glue.h/.cpp — the UNIFORM Qt-plugin glue: a +// LogosProviderObject + LogosProviderPlugin that forwards everything +// to the cdylib's C ABI. Identical regardless of the module's source +// language — the Rust SDK's exports plug into the same glue. +// +// Supported types are the std-convertible LIDL subset (tstr/bstr/int/uint/ +// float64/bool + arrays thereof) plus LogosMap/LogosList and StdLogosResult +// returns. Types that map to Qt containers (map/any/named/optional, result +// parameters) are rejected at generation time — a cdylib impl is Qt-free by +// definition. +// --------------------------------------------------------------------------- + +// Returns false (with *error filled) when the module uses types outside the +// cdylib-supported subset. +bool lidlCdylibSupported(const ModuleDecl& module, QString* error); + +QString lidlMakeModuleImplExports(const ModuleDecl& module, + const QString& implClass, + const QString& implHeader); + +QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, + const QString& implClass, + const QString& implHeader); + + +#endif // LIDL_GEN_CDYLIB_H diff --git a/cpp-generator/experimental/lidl_gen_provider.cpp b/cpp-generator/experimental/lidl_gen_provider.cpp deleted file mode 100644 index 0d5e55d..0000000 --- a/cpp-generator/experimental/lidl_gen_provider.cpp +++ /dev/null @@ -1,732 +0,0 @@ -#include "lidl_gen_provider.h" -#include "lidl_emit_common.h" -#include "lidl_gen_client.h" // lidlToPascalCase, lidlTypeToQt -#include "lidl_parser.h" -#include "lidl_serializer.h" -#include "lidl_validator.h" - -#include -#include -#include -#include -#include -#include -#include - -// --------------------------------------------------------------------------- -// Type mapping: LIDL → C++ std types -// --------------------------------------------------------------------------- - - - -// --------------------------------------------------------------------------- -// Conversion helpers: Qt type ↔ std type -// --------------------------------------------------------------------------- - -static QString qtParamToStd(const TypeExpr& te, const QString& paramName) -{ - if (!lidlIsStdConvertible(te)) - return paramName; - - if (te.kind == TypeExpr::Primitive) { - if (te.name == "tstr") return paramName + ".toStdString()"; - if (te.name == "bstr") return "std::vector(" + paramName + ".begin(), " + paramName + ".end())"; - if (te.name == "int") return "static_cast(" + paramName + ")"; - if (te.name == "uint") return "static_cast(" + paramName + ")"; - return paramName; - } - if (te.kind == TypeExpr::Array && te.elements.size() == 1) { - const TypeExpr& elem = te.elements[0]; - if (elem.kind == TypeExpr::Primitive && elem.name == "tstr") - return "lidlToStdStringVector(" + paramName + ")"; - return "lidlToStdVector_" + elem.name + "(" + paramName + ")"; - } - return paramName; -} - -static QString stdReturnToQt(const TypeExpr& te, const QString& varName) -{ - if (!lidlIsStdConvertible(te)) - return varName; - - if (te.kind == TypeExpr::Primitive) { - if (te.name == "tstr") return "QString::fromStdString(" + varName + ")"; - if (te.name == "bstr") return "QByteArray(reinterpret_cast(" + varName + ".data()), static_cast(" + varName + ".size()))"; - if (te.name == "int") return "static_cast(" + varName + ")"; - if (te.name == "uint") return "static_cast(" + varName + ")"; - return varName; - } - if (te.kind == TypeExpr::Array && te.elements.size() == 1) { - const TypeExpr& elem = te.elements[0]; - if (elem.kind == TypeExpr::Primitive && elem.name == "tstr") - return "lidlToQStringList(" + varName + ")"; - return "lidlToQVariantList_" + elem.name + "(" + varName + ")"; - } - return varName; -} - -static QString variantToQtArg(const TypeExpr& te, int argIdx) -{ - QString a = "args.at(" + QString::number(argIdx) + ")"; - QString qt = lidlTypeToQt(te); - if (qt == "QString") return a + ".toString()"; - if (qt == "QByteArray") return a + ".toByteArray()"; - if (qt == "int") return a + ".toInt()"; - if (qt == "double") return a + ".toDouble()"; - if (qt == "bool") return a + ".toBool()"; - if (qt == "QStringList") return a + ".toStringList()"; - if (qt == "QVariantList") return a + ".toList()"; - if (qt == "QVariantMap") return a + ".toMap()"; - if (qt == "LogosResult") return a + ".value()"; - return a; -} - -// --------------------------------------------------------------------------- -// Provider header generation -// --------------------------------------------------------------------------- - -QString lidlMakeProviderHeader(const ModuleDecl& module, - const QString& implClass, - const QString& implHeader) -{ - QString className = lidlToPascalCase(module.name); - QString providerObjectClass = className + "ProviderObject"; - QString pluginClass = className + "Plugin"; - QString h; - QTextStream s(&h); - - s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n"; - s << "#pragma once\n\n"; - - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n\n"; - - s << "#include \"logos_provider_object.h\"\n"; - s << "#include \"interface.h\"\n"; - s << "#include \"logos_types.h\"\n"; - // The generated provider always emits an `onInit(LogosAPI*) override` - // that — when the impl class inherits from LogosModuleContext — - // copies the three runtime-injected properties (modulePath, - // instanceId, instancePersistencePath) into the context base AND - // builds a per-module `LogosModules` aggregate so impls can call - // other modules without ever touching the raw `LogosAPI`. Both - // dispatch paths sit behind SFINAE'd helpers in - // logos_module_context.h, so modules that don't inherit - // LogosModuleContext compile unchanged (the helpers resolve to - // no-op overloads). - s << "#include \"logos_api.h\"\n"; - s << "#include \"logos_module_context.h\"\n"; - // logos_sdk.h is generated alongside this header by the codegen's - // umbrella pass (lidl_gen_client.cpp / legacy/main.cpp) and defines - // the per-module `LogosModules` struct from the module's - // metadata.json#dependencies list. Always present in the build's - // generated_code/ directory; included unconditionally so the - // onInit override below can construct an instance. - s << "#include \"logos_sdk.h\"\n"; - s << "#include \n"; - s << "#include \n\n"; - - s << "#include \"" << implHeader << "\"\n\n"; - - // Conversion helpers (only if needed) - bool needsStringVecHelper = false; - for (const MethodDecl& md : module.methods) { - for (const ParamDecl& pd : md.params) { - if (pd.type.kind == TypeExpr::Array && pd.type.elements.size() == 1 - && pd.type.elements[0].kind == TypeExpr::Primitive - && pd.type.elements[0].name == "tstr") - needsStringVecHelper = true; - } - if (md.returnType.kind == TypeExpr::Array && md.returnType.elements.size() == 1 - && md.returnType.elements[0].kind == TypeExpr::Primitive - && md.returnType.elements[0].name == "tstr") - needsStringVecHelper = true; - } - - if (needsStringVecHelper) { - s << "namespace {\n"; - s << "inline QStringList lidlToQStringList(const std::vector& v) {\n"; - s << " QStringList result;\n"; - s << " result.reserve(static_cast(v.size()));\n"; - s << " for (const auto& s : v)\n"; - s << " result.append(QString::fromStdString(s));\n"; - s << " return result;\n"; - s << "}\n\n"; - s << "inline std::vector lidlToStdStringVector(const QStringList& list) {\n"; - s << " std::vector result;\n"; - s << " result.reserve(static_cast(list.size()));\n"; - s << " for (const auto& s : list)\n"; - s << " result.push_back(s.toStdString());\n"; - s << " return result;\n"; - s << "}\n"; - s << "} // anonymous namespace\n\n"; - } - - // Emit nlohmannToQVariant helper if any method returns LogosMap / LogosList / StdLogosResult - bool needsNlohmannHelper = false; - bool needsResultHelper = false; - for (const MethodDecl& md : module.methods) { - if (md.jsonReturn) needsNlohmannHelper = true; - if (md.resultReturn) { needsNlohmannHelper = true; needsResultHelper = true; } - } - if (needsNlohmannHelper) { - s << "#include \n\n"; - s << "namespace {\n"; - s << "inline QVariant nlohmannToQVariant(const nlohmann::json& j) {\n"; - s << " if (j.is_null()) return QVariant();\n"; - s << " if (j.is_boolean()) return QVariant(j.get());\n"; - s << " if (j.is_number_integer()) return QVariant(static_cast(j.get()));\n"; - s << " if (j.is_number_unsigned()) return QVariant(static_cast(j.get()));\n"; - s << " if (j.is_number_float()) return QVariant(j.get());\n"; - s << " if (j.is_string()) return QVariant(QString::fromStdString(j.get()));\n"; - s << " if (j.is_array()) {\n"; - s << " QVariantList list;\n"; - s << " list.reserve(static_cast(j.size()));\n"; - s << " for (const auto& elem : j)\n"; - s << " list.append(nlohmannToQVariant(elem));\n"; - s << " return QVariant(list);\n"; - s << " }\n"; - s << " if (j.is_object()) {\n"; - s << " QVariantMap map;\n"; - s << " for (auto it = j.begin(); it != j.end(); ++it)\n"; - s << " map.insert(QString::fromStdString(it.key()), nlohmannToQVariant(it.value()));\n"; - s << " return QVariant(map);\n"; - s << " }\n"; - s << " return QVariant();\n"; - s << "}\n"; - if (needsResultHelper) { - s << "\n"; - s << "#include \"logos_result.h\"\n"; - s << "inline LogosResult stdResultToQt(const StdLogosResult& r) {\n"; - s << " LogosResult qr;\n"; - s << " qr.success = r.success;\n"; - s << " qr.value = nlohmannToQVariant(r.value);\n"; - s << " qr.error = r.error.empty() ? QVariant() : QVariant(QString::fromStdString(r.error));\n"; - s << " return qr;\n"; - s << "}\n"; - } - s << "} // anonymous namespace\n\n"; - } - - // --- ProviderObject class --- - s << "class " << providerObjectClass << " : public LogosProviderBase {\n"; - s << " LOGOS_PROVIDER(" << providerObjectClass << ", \"" - << module.name << "\", \"" << (module.version.isEmpty() ? "0.0.0" : module.version) << "\")\n\n"; - s << "public:\n"; - - for (const MethodDecl& md : module.methods) { - QString qtRet = lidlTypeToQt(md.returnType); - bool retConvertible = lidlIsStdConvertible(md.returnType); - - s << " " << qtRet << " " << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - QString qt = lidlTypeToQt(md.params[i].type); - if (qt == "QString" || qt == "QByteArray" || qt == "QStringList" - || qt == "QVariantList" || qt == "QVariantMap" || qt == "LogosResult") - s << "const " << qt << "& " << md.params[i].name; - else - s << qt << " " << md.params[i].name; - if (i + 1 < md.params.size()) s << ", "; - } - s << ") {\n"; - - if (qtRet == "void") { - s << " m_impl." << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << qtParamToStd(md.params[i].type, md.params[i].name); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - } else if (md.jsonReturn) { - // LogosMap / LogosList: impl returns nlohmann::json, convert to Qt type - s << " auto _result = m_impl." << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << qtParamToStd(md.params[i].type, md.params[i].name); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - if (qtRet == "QVariantMap") - s << " return nlohmannToQVariant(_result).toMap();\n"; - else - s << " return nlohmannToQVariant(_result).toList();\n"; - } else if (md.resultReturn) { - // StdLogosResult: impl returns pure-C++ result, convert to Qt LogosResult - s << " auto _result = m_impl." << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << qtParamToStd(md.params[i].type, md.params[i].name); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - s << " return stdResultToQt(_result);\n"; - } else if (retConvertible) { - s << " auto _result = m_impl." << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << qtParamToStd(md.params[i].type, md.params[i].name); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - s << " return " << stdReturnToQt(md.returnType, "_result") << ";\n"; - } else { - s << " return m_impl." << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << qtParamToStd(md.params[i].type, md.params[i].name); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - } - - s << " }\n\n"; - } - - // Always emit an `onInit` override that: - // 1. copies the three runtime-injected LogosAPI properties - // (modulePath, instanceId, instancePersistencePath) into the - // impl when it opts in by inheriting LogosModuleContext; - // 2. builds a `LogosModules` aggregate (auto-generated by the - // codegen's umbrella pass — `generated_code/logos_sdk.h`) - // from the LogosAPI and threads its pointer into the same - // context base, giving the impl typed access to its - // declared dependencies via `modules()....`. - // - // Both wire-ups go through SFINAE'd helpers - // (`_logos_codegen_::maybeSet*`) so impls that don't inherit - // LogosModuleContext compile unchanged and pay zero runtime cost - // (the no-op overloads inline away). The full LogosAPI is never - // exposed to user code. - // - // `m_logosModules` is owned by the provider object, lifetime - // matched to it — the impl is a member of this same provider - // (`m_impl`), so the pointer the context holds stays valid for - // the impl's entire lifetime. - s << "protected:\n"; - s << " void onInit(LogosAPI* api) override {\n"; - s << " if (!api) return;\n"; - // Order matters: the modules() aggregate and the event wiring must be - // in place BEFORE maybeSetContext — setting the context fires the - // impl's onContextReady() hook, whose documented contract is "do your - // one-time setup here", which includes typed dependency calls - // (modules().dep.method()) and typed event subscriptions/emission. - s << " m_logosModules = std::make_unique(api);\n"; - s << " _logos_codegen_::maybeSetLogosModules(m_impl, m_logosModules.get());\n"; - // Wire the impl's `logos_events:` declarations to LogosProviderBase's - // emitEvent. Codegen-emitted `_events.cpp` bodies call - // `this->emitEventImpl_(name, &args)`; the lambda below casts the - // void* back to QVariantList and routes through the existing wire. - s << " _logos_codegen_::maybeSetEmitEvent(m_impl,\n"; - s << " [this](const std::string& name, void* args) {\n"; - s << " emitEvent(QString::fromStdString(name),\n"; - s << " *static_cast(args));\n"; - s << " });\n"; - // Context LAST: _logosCoreSetContext_ fires onContextReady() — by then - // the impl must be fully wired (see ordering note above). - s << " _logos_codegen_::maybeSetContext(m_impl,\n"; - s << " api->property(\"modulePath\").toString().toStdString(),\n"; - s << " api->property(\"instanceId\").toString().toStdString(),\n"; - s << " api->property(\"instancePersistencePath\").toString().toStdString());\n"; - s << " }\n\n"; - - if (!module.events.isEmpty()) { - s << "protected:\n"; - for (const EventDecl& ed : module.events) { - QString methodName = "emit" + lidlToPascalCase(ed.name); - s << " void " << methodName << "("; - for (int i = 0; i < ed.params.size(); ++i) { - QString qt = lidlTypeToQt(ed.params[i].type); - if (qt == "QString" || qt == "QByteArray" || qt == "QStringList" - || qt == "QVariantList" || qt == "QVariantMap" || qt == "LogosResult") - s << "const " << qt << "& " << ed.params[i].name; - else - s << qt << " " << ed.params[i].name; - if (i + 1 < ed.params.size()) s << ", "; - } - s << ") {\n"; - s << " emitEvent(\"" << ed.name << "\", QVariantList{"; - for (int i = 0; i < ed.params.size(); ++i) { - s << "QVariant::fromValue(" << ed.params[i].name << ")"; - if (i + 1 < ed.params.size()) s << ", "; - } - s << "});\n"; - s << " }\n\n"; - } - } - - s << "private:\n"; - // Built by onInit; lives for the provider's full lifetime. The impl - // (declared next, so destroyed first — reverse-of-construction - // order) holds only a non-owning pointer to it via the - // LogosModuleContext base. Using unique_ptr instead of a direct - // member so the build doesn't require LogosModules to be - // default-constructible (it isn't — it takes a LogosAPI*). - s << " std::unique_ptr m_logosModules;\n"; - s << " " << implClass << " m_impl;\n"; - s << "};\n\n"; - - // --- Plugin/Loader class --- - s << "class " << pluginClass << " : public QObject, public PluginInterface, public LogosProviderPlugin {\n"; - s << " Q_OBJECT\n"; - s << " Q_PLUGIN_METADATA(IID LogosProviderPlugin_iid FILE \"metadata.json\")\n"; - s << " Q_INTERFACES(PluginInterface LogosProviderPlugin)\n\n"; - s << "public:\n"; - s << " QString name() const override { return QStringLiteral(\"" << module.name << "\"); }\n"; - s << " QString version() const override { return QStringLiteral(\"" - << (module.version.isEmpty() ? "0.0.0" : module.version) << "\"); }\n"; - s << " LogosProviderObject* createProviderObject() override {\n"; - s << " return new " << providerObjectClass << "();\n"; - s << " }\n"; - s << "};\n"; - - return h; -} - -// --------------------------------------------------------------------------- -// Dispatch source generation -// --------------------------------------------------------------------------- - -QString lidlMakeProviderDispatch(const ModuleDecl& module) -{ - QString className = lidlToPascalCase(module.name); - QString providerObjectClass = className + "ProviderObject"; - QString c; - QTextStream s(&c); - - s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n"; - s << "#include \"" << module.name << "_qt_glue.h\"\n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \"logos_types.h\"\n"; - s << "#include \n\n"; - - // --- callMethod --- - // The dispatch body is wrapped in a catch-all: any exception the author's - // code lets escape becomes an ordinary method failure (invalid QVariant) - // instead of unwinding through Qt event dispatch and killing the module - // process. - s << "QVariant " << providerObjectClass - << "::callMethod(const QString& methodName, const QVariantList& args)\n{\n"; - s << " try {\n"; - - for (const MethodDecl& md : module.methods) { - QString qtRet = lidlTypeToQt(md.returnType); - s << " if (methodName == \"" << md.name << "\") {\n"; - - if (qtRet == "void") { - s << " " << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << variantToQtArg(md.params[i].type, i); - if (i + 1 < md.params.size()) s << ", "; - } - s << ");\n"; - s << " return QVariant(true);\n"; - } else { - s << " return QVariant::fromValue(" << md.name << "("; - for (int i = 0; i < md.params.size(); ++i) { - s << variantToQtArg(md.params[i].type, i); - if (i + 1 < md.params.size()) s << ", "; - } - s << "));\n"; - } - s << " }\n"; - } - - s << " } catch (const std::exception& e) {\n"; - s << " qWarning() << \"" << providerObjectClass - << "::callMethod:\" << methodName << \"failed:\" << e.what();\n"; - s << " return QVariant();\n"; - s << " }\n"; - s << " qWarning() << \"" << providerObjectClass - << "::callMethod: unknown method:\" << methodName;\n"; - s << " return QVariant();\n"; - s << "}\n\n"; - - // --- getMethods --- - s << "QJsonArray " << providerObjectClass << "::getMethods()\n{\n"; - s << " QJsonArray methods;\n"; - - for (const MethodDecl& md : module.methods) { - QString qtRet = lidlTypeToQt(md.returnType); - s << " {\n"; - s << " QJsonObject obj;\n"; - s << " obj[\"type\"] = QStringLiteral(\"method\");\n"; - s << " obj[\"name\"] = QStringLiteral(\"" << md.name << "\");\n"; - s << " obj[\"returnType\"] = QStringLiteral(\"" << qtRet << "\");\n"; - s << " obj[\"isInvokable\"] = true;\n"; - if (!md.description.isEmpty()) { - QString escDesc = md.description; - escDesc.replace('\\', "\\\\"); - escDesc.replace('"', "\\\""); - escDesc.replace('\n', "\\n"); - s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n"; - } - - QString sig = md.name + "("; - for (int i = 0; i < md.params.size(); ++i) { - sig += lidlTypeToQt(md.params[i].type); - if (i + 1 < md.params.size()) sig += ","; - } - sig += ")"; - s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n"; - - if (!md.params.isEmpty()) { - s << " QJsonArray params;\n"; - for (int i = 0; i < md.params.size(); ++i) { - s << " params.append(QJsonObject{{\"type\", QStringLiteral(\"" - << lidlTypeToQt(md.params[i].type) << "\")}, {\"name\", QStringLiteral(\"" - << md.params[i].name << "\")}});\n"; - } - s << " obj[\"parameters\"] = params;\n"; - } - - s << " methods.append(obj);\n"; - s << " }\n"; - } - - // Events are appended to the SAME interface list, tagged type "event" (and - // with no returnType/isInvokable — they are void/fire-and-forget). Folding - // them into getMethods() instead of adding a getEvents() vtable slot keeps - // LogosProviderObject's vtable layout stable, so old/new hosts and modules - // stay binary-compatible. Callers split the list back out by "type" (see - // ModuleProxy::getPluginMethods/getPluginEvents/getPluginInterface). - for (const EventDecl& ed : module.events) { - s << " {\n"; - s << " QJsonObject obj;\n"; - s << " obj[\"type\"] = QStringLiteral(\"event\");\n"; - s << " obj[\"name\"] = QStringLiteral(\"" << ed.name << "\");\n"; - if (!ed.description.isEmpty()) { - QString escDesc = ed.description; - escDesc.replace('\\', "\\\\"); - escDesc.replace('"', "\\\""); - escDesc.replace('\n', "\\n"); - s << " obj[\"description\"] = QStringLiteral(\"" << escDesc << "\");\n"; - } - - QString sig = ed.name + "("; - for (int i = 0; i < ed.params.size(); ++i) { - sig += lidlTypeToQt(ed.params[i].type); - if (i + 1 < ed.params.size()) sig += ","; - } - sig += ")"; - s << " obj[\"signature\"] = QStringLiteral(\"" << sig << "\");\n"; - - if (!ed.params.isEmpty()) { - s << " QJsonArray params;\n"; - for (int i = 0; i < ed.params.size(); ++i) { - s << " params.append(QJsonObject{{\"type\", QStringLiteral(\"" - << lidlTypeToQt(ed.params[i].type) << "\")}, {\"name\", QStringLiteral(\"" - << ed.params[i].name << "\")}});\n"; - } - s << " obj[\"parameters\"] = params;\n"; - } - - s << " methods.append(obj);\n"; - s << " }\n"; - } - - s << " return methods;\n"; - s << "}\n"; - - return c; -} - -// --------------------------------------------------------------------------- -// Events source generation — Qt-MOC-style bodies for `logos_events:` decls -// --------------------------------------------------------------------------- -// -// The impl header declares typed event prototypes in a `logos_events:` -// section. The compiler sees them as ordinary public-method declarations -// (the macro expands to `public:`). This generator emits the matching -// definitions in a sidecar `_events.cpp` — exactly the role that -// `moc_*.cpp` plays for Qt's `signals:`. Each body marshals typed args -// into a `QVariantList` and calls `LogosModuleContext::emitEventImpl_`, -// which the provider's onInit wired to LogosProviderBase::emitEvent (and -// onward over QRO). - -// Returns a C++ expression of static type QVariant for the given -// std-typed parameter. Mirrors the type-mapping table in lidlTypeToStd. -static QString stdParamToQVariantExpr(const TypeExpr& te, const QString& pn) -{ - if (te.kind == TypeExpr::Primitive) { - if (te.name == "tstr") - return "QVariant(QString::fromStdString(" + pn + "))"; - if (te.name == "bstr") - return "QVariant(QByteArray(reinterpret_cast(" + pn - + ".data()), static_cast(" + pn + ".size())))"; - if (te.name == "int") - return "QVariant(static_cast(" + pn + "))"; - if (te.name == "uint") - return "QVariant(static_cast(" + pn + "))"; - if (te.name == "float64") return "QVariant(" + pn + ")"; - if (te.name == "bool") return "QVariant(" + pn + ")"; - } - if (te.kind == TypeExpr::Array && te.elements.size() == 1 - && te.elements[0].kind == TypeExpr::Primitive - && te.elements[0].name == "tstr") { - // std::vector -> QStringList wrapped in QVariant - return "QVariant([&](){ QStringList _l; _l.reserve(static_cast(" + pn - + ".size())); for (const auto& _e : " + pn - + ") _l.append(QString::fromStdString(_e)); return _l; }())"; - } - // Fallback — let QVariant::fromValue figure it out (works for primitives - // and any QMetaType-registered user type). - return "QVariant::fromValue(" + pn + ")"; -} - -QString lidlMakeEventsSource(const ModuleDecl& module, - const QString& implClass, - const QString& implHeader) -{ - QString c; - QTextStream s(&c); - s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n"; - s << "//\n"; - s << "// Bodies for `logos_events:` methods declared in " << implHeader << ".\n"; - s << "// Each call marshals typed args into a QVariantList and routes\n"; - s << "// them through LogosModuleContext::emitEventImpl_, which the\n"; - s << "// generated provider wires to LogosProviderBase::emitEvent.\n"; - s << "#include \"" << implHeader << "\"\n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n\n"; - - for (const EventDecl& ed : module.events) { - // Signature — mirrors the prototype the impl declared. - s << "void " << implClass << "::" << ed.name << "("; - for (int i = 0; i < ed.params.size(); ++i) { - QString stdType = lidlTypeToStd(ed.params[i].type); - const TypeExpr& te = ed.params[i].type; - // Pass-by-const-ref for non-trivial std types; by-value for - // primitives. Mirrors the existing method-signature shape. - bool byRef = (te.kind == TypeExpr::Array) - || (te.kind == TypeExpr::Primitive - && (te.name == "tstr" || te.name == "bstr")); - if (byRef) s << "const " << stdType << "& " << ed.params[i].name; - else s << stdType << " " << ed.params[i].name; - if (i + 1 < ed.params.size()) s << ", "; - } - s << ") {\n"; - s << " QVariantList _args{"; - for (int i = 0; i < ed.params.size(); ++i) { - s << stdParamToQVariantExpr(ed.params[i].type, ed.params[i].name); - if (i + 1 < ed.params.size()) s << ", "; - } - s << "};\n"; - s << " this->emitEventImpl_(\"" << ed.name << "\", &_args);\n"; - s << "}\n\n"; - } - - return c; -} - -// --------------------------------------------------------------------------- -// Full pipeline (from .lidl file) -// --------------------------------------------------------------------------- - -int lidlGenerateProviderGlue(const QString& lidlPath, - const QString& implClass, - const QString& implHeader, - const QString& outputDir, - QTextStream& out, QTextStream& err) -{ - QFileInfo fi(lidlPath); - if (!fi.exists()) { - err << "LIDL file does not exist: " << lidlPath << "\n"; - return 2; - } - QFile file(fi.canonicalFilePath().isEmpty() ? fi.absoluteFilePath() : fi.canonicalFilePath()); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - err << "Failed to open LIDL file: " << lidlPath << "\n"; - return 3; - } - QString source = QString::fromUtf8(file.readAll()); - file.close(); - - LidlParseResult pr = lidlParse(source); - if (pr.hasError()) { - err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn - << ": " << pr.error << "\n"; - return 4; - } - - LidlValidationResult vr = lidlValidate(pr.module); - if (vr.hasErrors()) { - for (const QString& e : vr.errors) - err << lidlPath << ": " << e << "\n"; - return 5; - } - - const ModuleDecl& mod = pr.module; - QString genDirPath = outputDir.isEmpty() - ? QDir::current().filePath("generated") - : outputDir; - QDir().mkpath(genDirPath); - - QString glueHeaderAbs = QDir(genDirPath).filePath(mod.name + "_qt_glue.h"); - { - QFile f(glueHeaderAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write glue header: " << glueHeaderAbs << "\n"; - return 6; - } - f.write(lidlMakeProviderHeader(mod, implClass, implHeader).toUtf8()); - } - - QString dispatchAbs = QDir(genDirPath).filePath(mod.name + "_dispatch.cpp"); - { - QFile f(dispatchAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write dispatch source: " << dispatchAbs << "\n"; - return 7; - } - f.write(lidlMakeProviderDispatch(mod).toUtf8()); - } - - out << "Generated: " << glueHeaderAbs << "\n"; - out << "Generated: " << dispatchAbs << "\n"; - - // Events bodies + LIDL sidecar: emitted whenever the module declares - // any events. The sidecar gets shipped in the dep's headers-* output - // by buildPlugin.nix's installPhase so consumer-side codegen can - // discover events without reintrospecting the .dylib. - if (!mod.events.isEmpty()) { - QString eventsAbs = QDir(genDirPath).filePath(mod.name + "_events.cpp"); - { - QFile f(eventsAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write events source: " << eventsAbs << "\n"; - return 8; - } - f.write(lidlMakeEventsSource(mod, implClass, implHeader).toUtf8()); - } - out << "Generated: " << eventsAbs << "\n"; - - QString lidlAbs = QDir(genDirPath).filePath(mod.name + ".lidl"); - { - QFile f(lidlAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write LIDL sidecar: " << lidlAbs << "\n"; - return 9; - } - f.write(lidlSerialize(mod).toUtf8()); - } - out << "Generated: " << lidlAbs << "\n"; - } - - out.flush(); - return 0; -} diff --git a/cpp-generator/experimental/lidl_gen_provider.h b/cpp-generator/experimental/lidl_gen_provider.h deleted file mode 100644 index 598dd3b..0000000 --- a/cpp-generator/experimental/lidl_gen_provider.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef LIDL_GEN_PROVIDER_H -#define LIDL_GEN_PROVIDER_H - -#include "lidl_ast.h" -#include -#include - -// Map a LIDL TypeExpr to the C++ std type string used in the pure implementation class -QString lidlTypeToStd(const TypeExpr& te); - -// True if this type can be represented as a pure C++ std type (no Qt) -bool lidlIsStdConvertible(const TypeExpr& te); - -// Generate the Qt glue header (plugin class + provider object with Q_INVOKABLE wrappers) -QString lidlMakeProviderHeader(const ModuleDecl& module, - const QString& implClass, - const QString& implHeader); - -// Generate callMethod() + getMethods() dispatch source -QString lidlMakeProviderDispatch(const ModuleDecl& module); - -// Generate the `_events.cpp` source: Qt-MOC-style definitions of -// methods declared in the impl's `logos_events:` section. Each body -// marshals typed args into a QVariantList and calls -// LogosModuleContext::emitEventImpl_, which the provider's onInit wires -// to the QRO wire via LogosProviderBase::emitEvent. -QString lidlMakeEventsSource(const ModuleDecl& module, - const QString& implClass, - const QString& implHeader); - -// Full pipeline: parse .lidl, generate provider glue + dispatch + metadata -// Returns 0 on success, non-zero on error -int lidlGenerateProviderGlue(const QString& lidlPath, - const QString& implClass, - const QString& implHeader, - const QString& outputDir, - QTextStream& out, QTextStream& err); - -#endif // LIDL_GEN_PROVIDER_H diff --git a/cpp-generator/main.cpp b/cpp-generator/main.cpp index 8095ee5..427ac32 100644 --- a/cpp-generator/main.cpp +++ b/cpp-generator/main.cpp @@ -1,6 +1,7 @@ #include "legacy/legacy_main.h" #include "experimental/lidl_gen_client.h" -#include "experimental/lidl_gen_provider.h" +#include "experimental/lidl_gen_cdylib.h" +#include "experimental/lidl_parser.h" #include "experimental/lidl_serializer.h" #include "experimental/impl_header_parser.h" @@ -161,66 +162,46 @@ int main(int argc, char* argv[]) : outputDir; QDir().mkpath(genDirPath); - if (backend == "qt") { - // Generate provider header (Qt glue) - QString glueHeaderAbs = QDir(genDirPath).filePath(mod.name + "_qt_glue.h"); - { - QFile f(glueHeaderAbs); + if (backend == "cdylib") { + // Cdylib authoring: the common module-impl C ABI exports + + // the uniform Qt-plugin glue (language-agnostic, forwards to + // the C symbols). See logos_module_impl.h in logos-protocol. + QString cdErr; + if (!lidlCdylibSupported(mod, &cdErr)) { + err << "Error: module not cdylib-eligible: " << cdErr << "\n"; + return 10; + } + struct Out { QString file; QString content; }; + QList outs; + outs.append({mod.name + "_module_impl.cpp", + lidlMakeModuleImplExports(mod, implClass, implHeader)}); + if (!mod.events.isEmpty()) + outs.append({mod.name + "_events_cdylib.cpp", + lidlMakeEventsSourceCdylib(mod, implClass, implHeader)}); + outs.append({mod.name + ".lidl", lidlSerialize(mod)}); + for (const Out& o : outs) { + const QString abs = QDir(genDirPath).filePath(o.file); + QFile f(abs); if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write glue header: " << glueHeaderAbs << "\n"; - return 6; + err << "Failed to write: " << abs << "\n"; + return 11; } - f.write(lidlMakeProviderHeader(mod, implClass, implHeader).toUtf8()); + f.write(o.content.toUtf8()); + out << "Generated: " << abs << "\n"; } - - // Generate dispatch source - QString dispatchAbs = QDir(genDirPath).filePath(mod.name + "_dispatch.cpp"); - { - QFile f(dispatchAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write dispatch source: " << dispatchAbs << "\n"; - return 7; - } - f.write(lidlMakeProviderDispatch(mod).toUtf8()); - } - - out << "Generated: " << glueHeaderAbs << "\n"; - out << "Generated: " << dispatchAbs << "\n"; - - // Events bodies (Qt-MOC-style) and LIDL sidecar — emitted - // when the impl declares any `logos_events:` prototypes. - // The sidecar travels in the dep's headers-* output so - // consumer-side codegen can generate typed `on()` - // accessors without reintrospecting the .dylib. - if (!mod.events.isEmpty()) { - QString eventsAbs = QDir(genDirPath).filePath(mod.name + "_events.cpp"); - { - QFile f(eventsAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write events source: " << eventsAbs << "\n"; - return 8; - } - f.write(lidlMakeEventsSource(mod, implClass, implHeader).toUtf8()); - } - out << "Generated: " << eventsAbs << "\n"; - - QString lidlAbs = QDir(genDirPath).filePath(mod.name + ".lidl"); - { - QFile f(lidlAbs); - if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write LIDL sidecar: " << lidlAbs << "\n"; - return 9; - } - f.write(lidlSerialize(mod).toUtf8()); - } - out << "Generated: " << lidlAbs << "\n"; - } - out.flush(); return 0; } - err << "Error: --from-header currently only supports --backend qt\n"; + if (backend == "qt") { + err << "Error: Qt glue generation moved to logos-qt-generator " + "(logos-qt-sdk). Use it for --backend qt; this tool " + "keeps the Qt-free outputs (--header-to-lidl emits the " + ".lidl sidecar).\n"; + return 6; + } + + err << "Error: --from-header supports --backend cdylib (Qt glue: logos-qt-generator)\n"; return 1; } @@ -232,6 +213,8 @@ int main(int argc, char* argv[]) << " " << QFileInfo(app.applicationFilePath()).fileName() << " --lidl /path/to/module.lidl --backend qt --impl-class Class --impl-header header.h [--output-dir /path]\n" << " " << QFileInfo(app.applicationFilePath()).fileName() + << " --lidl /path/to/module.lidl --backend cdylib [--output-dir /path] (glue-only: C exports come from the module's own language backend)\n" + << " " << QFileInfo(app.applicationFilePath()).fileName() << " --from-header src/impl.h --backend qt --impl-class Class --metadata metadata.json [--output-dir /path]\n"; return 1; } @@ -248,6 +231,76 @@ int main(int argc, char* argv[]) const int implClassIdx = args.indexOf("--impl-class"); const int implHeaderIdx = args.indexOf("--impl-header"); + + // Cdylib-from-LIDL (contract-first): + // - with no --impl-class: GLUE-ONLY — the C exports come from + // the module's own language backend (e.g. the Rust SDK's + // lidl-gen --provider); the glue only knows the C symbols. + // - with --impl-class/--impl-header: the FULL set — the C-ABI + // export wrapper around the named (hand-written, Qt-free) + // C++ impl class, plus the same uniform glue. The contract + // stays the .lidl; the author just implements the class. + if (backend == "cdylib") { + QFile f(lidlPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + err << "Error: cannot read " << lidlPath << "\n"; + return 1; + } + LidlParseResult pr = lidlParse(QString::fromUtf8(f.readAll())); + if (pr.hasError()) { + err << "Error parsing " << lidlPath << ": " << pr.error + << " (line " << pr.errorLine << ")\n"; + return 4; + } + const ModuleDecl& mod = pr.module; + QString cdErr; + if (!lidlCdylibSupported(mod, &cdErr)) { + err << "Error: module not cdylib-eligible: " << cdErr << "\n"; + return 10; + } + QString genDirPath = outputDir.isEmpty() + ? QDir::current().filePath("generated") + : outputDir; + QDir().mkpath(genDirPath); + struct Out { QString file; QString content; }; + QList outs; + if (implClassIdx != -1) { + if (implClassIdx + 1 >= args.size()) { + err << "Error: --impl-class requires a class name\n"; + return 1; + } + const QString implClass = args.at(implClassIdx + 1); + QString implHeader; + if (implHeaderIdx != -1 && implHeaderIdx + 1 < args.size()) + implHeader = args.at(implHeaderIdx + 1); + else + implHeader = mod.name + "_impl.h"; + outs.append({mod.name + "_module_impl.cpp", + lidlMakeModuleImplExports(mod, implClass, implHeader)}); + if (!mod.events.isEmpty()) + outs.append({mod.name + "_events_cdylib.cpp", + lidlMakeEventsSourceCdylib(mod, implClass, implHeader)}); + } else { + err << "Error: the uniform cdylib Qt glue is generated by " + "logos-qt-generator (logos-qt-sdk); this tool emits " + "the Qt-free C-ABI export wrapper, which requires " + "--impl-class.\n"; + return 12; + } + for (const Out& o : outs) { + const QString abs = QDir(genDirPath).filePath(o.file); + QFile of(abs); + if (!of.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + err << "Failed to write: " << abs << "\n"; + return 11; + } + of.write(o.content.toUtf8()); + out << "Generated: " << abs << "\n"; + } + out.flush(); + return 0; + } + if (implClassIdx == -1 || implClassIdx + 1 >= args.size()) { err << "Error: --backend " << backend << " requires --impl-class \n"; return 1; @@ -260,10 +313,13 @@ int main(int argc, char* argv[]) QString implClass = args.at(implClassIdx + 1); QString implHeader = args.at(implHeaderIdx + 1); - if (backend == "qt") - return lidlGenerateProviderGlue(lidlPath, implClass, implHeader, outputDir, out, err); + if (backend == "qt") { + err << "Error: Qt glue generation moved to logos-qt-generator " + "(logos-qt-sdk).\n"; + return 6; + } - err << "Error: unsupported backend '" << backend << "' (supported: qt)\n"; + err << "Error: unsupported backend '" << backend << "' (supported: cdylib)\n"; return 1; } diff --git a/tests/experimental/CMakeLists.txt b/tests/experimental/CMakeLists.txt index aabe1b0..181a948 100644 --- a/tests/experimental/CMakeLists.txt +++ b/tests/experimental/CMakeLists.txt @@ -11,7 +11,6 @@ add_executable(experimental_tests test_lidl_type_mapping.cpp # Code generation tests - test_lidl_gen_provider.cpp test_lidl_gen_client.cpp # Impl header parser tests @@ -24,7 +23,6 @@ add_executable(experimental_tests ${EXPERIMENTAL_SRC_DIR}/lidl_validator.cpp ${EXPERIMENTAL_SRC_DIR}/lidl_serializer.cpp ${EXPERIMENTAL_SRC_DIR}/lidl_gen_client.cpp - ${EXPERIMENTAL_SRC_DIR}/lidl_gen_provider.cpp ${EXPERIMENTAL_SRC_DIR}/impl_header_parser.cpp ) diff --git a/tests/experimental/test_lidl_gen_provider.cpp b/tests/experimental/test_lidl_gen_provider.cpp deleted file mode 100644 index 3bd1d26..0000000 --- a/tests/experimental/test_lidl_gen_provider.cpp +++ /dev/null @@ -1,462 +0,0 @@ -#include -#include "lidl_gen_provider.h" -#include "lidl_gen_client.h" - -static ModuleDecl makeTestModule() -{ - ModuleDecl m; - m.name = "test_module"; - m.version = "1.0.0"; - m.description = "Test module"; - - // Method: std::string greet(const std::string& name) → tstr - { - MethodDecl md; - md.name = "greet"; - md.returnType = { TypeExpr::Primitive, "tstr", {} }; - ParamDecl p; p.name = "name"; p.type = { TypeExpr::Primitive, "tstr", {} }; - md.params.append(p); - m.methods.append(md); - } - // Method: bool isValid() → bool - { - MethodDecl md; - md.name = "isValid"; - md.returnType = { TypeExpr::Primitive, "bool", {} }; - m.methods.append(md); - } - // Method: int64_t getCount() → int - { - MethodDecl md; - md.name = "getCount"; - md.returnType = { TypeExpr::Primitive, "int", {} }; - m.methods.append(md); - } - // Method: std::vector getNames() → [tstr] - { - MethodDecl md; - md.name = "getNames"; - TypeExpr elem = { TypeExpr::Primitive, "tstr", {} }; - md.returnType = { TypeExpr::Array, "", { elem } }; - m.methods.append(md); - } - // Method: void doNothing() → void - { - MethodDecl md; - md.name = "doNothing"; - md.returnType = { TypeExpr::Primitive, "void", {} }; - m.methods.append(md); - } - return m; -} - -// --------------------------------------------------------------------------- -// Provider header generation -// --------------------------------------------------------------------------- - -TEST(LidlGenProvider, HeaderContainsClassName) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("TestModuleProviderObject")); - EXPECT_TRUE(h.contains("TestModulePlugin")); -} - -TEST(LidlGenProvider, HeaderIncludesImplHeader) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("#include \"test_module_impl.h\"")); -} - -TEST(LidlGenProvider, HeaderIncludesFrameworkHeaders) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("#include \"logos_provider_object.h\"")); - EXPECT_TRUE(h.contains("#include \"interface.h\"")); - EXPECT_TRUE(h.contains("#include \"logos_types.h\"")); -} - -TEST(LidlGenProvider, HeaderHasPluginMetadata) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("Q_PLUGIN_METADATA")); - EXPECT_TRUE(h.contains("Q_INTERFACES(PluginInterface LogosProviderPlugin)")); - EXPECT_TRUE(h.contains("Q_OBJECT")); -} - -TEST(LidlGenProvider, HeaderHasLogosMacro) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("LOGOS_PROVIDER(TestModuleProviderObject, \"test_module\", \"1.0.0\")")); -} - -TEST(LidlGenProvider, HeaderHasWrapperMethods) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("QString greet(")); - EXPECT_TRUE(h.contains("bool isValid(")); - EXPECT_TRUE(h.contains("int getCount(")); - EXPECT_TRUE(h.contains("QStringList getNames(")); - EXPECT_TRUE(h.contains("void doNothing(")); -} - -TEST(LidlGenProvider, HeaderConvertsStringParams) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - // greet should convert QString→std::string and back - EXPECT_TRUE(h.contains(".toStdString()")); - EXPECT_TRUE(h.contains("QString::fromStdString(")); -} - -TEST(LidlGenProvider, HeaderHasStringVecHelpers) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - // getNames returns [tstr], so helpers should be generated - EXPECT_TRUE(h.contains("lidlToQStringList")); - EXPECT_TRUE(h.contains("lidlToStdStringVector")); -} - -TEST(LidlGenProvider, HeaderHasImplMember) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("TestModuleImpl m_impl")); -} - -TEST(LidlGenProvider, HeaderHasCreateProviderObject) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("createProviderObject()")); - EXPECT_TRUE(h.contains("new TestModuleProviderObject()")); -} - -TEST(LidlGenProvider, HeaderPluginReturnsCorrectName) -{ - auto m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_TRUE(h.contains("return QStringLiteral(\"test_module\")")); - EXPECT_TRUE(h.contains("return QStringLiteral(\"1.0.0\")")); -} - -// --------------------------------------------------------------------------- -// No string vec helpers when not needed -// --------------------------------------------------------------------------- - -TEST(LidlGenProvider, NoStringVecHelpersWhenNotNeeded) -{ - ModuleDecl m; - m.name = "simple"; - m.version = "1.0.0"; - MethodDecl md; - md.name = "getFlag"; - md.returnType = { TypeExpr::Primitive, "bool", {} }; - m.methods.append(md); - - QString h = lidlMakeProviderHeader(m, "SimpleImpl", "simple_impl.h"); - EXPECT_FALSE(h.contains("lidlToQStringList")); -} - -// --------------------------------------------------------------------------- -// Dispatch generation -// --------------------------------------------------------------------------- - -TEST(LidlGenProvider, DispatchContainsCallMethod) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("::callMethod(const QString& methodName, const QVariantList& args)")); -} - -TEST(LidlGenProvider, DispatchHasMethodChecks) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("if (methodName == \"greet\")")); - EXPECT_TRUE(d.contains("if (methodName == \"isValid\")")); - EXPECT_TRUE(d.contains("if (methodName == \"getCount\")")); - EXPECT_TRUE(d.contains("if (methodName == \"getNames\")")); - EXPECT_TRUE(d.contains("if (methodName == \"doNothing\")")); -} - -TEST(LidlGenProvider, DispatchVoidReturnsTrueVariant) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - // doNothing is void, dispatch should return QVariant(true) - EXPECT_TRUE(d.contains("return QVariant(true)")); -} - -TEST(LidlGenProvider, DispatchHasUnknownMethodWarning) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("unknown method")); -} - -TEST(LidlGenProvider, DispatchContainsGetMethods) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("::getMethods()")); - EXPECT_TRUE(d.contains("QJsonArray methods")); -} - -TEST(LidlGenProvider, DispatchGetMethodsHasAllMethods) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("\"greet\"")); - EXPECT_TRUE(d.contains("\"isValid\"")); - EXPECT_TRUE(d.contains("\"getCount\"")); - EXPECT_TRUE(d.contains("\"getNames\"")); - EXPECT_TRUE(d.contains("\"doNothing\"")); -} - -TEST(LidlGenProvider, DispatchGetMethodsHasSignature) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("\"signature\"")); - EXPECT_TRUE(d.contains("greet(QString)")); -} - -TEST(LidlGenProvider, DispatchGetMethodsHasReturnType) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("\"returnType\"")); -} - -TEST(LidlGenProvider, DispatchGetMethodsHasParameters) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("\"parameters\"")); -} - -TEST(LidlGenProvider, DispatchIncludesGlueHeader) -{ - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("#include \"test_module_qt_glue.h\"")); -} - -// --------------------------------------------------------------------------- -// Events in provider header -// --------------------------------------------------------------------------- - -TEST(LidlGenProvider, EventEmitters) -{ - ModuleDecl m; - m.name = "evented"; - m.version = "1.0.0"; - EventDecl ed; - ed.name = "onUpdate"; - ParamDecl p; p.name = "data"; p.type = { TypeExpr::Primitive, "tstr", {} }; - ed.params.append(p); - m.events.append(ed); - - QString h = lidlMakeProviderHeader(m, "EventedImpl", "evented_impl.h"); - // The provider class still gains a protected `emit(...)` - // helper per declared event (callable from custom provider code, - // unused by the standard impl flow which goes via the typed - // `logos_events:` path). - EXPECT_TRUE(h.contains("emitOnupdate")); - EXPECT_TRUE(h.contains("emitEvent(\"onUpdate\"")); - // Events declared in ModuleDecl.events flow through the - // `_logos_codegen_::maybeSetEmitEvent` SFINAE helper invoked from - // the generated `onInit`. There is no constructor-based wiring — the - // old `m_impl.emitEvent = …` path was removed along with the legacy - // `std::function emitEvent` member it supported. - EXPECT_TRUE(h.contains("_logos_codegen_::maybeSetEmitEvent(m_impl")); - EXPECT_TRUE(h.contains("emitEvent(QString::fromStdString(name)")); - EXPECT_FALSE(h.contains("m_impl.emitEvent =")); -} - -TEST(LidlGenProvider, HeaderIncludesNlohmannConversionForJsonMapReturn) -{ - ModuleDecl m; - m.name = "jm"; - m.version = "1.0.0"; - MethodDecl md; - md.name = "getPayload"; - md.returnType = { TypeExpr::Map, "", { { TypeExpr::Primitive, "tstr", {} }, - { TypeExpr::Primitive, "any", {} } } }; - md.jsonReturn = true; - m.methods.append(md); - - QString h = lidlMakeProviderHeader(m, "JmImpl", "jm_impl.h"); - EXPECT_TRUE(h.contains("#include ")); - EXPECT_TRUE(h.contains("nlohmannToQVariant")); - EXPECT_TRUE(h.contains("auto _result = m_impl.getPayload(")); - EXPECT_TRUE(h.contains("return nlohmannToQVariant(_result).toMap()")); -} - -TEST(LidlGenProvider, HeaderUsesToListForJsonListReturn) -{ - ModuleDecl m; - m.name = "jl"; - m.version = "1.0.0"; - MethodDecl md; - md.name = "getItems"; - md.returnType = { TypeExpr::Array, "", { { TypeExpr::Primitive, "any", {} } } }; - md.jsonReturn = true; - m.methods.append(md); - - QString h = lidlMakeProviderHeader(m, "JlImpl", "jl_impl.h"); - EXPECT_TRUE(h.contains("return nlohmannToQVariant(_result).toList()")); -} - -TEST(LidlGenProvider, NoNlohmannBlockWithoutJsonReturnMethods) -{ - ModuleDecl m = makeTestModule(); - QString h = lidlMakeProviderHeader(m, "TestModuleImpl", "test_module_impl.h"); - EXPECT_FALSE(h.contains("nlohmann/json.hpp")); - EXPECT_FALSE(h.contains("nlohmannToQVariant")); -} - -TEST(LidlGenProvider, HeaderIncludesNlohmannAndStdResultForResultReturn) -{ - ModuleDecl m; - m.name = "resmod"; - m.version = "1.0.0"; - MethodDecl md; - md.name = "getResult"; - md.returnType = { TypeExpr::Primitive, "result", {} }; - md.resultReturn = true; - m.methods.append(md); - - QString h = lidlMakeProviderHeader(m, "ResModImpl", "resmod_impl.h"); - EXPECT_TRUE(h.contains("#include ")); - EXPECT_TRUE(h.contains("nlohmannToQVariant")); - EXPECT_TRUE(h.contains("#include \"logos_result.h\"")); - EXPECT_TRUE(h.contains("stdResultToQt")); - EXPECT_TRUE(h.contains("auto _result = m_impl.getResult(")); - EXPECT_TRUE(h.contains("return stdResultToQt(_result)")); -} - -TEST(LidlGenProvider, StdResultMethodHasLogosResultReturnType) -{ - ModuleDecl m; - m.name = "resmod2"; - m.version = "1.0.0"; - MethodDecl md; - md.name = "doWork"; - md.returnType = { TypeExpr::Primitive, "result", {} }; - md.resultReturn = true; - m.methods.append(md); - - QString h = lidlMakeProviderHeader(m, "ResModImpl2", "resmod2_impl.h"); - EXPECT_TRUE(h.contains("LogosResult doWork(")); -} - -// --------------------------------------------------------------------------- -// Empty module -// --------------------------------------------------------------------------- - -TEST(LidlGenProvider, EmptyModuleGeneratesValidCode) -{ - ModuleDecl m; - m.name = "empty"; - m.version = "0.0.1"; - - QString h = lidlMakeProviderHeader(m, "EmptyImpl", "empty_impl.h"); - EXPECT_TRUE(h.contains("EmptyProviderObject")); - EXPECT_TRUE(h.contains("EmptyPlugin")); - - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("::callMethod(")); - EXPECT_TRUE(d.contains("::getMethods()")); - // There is no separate getEvents(): events (when present) ride inside - // getMethods(), so the provider vtable never gains a slot. - EXPECT_FALSE(d.contains("::getEvents()")); -} - -// --------------------------------------------------------------------------- -// Event introspection generation — events are folded INTO getMethods() (each -// tagged type "event"), not a separate getEvents() vtable method. This keeps -// LogosProviderObject's vtable layout stable across SDK versions. -// --------------------------------------------------------------------------- - -static ModuleDecl makeEventModule() -{ - ModuleDecl m; - m.name = "evt_module"; - m.version = "1.0.0"; - - // Documented event with two params (multi-line description). - { - EventDecl ed; - ed.name = "userLoggedIn"; - ed.description = "Auth done.\nToken issued."; - ParamDecl p1; p1.name = "userId"; p1.type = { TypeExpr::Primitive, "tstr", {} }; - ParamDecl p2; p2.name = "token"; p2.type = { TypeExpr::Primitive, "tstr", {} }; - ed.params.append(p1); - ed.params.append(p2); - m.events.append(ed); - } - // Undocumented, no-arg event. - { - EventDecl ed; - ed.name = "tick"; - m.events.append(ed); - } - return m; -} - -TEST(LidlGenProvider, GetMethodsContainsEventsTaggedEvent) -{ - auto m = makeEventModule(); - QString d = lidlMakeProviderDispatch(m); - // No separate getEvents() override is generated… - EXPECT_FALSE(d.contains("::getEvents()")); - // …instead events appear inside getMethods(), each tagged type "event". - EXPECT_TRUE(d.contains("::getMethods()")); - EXPECT_TRUE(d.contains("QStringLiteral(\"event\")")); - EXPECT_TRUE(d.contains("\"userLoggedIn\"")); - EXPECT_TRUE(d.contains("\"tick\"")); -} - -TEST(LidlGenProvider, GetMethodsEventHasSignatureAndParams) -{ - auto m = makeEventModule(); - QString d = lidlMakeProviderDispatch(m); - // Event signature is computed from its params (tstr → QString). - EXPECT_TRUE(d.contains("userLoggedIn(QString,QString)")); - EXPECT_TRUE(d.contains("\"userId\"")); - EXPECT_TRUE(d.contains("\"token\"")); -} - -TEST(LidlGenProvider, GetMethodsEventEmitsDescription) -{ - auto m = makeEventModule(); - QString d = lidlMakeProviderDispatch(m); - // The multi-line description is emitted with its newline escaped (\n). - EXPECT_TRUE(d.contains("Auth done.\\nToken issued.")); -} - -TEST(LidlGenProvider, EventEntriesHaveNoReturnType) -{ - // An events-only module: events are void, so no returnType/isInvokable key - // is emitted anywhere (those belong to methods only). - auto m = makeEventModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_FALSE(d.contains("\"returnType\"")); - EXPECT_FALSE(d.contains("\"isInvokable\"")); -} - -TEST(LidlGenProvider, MethodEntriesTaggedMethod) -{ - // A module with methods: each getMethods() entry is tagged type "method". - auto m = makeTestModule(); - QString d = lidlMakeProviderDispatch(m); - EXPECT_TRUE(d.contains("QStringLiteral(\"method\")")); -} diff --git a/tests/experimental/test_lidl_type_mapping.cpp b/tests/experimental/test_lidl_type_mapping.cpp index f68b5e2..45ca949 100644 --- a/tests/experimental/test_lidl_type_mapping.cpp +++ b/tests/experimental/test_lidl_type_mapping.cpp @@ -1,6 +1,6 @@ #include #include "lidl_gen_client.h" -#include "lidl_gen_provider.h" +#include "lidl_emit_common.h" // --------------------------------------------------------------------------- // lidlTypeToQt