diff --git a/README.md b/README.md index 4227546..10aedcb 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,11 @@ An impl opts in by inheriting from `LogosModuleContext` (defined in `logos_modul class MyModuleImpl : public LogosModuleContext { public: LogosMap doWork(const std::string& input) { - // Call into a declared dependency — no LogosAPI in sight. - auto reply = logos().some_dep.somethingAsync(input); + // Cross-module call through the flat LogosModules aggregator. + // Because this module is `interface: "universal"`, mkLogosModule.nix + // passed -DLOGOS_API_STYLE=std to the codegen, so every + // wrapper takes/returns std types — no Qt at the call site. + std::string reply = modules().some_dep.echo(input); // ... } @@ -231,12 +234,74 @@ Available getters: | `modulePath()` | Directory containing the module's plugin file. Useful for loading bundled resources (icons, QML files, schema docs). | | `instanceId()` | Stable per-instance ID assigned by the host. Two side-by-side instances of the same module get distinct IDs. | | `instancePersistencePath()` | Per-instance writable data directory the host owns the lifecycle of. The canonical place for module state (config, caches, small databases). Wiped on uninstall; survives upgrades. | -| `logos()` | Typed access to the module's `LogosModules` aggregate (one accessor per declared dependency in `metadata.json`, plus the always-present `core_manager`). | +| `modules()` | The module's flat `LogosModules` aggregate — one accessor per `metadata.json#dependencies` entry (nothing else; apps that need to manage the core do so via liblogos' C API). `LogosModules` is forward-declared in the SDK header and made complete by the impl's `#include "logos_sdk.h"`, so the call site just writes `modules().some_dep.someMethod(...)`. Each accessor's wrapper class signatures use the type surface picked at THIS module's build time (see "API style" below). | + +#### API style: Qt vs std + +Each module's build picks **one** API style for the generated `` client wrappers and the `LogosModules` umbrella — they're mutually exclusive, no composite output: + +| `metadata.json#interface` | `LOGOS_API_STYLE` | Wrapper signatures | +|---|---|---| +| `"universal"` | `std` | `std::string`, `std::vector`, `LogosMap`, `LogosList`, `int64_t`, `StdLogosResult` | +| `"legacy"` / `"provider"` / absent | `qt` (default) | `QString`, `QStringList`, `QVariantList`, `QVariantMap`, `int`, `LogosResult` | + +`mkLogosModule.nix` reads `interface` and threads `-DLOGOS_API_STYLE=std` through to the codegen for universal modules; everyone else defaults to Qt and stays bit-for-bit backward compatible. Inside the universal module's `.cpp`, the call site is: + +```cpp +// Universal module (api-style=std): +std::string reply = modules().some_dep.echo("hi"); +``` + +…and in a handcrafted Qt module the same call is: + +```cpp +// Legacy / provider module (api-style=qt): +QString reply = modules().some_dep.echo(QString("hi")); +``` + +The wire is identical (`QVariant` under the hood); for std mode the Qt↔std conversion is inlined in the generated wrapper's `.cpp`, so the calling translation unit needs zero Qt headers. + +> **Migrating to std types**: The choice is driven entirely by `interface`. A handcrafted module that wants std types should switch to `interface: "universal"` — there's no per-flag override on `metadata.json`. All getters return empty / null values when the module is loaded outside a host that provisions a context (CLI tests, unit tests using the impl directly). The `onContextReady()` hook still fires once at framework load time; tests that bypass the framework can call `_logosCoreSetContext_` / `_logosCoreSetLogosModulesPtr_` directly to simulate. Codegen does NOT require inheritance — modules that don't inherit `LogosModuleContext` compile unchanged. The generator emits a single `onInit` override per provider that delegates to SFINAE'd helpers (`_logos_codegen_::maybeSet*`), and the non-inheriting overloads collapse to no-ops. +#### Events: `logos_events:` + +Universal modules declare events in a Qt-`signals:`-style `logos_events:` section. The codegen parses each prototype, emits the matching method bodies in a sidecar `_events.cpp` (Qt-MOC style), and ships a `.lidl` file describing them so consumer-side codegen can produce typed subscribers: + +```cpp +#include + +class MyModuleImpl : public LogosModuleContext { +public: + void doWork() { + userLoggedIn("alice", 12345); // typed emit — same name as the declaration + } + +logos_events: // expands to `public:`; parsed by the codegen + void userLoggedIn(const std::string& userId, int64_t timestamp); + void messageReceived(const std::string& from, const std::string& body); +}; +``` + +The author writes only the declarations; the codegen supplies the bodies (analogous to Qt MOC for `signals:`). Each call marshals typed args into a `QVariantList` and routes them through `LogosModuleContext::emitEventImpl_` → `LogosProviderBase::emitEvent` → the existing QRO `eventResponse` channel. No wire-format change. + +**Consumer side** — typed `on(...)` accessors are generated on the dep's `` wrapper. The generic `onEvent(name, cb)` channel stays available as a forward-compat escape hatch: + +```cpp +// From any module that depends on the one declaring the events: +modules().my_module.onUserLoggedIn( + [](const std::string& userId, int64_t timestamp) { + // typed args, no manual QVariantList unpacking + }); +``` + +The accessor's parameter types follow the consumer's own `--api-style` (so a `universal` consumer sees `const std::string&` / `int64_t`, a handcrafted Qt consumer sees `const QString&` / `int`). + +**Legacy** — modules that haven't migrated keep the old `std::function emitEvent` member working. The codegen still detects it and wires the lambda in the provider constructor. New code should prefer `logos_events:`. + ### API #### LogosResult diff --git a/cpp-generator/docs/project.md b/cpp-generator/docs/project.md index e3854a3..0ea2ea3 100644 --- a/cpp-generator/docs/project.md +++ b/cpp-generator/docs/project.md @@ -81,24 +81,64 @@ Converts `ModuleDecl` back to LIDL text. Used for roundtrip testing (parse → s - `lidlMakeSource(ModuleDecl)` — generates client API source - `lidlGenerateMetadataJson(ModuleDecl)` — generates metadata.json content +### Per-build API-style choice (`legacy/generator_lib.{h,cpp}`) + +The codegen exposes **one** wrapper class per module — `` — with signatures that match the API style picked at the consumer's build time. The two styles are mutually exclusive (no composite output): + +| `--api-style` | Wrapper signatures | +|---|---| +| `qt` (default) | `QString` / `QStringList` / `QVariantList` / `QVariantMap` / `int` / `LogosResult` | +| `std` | `std::string` / `std::vector` / `LogosMap` / `LogosList` / `int64_t` / `StdLogosResult` | + +Both styles emit: + +- A `` client class with sync method shapes + matching `Async(...)` overloads. +- The std variant additionally inlines Qt↔std conversion in its `.cpp` so the caller's translation unit needs zero Qt headers. + +The umbrella `logos_sdk.h` is also generated per-build and aggregates every dep into a flat `LogosModules` struct — no nested view: + +```cpp +struct LogosModules { + LogosAPI* api; + SomeDep some_dep; // one accessor per `metadata.json#dependencies` entry + // ... +}; +``` + +Only the modules explicitly listed as dependencies are exposed. The runtime's `core_manager` is intentionally NOT in `LogosModules` — apps that need to manage the core do so via liblogos' C API, not via a typed RPC wrapper. + +`ApiStyle` enum + new helpers in `generator_lib`: + +- `enum class ApiStyle { Qt, Std }` — passed to every wrapper-emitting function. +- File-local `mapParamTypeStd` / `mapReturnTypeStd` / `stdParamToQVariant` / `qVariantToStdReturn` — std-side type-mapping + Qt↔std conversion expressions. Hidden from `generator_lib.h` (not part of the public surface). +- `makeHeader(moduleName, className, methods, apiStyle, events)` / `makeSource(moduleName, className, headerBaseName, methods, apiStyle, events)` — single entry points that branch on `apiStyle` internally to emit the right include block, signature shape, and conversion bridges. `events` is loaded from a `.lidl` sidecar via `--events-from`; when non-empty, the wrapper also gets one typed `on(callback)` adapter per declared event (callback arg types follow `apiStyle`). The std-style wrapper grows the necessary `ensureReplica()` plumbing on demand. + +Flag plumbing: + +1. `metadata.json#interface == "universal"` → `mkLogosModule.nix` adds `-DLOGOS_API_STYLE=std` to `extraCmakeFlags`. Anything else (`"legacy"`, `"provider"`, absent) leaves the default `qt`. +2. `LogosModule.cmake` reads `${LOGOS_API_STYLE}` (default `qt`) and forwards `--api-style=${LOGOS_API_STYLE}` to the `logos-cpp-generator --general-only` invocation that writes the umbrella. Each module's Nix build emits **two** header derivations (`.headers-qt` and `.headers-std`) via `buildHeaders.nix` — one `logos-cpp-generator --api-style=…` run per style, at the dep's build time. A consumer's `buildPlugin.nix` picks `dep.headers-${apiStyle}` and copies its `include/` straight into the build sandbox; no codegen runs at consume time. Nix's laziness means only the variant a downstream actually depends on is realised. +3. `legacy/main.cpp` parses `--api-style` once and threads the resulting `ApiStyle` through `generateFromPlugin`, `writeUmbrellaHeader{,FromDeps}`. No `_api_std.{h,cpp}` files are ever emitted; each module gets a single `_api.h` + `_api.cpp` pair regardless of style. + ### Provider Generation (`lidl_gen_provider.h/cpp`) - `lidlTypeToStd(TypeExpr)` — maps LIDL types to C++ std type strings - `lidlIsStdConvertible(TypeExpr)` — checks if a type has a pure C++ representation - `lidlMakeProviderHeader(ModuleDecl, implClass, implHeader)` — generates Qt glue header - Emits `nlohmannToQVariant()` helper when any method has `jsonReturn = true` - - Wires `m_impl.emitEvent` → `LogosProviderBase::emitEvent` when `hasEmitEvent` or `events` are present - - Always emits an `onInit(LogosAPI*) override` that, via SFINAE'd helpers in `logos_module_context.h`, (a) copies the three runtime-injected properties (`modulePath`, `instanceId`, `instancePersistencePath`) into the impl and (b) constructs a per-module `LogosModules` aggregate and threads its pointer through the same base. Impls that don't inherit `LogosModuleContext` compile unchanged — the helper overloads collapse to no-ops. The full `LogosAPI` is never exposed past the provider boundary. - - Always emits `#include "logos_sdk.h"` and a `std::unique_ptr m_logosModules` member; ownership lives on the provider, the context base sees only a non-owning `void*` reinterpreted in `LogosModuleContext::logos()`. + - Legacy path: wires `m_impl.emitEvent` → `LogosProviderBase::emitEvent` in the constructor when `hasEmitEvent` is set (un-migrated modules using the old `std::function emitEvent` member) + - Always emits an `onInit(LogosAPI*) override` that, via SFINAE'd helpers in `logos_module_context.h`, (a) copies the three runtime-injected properties (`modulePath`, `instanceId`, `instancePersistencePath`) into the impl, (b) constructs a per-module `LogosModules` aggregate and threads its pointer through the same base, and (c) installs the typed-event callback (`maybeSetEmitEvent`) consumed by `_events.cpp` method bodies. Impls that don't inherit `LogosModuleContext` compile unchanged — the helper overloads collapse to no-ops. The full `LogosAPI` is never exposed past the provider boundary. + - Always emits `#include "logos_sdk.h"` and a `std::unique_ptr m_logosModules` member; ownership lives on the provider, the context base sees only a non-owning `void*` reinterpreted in `LogosModuleContext::modules()` (which depends on the impl's TU having included `logos_sdk.h`). - `lidlMakeProviderDispatch(ModuleDecl)` — generates callMethod/getMethods dispatch -- `lidlGenerateProviderGlue(lidlPath, ...)` — full pipeline from .lidl file +- `lidlMakeEventsSource(ModuleDecl, implClass, implHeader)` — generates `_events.cpp`: Qt-MOC-style method bodies for prototypes declared in the impl's `logos_events:` block. Each body marshals typed args into a `QVariantList` and calls `this->emitEventImpl_("", &args)` on the LogosModuleContext base. +- `lidlGenerateProviderGlue(lidlPath, ...)` — full pipeline from .lidl file. Also emits `_events.cpp` and a `.lidl` sidecar (via `lidlSerialize`) when the module has any events; both ride the dep's `headers-*` outputs to power consumer-side typed `on()` accessors. ### Impl Header Parser (`impl_header_parser.h/cpp`) - `parseImplHeader(headerPath, className, metadataPath, err)` — parses C++ header + metadata.json into ModuleDecl -- State machine: `LookingForClass` → `InClass` → `InPublic`/`InPrivate` +- State machine: `LookingForClass` → `InClass` → `InPublic`/`InPrivate`/`InLogosEvents` +- The literal `logos_events:` token (defined in `logos_module_context.h` as `#define logos_events public`) opens an events section; bare prototypes inside become `EventDecl{name, params}` entries appended to `ModuleDecl.events` - Skips: constructors, destructors, typedefs, using, friend, enum, struct, `std::function` declarations -- Detects `std::function<...> emitEvent` members and sets `ModuleDecl.hasEmitEvent = true` +- Legacy: still detects `std::function<...> emitEvent` members and sets `ModuleDecl.hasEmitEvent = true` so un-migrated modules keep working through the provider constructor's lambda wiring - Recognizes `LogosMap` and `LogosList` return types (nlohmann::json aliases) and sets `MethodDecl.jsonReturn = true` - Template-aware parameter splitting (handles `std::vector` correctly) @@ -143,6 +183,19 @@ logos-cpp-generator --metadata metadata.json --general-only --output-dir ./gener logos-cpp-generator --provider-header src/provider.h --output-dir ./generated ``` +### Consumer wrapper with typed event accessors + +The `--events-from ` flag points the legacy `.dylib --module-only` codegen at a LIDL sidecar shipped alongside the dep's pre-built headers. When set, the generated `_api.{h,cpp}` gains one typed `on(callback)` accessor per declared event (callback arg types match `--api-style`): + +```bash +logos-cpp-generator /path/to/plugin.dylib \ + --module-only --api-style std \ + --events-from /path/to/dep/share/logos/my_module.lidl \ + --output-dir ./generated +``` + +In Nix builds this is wired automatically: `buildHeaders.nix` looks for `/share/logos/.lidl` (which `buildPlugin.nix`'s installPhase placed there) and threads it through. + ## Building The generator is built as part of logos-cpp-sdk: diff --git a/cpp-generator/docs/spec.md b/cpp-generator/docs/spec.md index 7f88a06..f594dec 100644 --- a/cpp-generator/docs/spec.md +++ b/cpp-generator/docs/spec.md @@ -142,31 +142,63 @@ The parser uses a state machine to find the target class, track access specifier Module metadata (name, version, description, dependencies) comes from `metadata.json`, not from the header. -### Event Emission via Header Detection +### Event Emission via `logos_events:` -Universal modules can emit named events to the host/runtime by declaring a public `emitEvent` callback in their impl header: +Universal modules declare events in a Qt-`signals:`-style section parsed by the codegen. The same method name appears on both sides — declared in `logos_events:`, called directly to emit: ```cpp -class MyModuleImpl { +#include + +class MyModuleImpl : public LogosModuleContext { public: - std::function emitEvent; - // ... methods ... + void doWork() { + userLoggedIn("alice", 12345); // typed emit, same name + } + +logos_events: // expands to `public:`; recognised by impl_header_parser + void userLoggedIn(const std::string& userId, int64_t timestamp); + void messageReceived(const std::string& from, const std::string& body); }; ``` -The parser detects this `std::function` member by name and sets `ModuleDecl.hasEmitEvent = true`. The generator then wires the callback in the provider constructor: +`impl_header_parser.cpp` recognises the raw `logos_events:` token (before preprocessing) and populates `ModuleDecl.events` with one `EventDecl` per prototype. Three artifacts get emitted from this: -```cpp -MyModuleProviderObject() { - m_impl.emitEvent = [this](const std::string& name, const std::string& data) { - QVariantList args; - if (!data.empty()) args << QString::fromStdString(data); - emitEvent(QString::fromStdString(name), args); - }; -} -``` +1. **`_events.cpp`** — Qt-MOC-style definitions of each declared event method on the impl class. Bodies marshal typed args into a `QVariantList` and call `this->emitEventImpl_("", &args)`, a protected helper on `LogosModuleContext`: -This replaces the previous approach of declaring events in `metadata.json`. The `events` array in metadata.json is still supported for backward compatibility (e.g., LIDL-defined modules), but header detection is the preferred approach for universal modules since it keeps event information co-located with the implementation. + ```cpp + void MyModuleImpl::userLoggedIn(const std::string& userId, int64_t timestamp) { + QVariantList _args{ + QVariant(QString::fromStdString(userId)), + QVariant(static_cast(timestamp)) + }; + this->emitEventImpl_("userLoggedIn", &_args); + } + ``` + +2. **Provider `onInit` wiring** — `_qt_glue.h` adds a `_logos_codegen_::maybeSetEmitEvent` call alongside the existing `maybeSetContext` / `maybeSetLogosModules`. The lambda casts the void* back to QVariantList and forwards to `LogosProviderBase::emitEvent(QString, QVariantList)` (same wire as before): + + ```cpp + _logos_codegen_::maybeSetEmitEvent(m_impl, + [this](const std::string& name, void* args) { + emitEvent(QString::fromStdString(name), + *static_cast(args)); + }); + ``` + +3. **`.lidl` sidecar** — a serialised view of the module's declared events (using the existing `lidlSerialize` from `lidl_serializer.cpp`): + + ``` + module my_module { + event userLoggedIn(userId: tstr, timestamp: int) + event messageReceived(from: tstr, body: tstr) + } + ``` + + `buildPlugin.nix` ships this at `$out/share/logos/.lidl`. `buildHeaders.nix` passes it to the consumer-side codegen via `--events-from`, which adds typed `on(callback)` accessors to the generated `` wrapper (one per declared event, callback-arg types respect `--api-style`). + +**Legacy backward-compat**: the older `std::function emitEvent` member is still detected by the parser and wired in the provider constructor — un-migrated modules (e.g. logos-package-manager-module) keep working through their existing `emitEvent("name", "json")` call sites. New code should prefer `logos_events:`. + +Module metadata (name, version, description, dependencies) still comes from `metadata.json`, not from the header. ### Generated Output @@ -192,12 +224,32 @@ Implements two methods on the ProviderObject: #### Client Stubs (`_api.h` + `_api.cpp`) -Generated from LIDL (not from `--from-header`). Provides: +Generated from LIDL (not from `--from-header`). Each module gets **one** `` wrapper class whose signature shape is picked by the consumer's build via `--api-style`: -- Typed sync methods that call `invokeRemoteMethod()` and convert the `QVariant` result -- Async overloads with callback + timeout -- Event subscription (`on()`) and emission (`trigger()`) -- Umbrella `logos_sdk.h` / `logos_sdk.cpp` aggregating all module wrappers +| `--api-style` | Wrapper signatures | +|---|---| +| `qt` (default) | QString / QStringList / QVariantList / QVariantMap / int / LogosResult | +| `std` | std::string / std::vector / LogosMap / LogosList / int64_t / StdLogosResult | + +Both styles provide: + +- Typed sync methods that call `invokeRemoteMethod()` and convert the `QVariant` result. +- Async overloads with callback + timeout. +- The Qt style additionally exposes event subscription (`on()`) and emission (`trigger()`); the std style omits these — universal modules that need cross-module events can be addressed in a follow-up. + +The std wrappers call the same underlying `invokeRemoteMethod`; the Qt↔std conversion is generated inline in their `.cpp` so the calling translation unit needs zero Qt headers. Both styles emit the **same filename** (`_api.h` / `_api.cpp`) and the **same class name** (``) — the two are mutually exclusive at build time. No `_api_std.{h,cpp}` files are ever produced. + +Umbrella files (`logos_sdk.h` / `logos_sdk.cpp`) aggregate every dep into a flat `LogosModules` struct — one accessor per `metadata.json#dependencies` entry, nothing else: + +```cpp +struct LogosModules { + LogosAPI* api; + SomeDep some_dep; // one per declared dependency + // ... +}; +``` + +Only the modules explicitly listed as dependencies appear. The runtime's `core_manager` is intentionally NOT exposed here — apps that need to manage the core (basecamp, logoscore) use liblogos' C API directly, not a typed RPC wrapper. ## Features & Requirements diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index a61122a..896d469 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -249,13 +249,17 @@ ImplParseResult parseImplHeader(const QString& headerPath, QStringList lines = source.split('\n'); - // State machine: find "class ", then collect public methods - enum State { LookingForClass, InClass, InPublic, InPrivate }; + // State machine: find "class ", then collect declarations. + // `InLogosEvents` is entered by the literal `logos_events:` token + // (mirrors Qt's `signals:`) — methods declared there are parsed as + // EventDecls and appended to ModuleDecl.events instead of .methods. + enum State { LookingForClass, InClass, InPublic, InPrivate, InLogosEvents }; State state = LookingForClass; int braceDepth = 0; QRegularExpression classRe("\\bclass\\s+" + QRegularExpression::escape(className) + "\\b"); QRegularExpression accessRe("^\\s*(public|private|protected)\\s*:"); + QRegularExpression eventsRe("^\\s*logos_events\\s*:"); QRegularExpression ctorDtorRe("^\\s*~?" + QRegularExpression::escape(className) + "\\s*\\("); for (const QString& rawLine : lines) { @@ -275,6 +279,7 @@ ImplParseResult parseImplHeader(const QString& headerPath, case InClass: case InPublic: case InPrivate: + case InLogosEvents: for (QChar c : line) { if (c == '{') braceDepth++; else if (c == '}') braceDepth--; @@ -285,6 +290,16 @@ ImplParseResult parseImplHeader(const QString& headerPath, goto done; } + // `logos_events:` takes precedence over the standard access + // specifiers: it's a separate section that the codegen pulls + // event prototypes from. (At preprocess time, `logos_events` + // expands to `public`, but the raw source still carries the + // token we recognise here.) + if (eventsRe.match(line).hasMatch()) { + state = InLogosEvents; + break; + } + { QRegularExpressionMatch am = accessRe.match(line); if (am.hasMatch()) { @@ -295,8 +310,7 @@ ImplParseResult parseImplHeader(const QString& headerPath, } } - if (state != InPublic) break; - + // Skip noise & non-declarations in any section. if (line.isEmpty() || line.startsWith("//") || line.startsWith("#") || line.startsWith("/*") || line.startsWith("*")) break; @@ -309,7 +323,34 @@ ImplParseResult parseImplHeader(const QString& headerPath, || line.startsWith("struct")) break; + if (state == InLogosEvents) { + // Inside `logos_events:` — every bare prototype is an event. + // Events are always void-returning by definition, so we + // re-use parseMethodLine to extract name + params and + // discard the return type. + if (line.endsWith(';')) { + QString decl = line.left(line.size() - 1).trimmed(); + MethodDecl md; + if (parseMethodLine(decl, md)) { + EventDecl ed; + ed.name = md.name; + ed.params = md.params; + result.module.events.append(ed); + } + } + break; + } + + if (state != InPublic) break; + if (line.contains("std::function<")) { + // Legacy `std::function<…> emitEvent` member — predates + // the typed `logos_events:` mechanism. Still recognised + // for backward compat: modules that haven't migrated yet + // (e.g. logos-package-manager-module) keep their existing + // emit("name", "json") call sites working through the + // provider constructor's lambda wiring. New code should + // prefer `logos_events:`. if (line.contains("emitEvent")) result.module.hasEmitEvent = true; break; diff --git a/cpp-generator/experimental/lidl_gen_provider.cpp b/cpp-generator/experimental/lidl_gen_provider.cpp index 7c2b1b6..6f091f3 100644 --- a/cpp-generator/experimental/lidl_gen_provider.cpp +++ b/cpp-generator/experimental/lidl_gen_provider.cpp @@ -1,6 +1,7 @@ #include "lidl_gen_provider.h" #include "lidl_gen_client.h" // lidlToPascalCase, lidlTypeToQt #include "lidl_parser.h" +#include "lidl_serializer.h" #include "lidl_validator.h" #include @@ -269,10 +270,15 @@ QString lidlMakeProviderHeader(const ModuleDecl& module, << module.name << "\", \"" << (module.version.isEmpty() ? "0.0.0" : module.version) << "\")\n\n"; s << "public:\n"; - // Wire m_impl.emitEvent → LogosProviderBase::emitEvent when the impl - // declares a public emitEvent callback (detected from header) or when - // events are declared in metadata.json (legacy path). - if (module.hasEmitEvent || !module.events.isEmpty()) { + // Legacy backward-compat: if the impl still declares a + // `std::function<…> emitEvent` member (the old text-pattern path), + // wire it in the provider's constructor so existing modules + // (e.g. logos-package-manager-module) keep working through their + // `emitEvent("name", "json")` call sites. New universal modules + // should declare events in a typed `logos_events:` section — that + // path goes through `maybeSetEmitEvent` in onInit (below) and + // doesn't touch this constructor. + if (module.hasEmitEvent) { s << " " << providerObjectClass << "() {\n"; s << " m_impl.emitEvent = [this](const std::string& name, const std::string& data) {\n"; s << " QVariantList args;\n"; @@ -354,7 +360,7 @@ QString lidlMakeProviderHeader(const ModuleDecl& module, // 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 `logos()....`. + // declared dependencies via `modules()....`. // // Both wire-ups go through SFINAE'd helpers // (`_logos_codegen_::maybeSet*`) so impls that don't inherit @@ -375,6 +381,15 @@ QString lidlMakeProviderHeader(const ModuleDecl& module, s << " api->property(\"instancePersistencePath\").toString().toStdString());\n"; 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"; s << " }\n\n"; if (!module.events.isEmpty()) { @@ -521,6 +536,100 @@ QString lidlMakeProviderDispatch(const ModuleDecl& module) 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) // --------------------------------------------------------------------------- @@ -586,6 +695,35 @@ int lidlGenerateProviderGlue(const QString& lidlPath, 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 index 8f53887..598dd3b 100644 --- a/cpp-generator/experimental/lidl_gen_provider.h +++ b/cpp-generator/experimental/lidl_gen_provider.h @@ -19,6 +19,15 @@ QString lidlMakeProviderHeader(const ModuleDecl& module, // 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, diff --git a/cpp-generator/legacy/generator_lib.cpp b/cpp-generator/legacy/generator_lib.cpp index 411fb3f..37374b4 100644 --- a/cpp-generator/legacy/generator_lib.cpp +++ b/cpp-generator/legacy/generator_lib.cpp @@ -68,229 +68,517 @@ QString toQVariantConversion(const QString& type, const QString& argExpr) return argExpr + ".toString()"; } -QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods) +// ─── Std (pure-C++) type-mapping helpers ───────────────────────────────── +// +// File-local — not exposed in generator_lib.h. The single public entry +// point is makeHeader / makeSource taking an `ApiStyle` argument; when +// `apiStyle == Std`, those functions internally route through these +// helpers to pick the std type-mapping table. There's only one wrapper +// class per module — `` — whose signatures depend on apiStyle. +// The std-typed body still goes through the QVariant wire; the Qt↔std +// conversion is inlined at the call site, contained to the generated +// .cpp so callers never include Qt headers. + +static QString mapParamTypeStd(const QString& qtType) +{ + const QString base = mapParamType(qtType); + if (base == "QString") return "std::string"; + if (base == "QStringList") return "std::vector"; + if (base == "QJsonArray") return "LogosList"; + if (base == "QVariantList") return "LogosList"; + if (base == "QVariantMap") return "LogosMap"; + if (base == "QVariant") return "LogosMap"; + if (base == "int") return "int64_t"; + return base; +} + +static QString mapReturnTypeStd(const QString& qtType) +{ + const QString base = mapReturnType(qtType); + if (base == "void") return "void"; + if (base == "QString") return "std::string"; + if (base == "QStringList") return "std::vector"; + if (base == "QJsonArray") return "LogosList"; + if (base == "QVariantList") return "LogosList"; + if (base == "QVariantMap") return "LogosMap"; + if (base == "QVariant") return "LogosMap"; + if (base == "LogosResult") return "StdLogosResult"; + if (base == "int") return "int64_t"; + return base; +} + +// Returns a C++ expression that lifts a std-typed parameter into a +// QVariant-side temporary suitable for invokeRemoteMethod. The +// temporaries are rvalues consumed inline at the call site. +static QString stdParamToQVariant(const QString& qtType, const QString& argName) +{ + const QString base = mapParamType(qtType); + if (base == "QString") + return "QString::fromStdString(" + argName + ")"; + if (base == "QStringList") + return "[&]{ QStringList _q; _q.reserve(static_cast(" + argName + + ".size())); for (const auto& _s : " + argName + + ") _q.append(QString::fromStdString(_s)); return _q; }()"; + if (base == "QJsonArray") + return "QJsonDocument::fromJson(QByteArray::fromStdString(" + argName + + ".dump())).array()"; + if (base == "QVariantList" || base == "QVariantMap" || base == "QVariant") + return "QJsonDocument::fromJson(QByteArray::fromStdString(" + argName + + ".dump())).toVariant()" + (base == "QVariantList" ? ".toList()" + : base == "QVariantMap" ? ".toMap()" + : ""); + if (base == "int") + return "static_cast(" + argName + ")"; + return argName; +} + +// Returns a C++ expression that converts a QVariant return value into +// the std-typed return type. `varExpr` is the source QVariant. +static QString qVariantToStdReturn(const QString& qtType, const QString& varExpr) +{ + const QString base = mapReturnType(qtType); + if (base == "void") + return QString(); + if (base == "bool") + return varExpr + ".toBool()"; + if (base == "int") + return "static_cast(" + varExpr + ".toInt())"; + if (base == "double" || base == "float") + return varExpr + ".toDouble()"; + if (base == "QString") + return varExpr + ".toString().toStdString()"; + if (base == "QStringList") + return "[&]{ std::vector _v; const QStringList _q = " + + varExpr + ".toStringList(); _v.reserve(static_cast(_q.size())); " + "for (const QString& _s : _q) _v.push_back(_s.toStdString()); return _v; }()"; + if (base == "QJsonArray" || base == "QVariantList") + return "LogosList::parse(QJsonDocument(QJsonArray::fromVariantList(" + + varExpr + ".toList())).toJson(QJsonDocument::Compact).toStdString())"; + if (base == "QVariantMap" || base == "QVariant") + return "LogosMap::parse(QJsonDocument(QJsonObject::fromVariantMap(" + + varExpr + ".toMap())).toJson(QJsonDocument::Compact).toStdString())"; + if (base == "LogosResult") + return "[&]{ StdLogosResult _r; const LogosResult _q = " + varExpr + + ".value(); _r.success = _q.success; " + "if (_q.value.isValid()) _r.value = LogosMap::parse(" + "QJsonDocument(QJsonObject::fromVariantMap(_q.value.toMap())).toJson(QJsonDocument::Compact).toStdString()); " + "_r.error = _q.error.toString().toStdString(); return _r; }()"; + return varExpr + ".toString().toStdString()"; +} + +// Param-type predicate: passed by const-ref? +static bool isStdRefType(const QString& t) +{ + return t == "std::string" || t.startsWith("std::vector") + || t == "LogosMap" || t == "LogosList"; +} + +static bool isQtRefType(const QString& t) +{ + return t == "QString" || t == "QByteArray" || t == "QStringList" + || t == "QJsonArray" || t == "QVariantList" || t == "QVariantMap" + || t == "LogosResult"; +} + +QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events) { QString h; QTextStream s(&h); s << "#pragma once\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 \"logos_types.h\"\n"; - s << "#include \"logos_api.h\"\n"; - s << "#include \"logos_api_client.h\"\n"; - s << "#include \"logos_object.h\"\n\n"; + if (apiStyle == ApiStyle::Std) { + // Pure-C++ surface. Qt is still pulled in transitively by + // logos_api.h (LogosAPI is a QObject), but the wrapper's + // signatures are entirely std types so callers never have to + // type a Qt name themselves. + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \"logos_types.h\"\n"; + s << "#include \"logos_json.h\"\n"; + s << "#include \"logos_result.h\"\n"; + s << "#include \"logos_api.h\"\n"; + s << "#include \"logos_api_client.h\"\n"; + // Needed for the m_eventReplica member when the module declares + // any events. Cheap to include unconditionally — keeps the + // header symmetric with the Qt-style branch. + if (!events.isEmpty()) s << "#include \"logos_object.h\"\n"; + s << "\n"; + } else { + 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 \"logos_types.h\"\n"; + s << "#include \"logos_api.h\"\n"; + s << "#include \"logos_api_client.h\"\n"; + s << "#include \"logos_object.h\"\n\n"; + } s << "class " << className << " {\n"; s << "public:\n"; s << " explicit " << className << "(LogosAPI* api);\n\n"; - s << " using RawEventCallback = std::function;\n"; - s << " using EventCallback = std::function;\n\n"; - s << " bool on(const QString& eventName, RawEventCallback callback);\n"; - s << " bool on(const QString& eventName, EventCallback callback);\n"; - s << " void setEventSource(LogosObject* source);\n"; - s << " LogosObject* eventSource() const;\n"; - s << " void trigger(const QString& eventName);\n"; - s << " void trigger(const QString& eventName, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, Args&&... args) {\n"; - s << " trigger(eventName, packVariantList(std::forward(args)...));\n"; - s << " }\n"; - s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n"; - s << " trigger(eventName, source, packVariantList(std::forward(args)...));\n"; - s << " }\n\n"; + if (apiStyle == ApiStyle::Qt) { + // Event subscription / trigger surface — Qt-typed. + s << " using RawEventCallback = std::function;\n"; + s << " using EventCallback = std::function;\n\n"; + s << " bool on(const QString& eventName, RawEventCallback callback);\n"; + s << " bool on(const QString& eventName, EventCallback callback);\n"; + s << " void setEventSource(LogosObject* source);\n"; + s << " LogosObject* eventSource() const;\n"; + s << " void trigger(const QString& eventName);\n"; + s << " void trigger(const QString& eventName, const QVariantList& data);\n"; + s << " template\n"; + s << " void trigger(const QString& eventName, Args&&... args) {\n"; + s << " trigger(eventName, packVariantList(std::forward(args)...));\n"; + s << " }\n"; + s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n"; + s << " template\n"; + s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n"; + s << " trigger(eventName, source, packVariantList(std::forward(args)...));\n"; + s << " }\n\n"; + } + // Typed event subscribers — generated from the `.lidl` sidecar shipped + // with the dep's pre-built headers (via --events-from). One typed + // adapter per declared event, callback-arg types follow apiStyle. + // The generic `on(name, cb)` channel above stays available; for + // std-style consumers it's not exposed but the typed accessors are. + for (const QJsonValue& ev : events) { + const QJsonObject eo = ev.toObject(); + const QString evName = eo.value("name").toString(); + if (evName.isEmpty()) continue; + // `on` + capitalized event name. `evName` is the verbatim name + // the impl declared in its `logos_events:` block (typically + // camelCase, e.g. `userLoggedIn`), so we just uppercase its + // first letter — `toPascalCase` would clobber the internal + // camelCase boundaries (snake_case input is its target). + QString cap = evName; + if (!cap.isEmpty()) cap[0] = cap[0].toUpper(); + const QString accessorName = QString("on") + cap; + const QJsonArray evParams = eo.value("params").toArray(); + + // Build the callback's parameter list using apiStyle's type table. + QString cbParams; + for (int i = 0; i < evParams.size(); ++i) { + const QJsonObject p = evParams.at(i).toObject(); + QString qtPt = p.value("type").toString(); + QString pt = (apiStyle == ApiStyle::Std) + ? mapParamTypeStd(qtPt) : mapParamType(qtPt); + bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt); + if (byRef) cbParams += "const " + pt + "& "; + else cbParams += pt + " "; + cbParams += p.value("name").toString(); + if (i + 1 < evParams.size()) cbParams += ", "; + } + s << " bool " << accessorName + << "(std::function callback);\n"; + } + if (!events.isEmpty()) s << "\n"; // Methods for (const QJsonValue& v : methods) { const QJsonObject o = v.toObject(); const bool invokable = o.value("isInvokable").toBool(); if (!invokable) continue; const QString name = o.value("name").toString(); - const QString ret = mapReturnType(o.value("returnType").toString()); + const QString qtRet = o.value("returnType").toString(); + const QString ret = (apiStyle == ApiStyle::Std) + ? mapReturnTypeStd(qtRet) : mapReturnType(qtRet); s << " " << ret << " " << name << "("; QJsonArray params = o.value("parameters").toArray(); for (int i = 0; i < params.size(); ++i) { QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); + QString qtPt = p.value("type").toString(); + QString pt = (apiStyle == ApiStyle::Std) + ? mapParamTypeStd(qtPt) : mapParamType(qtPt); QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray" || pt == "QVariantList" || pt == "QVariantMap") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } + bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt); + if (byRef) s << "const " << pt << "& " << pn; + else s << pt << " " << pn; if (i + 1 < params.size()) s << ", "; } s << ");\n"; // Async overload: same params + callback + optional Timeout - QString asyncCallbackType = (ret == "void") ? QString("std::function") : QString("std::function"; + QString asyncCallbackType = (ret == "void") + ? QString("std::function") + : QString("std::function"; s << " void " << name << "Async("; for (int i = 0; i < params.size(); ++i) { QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); + QString qtPt = p.value("type").toString(); + QString pt = (apiStyle == ApiStyle::Std) + ? mapParamTypeStd(qtPt) : mapParamType(qtPt); QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray" || pt == "QVariantList" || pt == "QVariantMap") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } + bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt); + if (byRef) s << "const " << pt << "& " << pn; + else s << pt << " " << pn; if (i + 1 < params.size()) s << ", "; } if (params.size() > 0) s << ", "; s << asyncCallbackType << " callback, Timeout timeout = Timeout());\n"; } s << "\nprivate:\n"; - s << " LogosObject* ensureReplica();\n"; - s << " template\n"; - s << " static QVariantList packVariantList(Args&&... args) {\n"; - s << " QVariantList list;\n"; - s << " list.reserve(sizeof...(Args));\n"; - s << " using Expander = int[];\n"; - s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; - s << " return list;\n"; - s << " }\n"; + // ensureReplica() is needed whenever the wrapper subscribes to + // events — in Qt mode that's always (the generic `on(...)` channel + // is exposed); in std mode it's gated on at least one declared + // event in the LIDL sidecar. + if (apiStyle == ApiStyle::Qt || !events.isEmpty()) { + s << " LogosObject* ensureReplica();\n"; + } + if (apiStyle == ApiStyle::Qt) { + s << " template\n"; + s << " static QVariantList packVariantList(Args&&... args) {\n"; + s << " QVariantList list;\n"; + s << " list.reserve(sizeof...(Args));\n"; + s << " using Expander = int[];\n"; + s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; + s << " return list;\n"; + s << " }\n"; + } s << " LogosAPI* m_api;\n"; s << " LogosAPIClient* m_client;\n"; s << " QString m_moduleName;\n"; - s << " LogosObject* m_eventReplica = nullptr;\n"; - s << " LogosObject* m_eventSource = nullptr;\n"; + if (apiStyle == ApiStyle::Qt) { + s << " LogosObject* m_eventReplica = nullptr;\n"; + s << " LogosObject* m_eventSource = nullptr;\n"; + } else if (!events.isEmpty()) { + // std-style consumer: only the receive-side replica is needed + // (no `trigger(...)` API on the std wrapper, so no eventSource). + s << " LogosObject* m_eventReplica = nullptr;\n"; + } s << "};\n"; return h; } -QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods) +QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events) { QString c; QTextStream s(&c); s << "#include \"" << headerBaseName << "\"\n\n"; - s << "#include \n\n"; + s << "#include \n"; + if (apiStyle == ApiStyle::Std) { + // Conversion bridges between std types and QVariant — confined + // to this .cpp so the caller's translation unit doesn't need + // any Qt headers itself. + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + // logos_object.h is the type of LogosObject* used by typed event + // accessors when the module declares events. Always include in + // std mode when events are present. + if (!events.isEmpty()) s << "#include \"logos_object.h\"\n"; + } + s << "\n"; s << className << "::" << className << "(LogosAPI* api) : m_api(api), m_client(api->getClient(\"" << moduleName << "\")), m_moduleName(QStringLiteral(\"" << moduleName << "\")) {}\n\n"; - s << "LogosObject* " << className << "::ensureReplica() {\n"; - s << " if (!m_eventReplica) {\n"; - s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; - s << " if (!replica) {\n"; - s << " qWarning() << \"" << className << ": failed to acquire remote object for events on\" << m_moduleName;\n"; - s << " return nullptr;\n"; - s << " }\n"; - s << " m_eventReplica = replica;\n"; - s << " }\n"; - s << " return m_eventReplica;\n"; - s << "}\n\n"; - s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " LogosObject* origin = ensureReplica();\n"; - s << " if (!origin) {\n"; - s << " return false;\n"; - s << " }\n"; - s << " m_client->onEvent(origin, eventName, callback);\n"; - s << " return true;\n"; - s << "}\n\n"; - s << "bool " << className << "::on(const QString& eventName, EventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n"; - s << " callback(data);\n"; - s << " });\n"; - s << "}\n\n"; - s << "void " << className << "::setEventSource(LogosObject* source) {\n"; - s << " m_eventSource = source;\n"; - s << "}\n\n"; - s << "LogosObject* " << className << "::eventSource() const {\n"; - s << " return m_eventSource;\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName) {\n"; - s << " trigger(eventName, QVariantList{});\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName, const QVariantList& data) {\n"; - s << " if (!m_eventSource) {\n"; - s << " qWarning() << \"" << className << ": no event source set for trigger\" << eventName;\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(m_eventSource, eventName, data);\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n"; - s << " if (!source) {\n"; - s << " qWarning() << \"" << className << ": cannot trigger\" << eventName << \"with null source\";\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(source, eventName, data);\n"; - s << "}\n\n"; + + // ensureReplica() — generated for std mode too when events are + // declared. The body is identical to the Qt version; pulled up + // here so both branches share it. + if (apiStyle == ApiStyle::Std && !events.isEmpty()) { + s << "LogosObject* " << className << "::ensureReplica() {\n"; + s << " if (!m_eventReplica) {\n"; + s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; + s << " if (!replica) {\n"; + s << " qWarning() << \"" << className << ": failed to acquire remote object for events on\" << m_moduleName;\n"; + s << " return nullptr;\n"; + s << " }\n"; + s << " m_eventReplica = replica;\n"; + s << " }\n"; + s << " return m_eventReplica;\n"; + s << "}\n\n"; + } + if (apiStyle == ApiStyle::Qt) { + s << "LogosObject* " << className << "::ensureReplica() {\n"; + s << " if (!m_eventReplica) {\n"; + s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; + s << " if (!replica) {\n"; + s << " qWarning() << \"" << className << ": failed to acquire remote object for events on\" << m_moduleName;\n"; + s << " return nullptr;\n"; + s << " }\n"; + s << " m_eventReplica = replica;\n"; + s << " }\n"; + s << " return m_eventReplica;\n"; + s << "}\n\n"; + s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) {\n"; + s << " if (!callback) {\n"; + s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; + s << " return false;\n"; + s << " }\n"; + s << " LogosObject* origin = ensureReplica();\n"; + s << " if (!origin) {\n"; + s << " return false;\n"; + s << " }\n"; + s << " m_client->onEvent(origin, eventName, callback);\n"; + s << " return true;\n"; + s << "}\n\n"; + s << "bool " << className << "::on(const QString& eventName, EventCallback callback) {\n"; + s << " if (!callback) {\n"; + s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; + s << " return false;\n"; + s << " }\n"; + s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n"; + s << " callback(data);\n"; + s << " });\n"; + s << "}\n\n"; + s << "void " << className << "::setEventSource(LogosObject* source) {\n"; + s << " m_eventSource = source;\n"; + s << "}\n\n"; + s << "LogosObject* " << className << "::eventSource() const {\n"; + s << " return m_eventSource;\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName) {\n"; + s << " trigger(eventName, QVariantList{});\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName, const QVariantList& data) {\n"; + s << " if (!m_eventSource) {\n"; + s << " qWarning() << \"" << className << ": no event source set for trigger\" << eventName;\n"; + s << " return;\n"; + s << " }\n"; + s << " m_client->onEventResponse(m_eventSource, eventName, data);\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n"; + s << " if (!source) {\n"; + s << " qWarning() << \"" << className << ": cannot trigger\" << eventName << \"with null source\";\n"; + s << " return;\n"; + s << " }\n"; + s << " m_client->onEventResponse(source, eventName, data);\n"; + s << "}\n\n"; + } + + // Typed event adapters — one per declared event. The callback type + // uses the apiStyle's type surface; the body unmarshals from the + // wire's QVariantList into typed args and invokes the user's + // callback. Subscription uses the same `m_client->onEvent` channel + // the generic `on(...)` uses. + for (const QJsonValue& ev : events) { + const QJsonObject eo = ev.toObject(); + const QString evName = eo.value("name").toString(); + if (evName.isEmpty()) continue; + // `on` + capitalized event name. `evName` is the verbatim name + // the impl declared in its `logos_events:` block (typically + // camelCase, e.g. `userLoggedIn`), so we just uppercase its + // first letter — `toPascalCase` would clobber the internal + // camelCase boundaries (snake_case input is its target). + QString cap = evName; + if (!cap.isEmpty()) cap[0] = cap[0].toUpper(); + const QString accessorName = QString("on") + cap; + const QJsonArray evParams = eo.value("params").toArray(); + + // Callback signature + QString cbParams; + for (int i = 0; i < evParams.size(); ++i) { + const QJsonObject p = evParams.at(i).toObject(); + QString qtPt = p.value("type").toString(); + QString pt = (apiStyle == ApiStyle::Std) + ? mapParamTypeStd(qtPt) : mapParamType(qtPt); + bool byRef = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt); + if (byRef) cbParams += "const " + pt + "& "; + else cbParams += pt + " "; + cbParams += p.value("name").toString(); + if (i + 1 < evParams.size()) cbParams += ", "; + } + s << "bool " << className << "::" << accessorName + << "(std::function callback) {\n"; + s << " if (!callback) {\n"; + s << " qWarning() << \"" << className << ": ignoring empty event callback for\" " + << "<< QStringLiteral(\"" << evName << "\");\n"; + s << " return false;\n"; + s << " }\n"; + s << " LogosObject* origin = ensureReplica();\n"; + s << " if (!origin) return false;\n"; + s << " m_client->onEvent(origin, QStringLiteral(\"" << evName << "\"), " + << "[callback](const QString&, const QVariantList& _args) {\n"; + s << " if (_args.size() < " << evParams.size() << ") return;\n"; + s << " callback("; + for (int i = 0; i < evParams.size(); ++i) { + const QJsonObject p = evParams.at(i).toObject(); + QString qtPt = p.value("type").toString(); + // Build the QVariant → typed-arg conversion expression. + const QString argExpr = QString("_args.at(%1)").arg(i); + QString conv; + if (apiStyle == ApiStyle::Std) { + conv = qVariantToStdReturn(qtPt, argExpr); + } else { + conv = toQVariantConversion(mapParamType(qtPt), argExpr); + } + s << conv; + if (i + 1 < evParams.size()) s << ", "; + } + s << ");\n"; + s << " });\n"; + s << " return true;\n"; + s << "}\n\n"; + } + for (const QJsonValue& v : methods) { const QJsonObject o = v.toObject(); const bool invokable = o.value("isInvokable").toBool(); if (!invokable) continue; const QString name = o.value("name").toString(); - const QString ret = mapReturnType(o.value("returnType").toString()); + const QString qtRet = o.value("returnType").toString(); + const QString ret = (apiStyle == ApiStyle::Std) + ? mapReturnTypeStd(qtRet) : mapReturnType(qtRet); QJsonArray params = o.value("parameters").toArray(); + + // Helper closures kept inline so the two branches don't get + // pulled apart visually — the per-arg / per-return shape is + // the only thing that varies between Qt and Std modes. + auto emitParam = [&](const QJsonObject& p, bool& byRefOut) { + QString qtPt = p.value("type").toString(); + QString pt = (apiStyle == ApiStyle::Std) + ? mapParamTypeStd(qtPt) : mapParamType(qtPt); + QString pn = p.value("name").toString(); + byRefOut = (apiStyle == ApiStyle::Std) ? isStdRefType(pt) : isQtRefType(pt); + if (byRefOut) s << "const " << pt << "& " << pn; + else s << pt << " " << pn; + }; + auto wireArg = [&](const QJsonObject& p) -> QString { + QString qtPt = p.value("type").toString(); + QString pn = p.value("name").toString(); + return (apiStyle == ApiStyle::Std) ? stdParamToQVariant(qtPt, pn) : pn; + }; + // Signature s << ret << " " << className << "::" << name << "("; for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray" || pt == "QVariantList" || pt == "QVariantMap") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } + bool byRef; + emitParam(params.at(i).toObject(), byRef); if (i + 1 < params.size()) s << ", "; } s << ") {\n"; + // Body: perform call - if (ret != "void") { - s << " QVariant _result = "; - } else { - s << " "; - } + if (ret != "void") s << " QVariant _result = "; + else s << " "; + if (params.size() == 0) { s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\");\n"; - } else if (params.size() == 1) { - QJsonObject p = params.at(0).toObject(); - QString pn = p.value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << pn << ");\n"; - } else if (params.size() == 2) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ");\n"; - } else if (params.size() == 3) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ");\n"; - } else if (params.size() == 4) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - QString p3 = params.at(3).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ");\n"; - } else if (params.size() == 5) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - QString p3 = params.at(3).toObject().value("name").toString(); - QString p4 = params.at(4).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ", " << p4 << ");\n"; + } else if (params.size() <= 5) { + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\""; + for (int i = 0; i < params.size(); ++i) { + s << ", " << wireArg(params.at(i).toObject()); + } + s << ");\n"; } else { s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", QVariantList{"; for (int i = 0; i < params.size(); ++i) { - QString pn = params.at(i).toObject().value("name").toString(); - s << pn; + s << wireArg(params.at(i).toObject()); if (i + 1 < params.size()) s << ", "; } s << "});\n"; } + // Return conversion if (ret == "void") { // nothing + } else if (apiStyle == ApiStyle::Std) { + s << " return " << qVariantToStdReturn(qtRet, "_result") << ";\n"; } else if (ret == "bool") { s << " return _result.toBool();\n"; } else if (ret == "int") { @@ -309,23 +597,18 @@ QString makeSource(const QString& moduleName, const QString& className, const QS s << " return _result.toList();\n"; } else if (ret == "QVariantMap") { s << " return _result.toMap();\n"; - }else if (ret == "LogosResult") { + } else if (ret == "LogosResult") { s << " return _result.value();\n"; } else { // QVariant s << " return _result;\n"; } s << "}\n\n"; + // Async implementation s << "void " << className << "::" << name << "Async("; for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray" || pt == "QVariantList" || pt == "QVariantMap") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } + bool byRef; + emitParam(params.at(i).toObject(), byRef); if (i + 1 < params.size()) s << ", "; } if (params.size() > 0) s << ", "; @@ -334,19 +617,33 @@ QString makeSource(const QString& moduleName, const QString& className, const QS s << " m_client->invokeRemoteMethodAsync(\"" << moduleName << "\", \"" << name << "\", "; if (params.size() == 0) { s << "QVariantList()"; - } else if (params.size() == 1) { - s << "QVariantList() << " << params.at(0).toObject().value("name").toString(); } else { s << "QVariantList{"; for (int i = 0; i < params.size(); ++i) { - s << params.at(i).toObject().value("name").toString(); + s << wireArg(params.at(i).toObject()); if (i + 1 < params.size()) s << ", "; } s << "}"; } s << ", [callback](QVariant v) {\n"; if (ret == "void") { - s << " callback();\n"; + s << " (void)v; callback();\n"; + } else if (apiStyle == ApiStyle::Std) { + // Default-construct on dispatch failure, matching the + // existing Qt code path which falls back to a zero / empty + // value when the QVariant is invalid. + QString defaultVal; + if (ret == "bool") defaultVal = "false"; + else if (ret == "int64_t") defaultVal = "0"; + else if (ret == "double") defaultVal = "0.0"; + else if (ret == "std::string") defaultVal = "std::string()"; + else if (ret.startsWith("std::vector")) defaultVal = ret + "()"; + else if (ret == "LogosMap") defaultVal = "LogosMap::object()"; + else if (ret == "LogosList") defaultVal = "LogosList::array()"; + else if (ret == "StdLogosResult") defaultVal = "StdLogosResult{}"; + else defaultVal = ret + "{}"; + s << " if (!v.isValid()) { callback(" << defaultVal << "); return; }\n"; + s << " callback(" << qVariantToStdReturn(qtRet, "v") << ");\n"; } else { QString defaultVal; if (ret == "bool") defaultVal = "false"; @@ -419,3 +716,4 @@ QVector parseProviderHeader(const QString& headerPath, QTextStream file.close(); return methods; } + diff --git a/cpp-generator/legacy/generator_lib.h b/cpp-generator/legacy/generator_lib.h index 206e88a..34aab0a 100644 --- a/cpp-generator/legacy/generator_lib.h +++ b/cpp-generator/legacy/generator_lib.h @@ -13,13 +13,39 @@ struct ParsedMethod { QVector> params; // (type, name) }; +// Which type surface to expose on the generated per-module wrapper. +// Each module's build picks ONE — there's no composite output. Default +// is Qt for backward compatibility; `interface: "universal"` modules +// flip to Std via the -DLOGOS_API_STYLE=std CMake flag the module +// builder threads through. +enum class ApiStyle { Qt, Std }; + QString toPascalCase(const QString& name); QString normalizeType(QString t); QString mapParamType(const QString& qtType); QString mapReturnType(const QString& qtType); QString toQVariantConversion(const QString& type, const QString& argExpr); -QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods); -QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods); + +// makeHeader / makeSource emit the single `` wrapper for a +// module. When `apiStyle == Std`, parameter / return types come from +// the std-typed mapping table (std::string / std::vector +// / LogosMap / LogosList / int64_t / StdLogosResult) and the .cpp +// body wraps the QVariant wire with inline Qt↔std conversions — +// callers never include Qt headers. When `apiStyle == Qt`, the output +// matches the legacy Qt-typed surface (QString / QStringList / +// QVariantList / QVariantMap / int / LogosResult). The class name is +// always `` either way; the two styles are mutually exclusive. +// +// `events` carries typed event prototypes loaded from a `.lidl` +// sidecar via --events-from. Each entry is +// { "name": "", "params": [ { "name": "...", "type": "" } ] } +// (Qt-typed names — same surface methods come through). When non-empty, +// the wrapper also gets one `on(callback)` accessor per +// event next to the existing generic `onEvent(name, callback)` channel. +// The accessor signature uses the apiStyle's type surface for the +// callback's argument types. +QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events = {}); +QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, ApiStyle apiStyle, const QJsonArray& events = {}); QVector parseProviderHeader(const QString& headerPath, QTextStream& err); #endif // GENERATOR_LIB_H diff --git a/cpp-generator/legacy/main.cpp b/cpp-generator/legacy/main.cpp index 34c6137..7e1808b 100644 --- a/cpp-generator/legacy/main.cpp +++ b/cpp-generator/legacy/main.cpp @@ -16,6 +16,74 @@ #include #include "logos_provider_object.h" #include "generator_lib.h" +#include "../experimental/lidl_parser.h" + +// Convert a TypeExpr → Qt-typed string name (same surface the +// metaobject-introspection path produces for methods, so generator_lib +// can consume both via one code path). +static QString lidlTypeExprToQtTypeName(const TypeExpr& te) +{ + if (te.kind == TypeExpr::Primitive) { + if (te.name == "tstr") return "QString"; + if (te.name == "bstr") return "QByteArray"; + if (te.name == "int") return "int"; + if (te.name == "uint") return "int"; // wire-as-int for now + if (te.name == "float64") return "double"; + if (te.name == "bool") return "bool"; + if (te.name == "result") return "LogosResult"; + if (te.name == "any") return "QVariant"; + return "QVariant"; + } + if (te.kind == TypeExpr::Array && te.elements.size() == 1) { + const TypeExpr& elem = te.elements[0]; + if (elem.kind == TypeExpr::Primitive && elem.name == "tstr") + return "QStringList"; + return "QVariantList"; + } + if (te.kind == TypeExpr::Map) return "QVariantMap"; + if (te.kind == TypeExpr::Optional) return "QVariant"; + if (te.kind == TypeExpr::Named) return "QVariant"; + return "QVariant"; +} + +// Load events from a `.lidl` sidecar shipped alongside a module's +// pre-built headers. Returns a JSON array of +// { name, params: [ { name, type } ] } +// using Qt-typed type names — same shape generator_lib's makeHeader / +// makeSource already consume for methods. +static QJsonArray loadEventsFromLidl(const QString& lidlPath, QTextStream& err) +{ + QJsonArray result; + QFile f(lidlPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + err << "Failed to open events sidecar: " << lidlPath << "\n"; + return result; + } + QString source = QString::fromUtf8(f.readAll()); + f.close(); + + LidlParseResult pr = lidlParse(source); + if (pr.hasError()) { + err << lidlPath << ":" << pr.errorLine << ":" << pr.errorColumn + << ": " << pr.error << "\n"; + return result; + } + + for (const EventDecl& ed : pr.module.events) { + QJsonObject obj; + obj["name"] = ed.name; + QJsonArray params; + for (const ParamDecl& pd : ed.params) { + QJsonObject p; + p["name"] = pd.name; + p["type"] = lidlTypeExprToQtTypeName(pd.type); + params.append(p); + } + obj["params"] = params; + result.append(obj); + } + return result; +} static QJsonArray enumerateMethods(QObject* moduleInstance) { @@ -69,232 +137,37 @@ static QJsonArray enumerateMethods(QObject* moduleInstance) // makeSource -> generator_lib.h/cpp -static QString makeCoreManagerHeader() -{ - QString h; - QTextStream s(&h); - s << "#pragma once\n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \"logos_api.h\"\n"; - s << "#include \"logos_api_client.h\"\n"; - s << "#include \"logos_object.h\"\n\n"; - s << "class CoreManager {\n"; - s << "public:\n"; - s << " explicit CoreManager(LogosAPI* api);\n\n"; - s << " using RawEventCallback = std::function;\n"; - s << " using EventCallback = std::function;\n\n"; - s << " bool on(const QString& eventName, RawEventCallback callback);\n"; - s << " bool on(const QString& eventName, EventCallback callback);\n"; - s << " void setEventSource(LogosObject* source);\n"; - s << " LogosObject* eventSource() const;\n"; - s << " void trigger(const QString& eventName);\n"; - s << " void trigger(const QString& eventName, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, Args&&... args) {\n"; - s << " trigger(eventName, packVariantList(std::forward(args)...));\n"; - s << " }\n"; - s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n"; - s << " trigger(eventName, source, packVariantList(std::forward(args)...));\n"; - s << " }\n\n"; - s << " void initialize(int argc, char* argv[]);\n"; - s << " void setPluginsDirectory(const QString& directory);\n"; - s << " void start();\n"; - s << " void cleanup();\n"; - s << " QStringList getLoadedPlugins();\n"; - s << " QJsonArray getKnownPlugins();\n"; - s << " QJsonArray getPluginMethods(const QString& pluginName);\n"; - s << " void helloWorld();\n"; - s << " bool loadPlugin(const QString& pluginName);\n"; - s << " bool unloadPlugin(const QString& pluginName);\n"; - s << " QString processPlugin(const QString& filePath);\n\n"; - s << "private:\n"; - s << " LogosObject* ensureReplica();\n"; - s << " template\n"; - s << " static QVariantList packVariantList(Args&&... args) {\n"; - s << " QVariantList list;\n"; - s << " list.reserve(sizeof...(Args));\n"; - s << " using Expander = int[];\n"; - s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; - s << " return list;\n"; - s << " }\n"; - s << " LogosAPI* m_api;\n"; - s << " LogosAPIClient* m_client;\n"; - s << " QString m_moduleName;\n"; - s << " LogosObject* m_eventReplica = nullptr;\n"; - s << " LogosObject* m_eventSource = nullptr;\n"; - s << "};\n"; - return h; -} - -static QString makeCoreManagerSource(const QString& headerBaseName) -{ - QString c; - QTextStream s(&c); - s << "#include \"" << headerBaseName << "\"\n\n"; - s << "#include \n"; - s << "#include \n\n"; - s << "CoreManager::CoreManager(LogosAPI* api) : m_api(api), m_client(api->getClient(\"core_manager\")), m_moduleName(QStringLiteral(\"core_manager\")) {}\n\n"; - s << "LogosObject* CoreManager::ensureReplica() {\n"; - s << " if (!m_eventReplica) {\n"; - s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; - s << " if (!replica) {\n"; - s << " qWarning() << \"CoreManager: failed to acquire remote object for events on\" << m_moduleName;\n"; - s << " return nullptr;\n"; - s << " }\n"; - s << " m_eventReplica = replica;\n"; - s << " }\n"; - s << " return m_eventReplica;\n"; - s << "}\n\n"; - s << "bool CoreManager::on(const QString& eventName, RawEventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"CoreManager: ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " LogosObject* origin = ensureReplica();\n"; - s << " if (!origin) {\n"; - s << " return false;\n"; - s << " }\n"; - s << " m_client->onEvent(origin, eventName, callback);\n"; - s << " return true;\n"; - s << "}\n\n"; - s << "bool CoreManager::on(const QString& eventName, EventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"CoreManager: ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n"; - s << " callback(data);\n"; - s << " });\n"; - s << "}\n\n"; - s << "void CoreManager::setEventSource(LogosObject* source) {\n"; - s << " m_eventSource = source;\n"; - s << "}\n\n"; - s << "LogosObject* CoreManager::eventSource() const {\n"; - s << " return m_eventSource;\n"; - s << "}\n\n"; - s << "void CoreManager::trigger(const QString& eventName) {\n"; - s << " trigger(eventName, QVariantList{});\n"; - s << "}\n\n"; - s << "void CoreManager::trigger(const QString& eventName, const QVariantList& data) {\n"; - s << " if (!m_eventSource) {\n"; - s << " qWarning() << \"CoreManager: no event source set for trigger\" << eventName;\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(m_eventSource, eventName, data);\n"; - s << "}\n\n"; - s << "void CoreManager::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n"; - s << " if (!source) {\n"; - s << " qWarning() << \"CoreManager: cannot trigger\" << eventName << \"with null source\";\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(source, eventName, data);\n"; - s << "}\n\n"; - s << "void CoreManager::initialize(int argc, char* argv[]) {\n"; - s << " QStringList args;\n"; - s << " if (argv) {\n"; - s << " for (int i = 0; i < argc; ++i) {\n"; - s << " args << QString::fromUtf8(argv[i] ? argv[i] : \"\");\n"; - s << " }\n"; - s << " }\n"; - s << " m_client->invokeRemoteMethod(\"core_manager\", \"initialize\", argc, args);\n"; - s << "}\n\n"; - s << "void CoreManager::setPluginsDirectory(const QString& directory) {\n"; - s << " m_client->invokeRemoteMethod(\"core_manager\", \"setPluginsDirectory\", directory);\n"; - s << "}\n\n"; - s << "void CoreManager::start() {\n"; - s << " m_client->invokeRemoteMethod(\"core_manager\", \"start\");\n"; - s << "}\n\n"; - s << "void CoreManager::cleanup() {\n"; - s << " m_client->invokeRemoteMethod(\"core_manager\", \"cleanup\");\n"; - s << "}\n\n"; - s << "QStringList CoreManager::getLoadedPlugins() {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getLoadedPlugins\");\n"; - s << " return _result.toStringList();\n"; - s << "}\n\n"; - s << "QJsonArray CoreManager::getKnownPlugins() {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getKnownPlugins\");\n"; - s << " return qvariant_cast(_result);\n"; - s << "}\n\n"; - s << "QJsonArray CoreManager::getPluginMethods(const QString& pluginName) {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getPluginMethods\", pluginName);\n"; - s << " return qvariant_cast(_result);\n"; - s << "}\n\n"; - s << "void CoreManager::helloWorld() {\n"; - s << " m_client->invokeRemoteMethod(\"core_manager\", \"helloWorld\");\n"; - s << "}\n\n"; - s << "bool CoreManager::loadPlugin(const QString& pluginName) {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"loadPlugin\", pluginName);\n"; - s << " return _result.toBool();\n"; - s << "}\n\n"; - s << "bool CoreManager::unloadPlugin(const QString& pluginName) {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"unloadPlugin\", pluginName);\n"; - s << " return _result.toBool();\n"; - s << "}\n\n"; - s << "QString CoreManager::processPlugin(const QString& filePath) {\n"; - s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"processPlugin\", filePath);\n"; - s << " return _result.toString();\n"; - s << "}\n\n"; - return c; -} - -static bool ensureCoreManagerWrapper(const QString& genDirPath, QTextStream& err) -{ - const QString headerRel = QStringLiteral("core_manager_api.h"); - const QString sourceRel = QStringLiteral("core_manager_api.cpp"); - const QString headerAbs = QDir(genDirPath).filePath(headerRel); - const QString sourceAbs = QDir(genDirPath).filePath(sourceRel); - - QString header = makeCoreManagerHeader(); - QString source = makeCoreManagerSource(headerRel); - - QFile headerFile(headerAbs); - if (!headerFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write core manager header: " << headerAbs << "\n"; - return false; - } - headerFile.write(header.toUtf8()); - headerFile.close(); - - QFile sourceFile(sourceAbs); - if (!sourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write core manager source: " << sourceAbs << "\n"; - return false; - } - sourceFile.write(source.toUtf8()); - sourceFile.close(); - - return true; -} - static bool writeUmbrellaHeader(const QString& genDirPath, QTextStream& err) { - // Generate logos-cpp-sdk/cpp/generated/logos_sdk.h that includes all *_api.h in this dir + // Generate logos_sdk.h: include every per-module wrapper header in + // the gen dir and aggregate them into a flat `LogosModules` struct. + // The wrappers may be Qt-typed or Std-typed depending on the + // --api-style picked for this build; the umbrella shape doesn't + // change because either flavor produces the same accessor name + // (``) on the same class name (``). + // + // `core_manager_api.h` (if present in the gen dir from an older + // run) is intentionally filtered out — universal modules access + // only the deps they explicitly declared in `metadata.json# + // dependencies`. Apps that need to manage the core use the C API + // in liblogos directly, not the typed `LogosModules` aggregate. QDir genDir(genDirPath); QStringList headers = genDir.entryList(QStringList() << "*_api.h", QDir::Files | QDir::Readable); + headers.removeAll(QStringLiteral("core_manager_api.h")); + QString content; QTextStream s(&content); s << "#pragma once\n"; s << "#include \"logos_api.h\"\n"; s << "#include \"logos_api_client.h\"\n\n"; - // Includes - for (const QString& h : headers) { - s << "#include \"" << h << "\"\n"; - } + for (const QString& h : headers) s << "#include \"" << h << "\"\n"; s << "\n"; - // Convenience aggregator exposing module wrappers + s << "struct LogosModules {\n"; s << " explicit LogosModules(LogosAPI* api) : api(api)"; for (const QString& h : headers) { QString base = h; base.chop(QString("_api.h").size()); - QString className = toPascalCase(base); s << ", \n " << base << "(api)"; } s << " {}\n"; @@ -319,24 +192,31 @@ static bool writeUmbrellaHeader(const QString& genDirPath, QTextStream& err) static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonArray& deps, QTextStream& err) { - // Generate logos-cpp-sdk/cpp/generated/logos_sdk.h based on dependencies list + // Generate logos_sdk.h from metadata.json's dependencies list. The + // shape doesn't depend on apiStyle — each dep emits a single + // `_api.h` whose class signature shape was already decided + // at codegen time. The umbrella just `#include`s and aggregates + // each wrapper into the flat `LogosModules` struct. + // + // Only the modules explicitly listed in `metadata.json# + // dependencies` are exposed. Apps that need to manage the core + // (basecamp, logoscore) use liblogos' C API directly rather than + // the typed `LogosModules` aggregate. QDir genDir(genDirPath); QString content; QTextStream s(&content); s << "#pragma once\n"; s << "#include \"logos_api.h\"\n"; s << "#include \"logos_api_client.h\"\n\n"; - // Includes - s << "#include \"core_manager_api.h\"\n"; for (const QJsonValue& v : deps) { if (!v.isString()) continue; QString depName = v.toString(); s << "#include \"" << depName << "_api.h\"\n"; } s << "\n"; - // Convenience aggregator exposing module wrappers + s << "struct LogosModules {\n"; - s << " explicit LogosModules(LogosAPI* api) : api(api), \n core_manager(api)"; + s << " explicit LogosModules(LogosAPI* api) : api(api)"; for (const QJsonValue& v : deps) { if (!v.isString()) continue; QString depName = v.toString(); @@ -344,7 +224,6 @@ static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonAr } s << " {}\n"; s << " LogosAPI* api;\n"; - s << " CoreManager core_manager;\n"; for (const QJsonValue& v : deps) { if (!v.isString()) continue; QString depName = v.toString(); @@ -365,15 +244,23 @@ static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonAr static bool writeUmbrellaSource(const QString& genDirPath, QTextStream& err) { - // Generate logos-cpp-sdk/cpp/generated/logos_sdk.cpp that includes all *_api.cpp in this dir + // Generate logos_sdk.cpp: one #include per per-module wrapper + // `.cpp` in the gen dir. There's now exactly one wrapper file per + // module (Qt or std, picked at generation time), so no de-dup or + // twin-file filtering is needed. + // + // `core_manager_api.cpp` (if present from an older run) is + // filtered out — the umbrella header no longer declares + // `CoreManager core_manager;` so including its definitions would + // produce dead code. QDir genDir(genDirPath); QStringList sources = genDir.entryList(QStringList() << "*_api.cpp", QDir::Files | QDir::Readable); + sources.removeAll(QStringLiteral("core_manager_api.cpp")); + QString content; QTextStream s(&content); s << "#include \"logos_sdk.h\"\n\n"; - for (const QString& c : sources) { - s << "#include \"" << c << "\"\n"; - } + for (const QString& c : sources) s << "#include \"" << c << "\"\n"; s << "\n"; QFile outFile(genDir.filePath("logos_sdk.cpp")); @@ -388,12 +275,13 @@ static bool writeUmbrellaSource(const QString& genDirPath, QTextStream& err) static bool writeUmbrellaSourceFromDeps(const QString& genDirPath, const QJsonArray& deps, QTextStream& err) { - // Generate logos-cpp-sdk/cpp/generated/logos_sdk.cpp based on dependencies list + // Generate logos_sdk.cpp from metadata.json's dependencies list. + // Each dep emits one wrapper `.cpp` (Qt or std — decided at codegen + // time, file name is the same either way), `#include`'d here. QDir genDir(genDirPath); QString content; QTextStream s(&content); s << "#include \"logos_sdk.h\"\n\n"; - s << "#include \"core_manager_api.cpp\"\n"; for (const QJsonValue& v : deps) { if (!v.isString()) continue; QString depName = v.toString(); @@ -557,7 +445,7 @@ static int generateProviderDispatch(const QString& headerPath, const QString& ou return 0; } -static int generateFromPlugin(const QString& pluginInputPath, const QString& outputDir, bool moduleOnly, QTextStream& out, QTextStream& err) +static int generateFromPlugin(const QString& pluginInputPath, const QString& outputDir, bool moduleOnly, ApiStyle apiStyle, const QJsonArray& events, QTextStream& out, QTextStream& err) { QFileInfo fi(pluginInputPath); if (!fi.exists()) { @@ -572,9 +460,6 @@ static int generateFromPlugin(const QString& pluginInputPath, const QString& out QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir; QDir().mkpath(genDirPath); - if (!moduleOnly && !ensureCoreManagerWrapper(genDirPath, err)) { - return 9; - } QPluginLoader loader(resolvedPath); if (!loader.load()) { @@ -620,8 +505,16 @@ static int generateFromPlugin(const QString& pluginInputPath, const QString& out QString headerAbs = QDir(genDirPath).filePath(headerRel); QString sourceAbs = QDir(genDirPath).filePath(sourceRel); - QString header = makeHeader(moduleName, className, methods); - QString source = makeSource(moduleName, className, headerRel, methods); + // Single per-module wrapper file pair. apiStyle decides the + // signature shape: Qt-typed for legacy / handcrafted callers + // (default), std-typed when the consuming module's build passed + // --api-style=std (typically because it's `interface: "universal"`). + // Both produce the same filename and class name, so the umbrella + // doesn't need to know which style was picked. `events` (loaded + // from a sibling `.lidl` sidecar via --events-from) adds typed + // `on(callback)` accessors next to the existing methods. + QString header = makeHeader(moduleName, className, methods, apiStyle, events); + QString source = makeSource(moduleName, className, headerRel, methods, apiStyle, events); { QFile f(headerAbs); @@ -689,6 +582,36 @@ int legacy_main(int argc, char* argv[]) // Parse --general-only option bool generalOnly = args.contains("--general-only"); + // Parse --api-style option (qt | std). Picks which type surface + // the generated `` wrapper exposes. Default is qt for + // backward compatibility — every existing module that doesn't + // declare `interface: "universal"` in its metadata.json keeps + // its Qt-typed LogosModules surface. Universal modules get + // -DLOGOS_API_STYLE=std threaded through by mkLogosModule.nix / + // LogosModule.cmake, which becomes `--api-style=std` here. + // Both forms accepted: `--api-style std` and `--api-style=std`. + ApiStyle apiStyle = ApiStyle::Qt; + { + QString apiVal; + for (int i = 0; i < args.size(); ++i) { + const QString& a = args.at(i); + if (a == "--api-style") { + if (i + 1 < args.size()) apiVal = args.at(i + 1); + break; + } + if (a.startsWith("--api-style=")) { + apiVal = a.section('=', 1); + break; + } + } + if (apiVal == "std") apiStyle = ApiStyle::Std; + else if (!apiVal.isEmpty() && apiVal != "qt") { + err << "Unknown --api-style value: " << apiVal + << " (expected 'qt' or 'std')\n"; + return 1; + } + } + // Support: extract dependencies from a metadata.json file { const int metaIdx = args.indexOf("--metadata"); @@ -726,16 +649,14 @@ int legacy_main(int argc, char* argv[]) const QJsonObject obj = doc.object(); const QJsonArray deps = obj.value("dependencies").toArray(); - // If --general-only provided, generate only core manager and umbrella files + // If --general-only provided, generate only the umbrella files. + // `LogosModules` exposes ONLY the modules listed in + // `metadata.json#dependencies` — apps that need to manage the + // core use liblogos' C API directly. if (generalOnly) { QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir; QDir().mkpath(genDirPath); - - // Generate core manager wrapper - if (!ensureCoreManagerWrapper(genDirPath, err)) { - return 9; - } - + // Generate umbrella headers based on dependencies from metadata if (!writeUmbrellaHeaderFromDeps(genDirPath, deps, err)) { return 7; @@ -743,8 +664,8 @@ int legacy_main(int argc, char* argv[]) if (!writeUmbrellaSourceFromDeps(genDirPath, deps, err)) { return 8; } - - out << "Generated core_manager_api.h, core_manager_api.cpp, logos_sdk.h, and logos_sdk.cpp\n"; + + out << "Generated logos_sdk.h and logos_sdk.cpp\n"; out.flush(); return 0; } @@ -768,9 +689,6 @@ int legacy_main(int argc, char* argv[]) QString genDirPath = outputDir.isEmpty() ? QDir::current().filePath("logos-cpp-sdk/cpp/generated") : outputDir; QDir().mkpath(genDirPath); - if (!moduleOnly && !ensureCoreManagerWrapper(genDirPath, err)) { - return 9; - } QString suffix; #if defined(Q_OS_MACOS) @@ -794,7 +712,11 @@ int legacy_main(int argc, char* argv[]) continue; } out << "Running generator for dependency plugin: " << pluginPath << "\n"; - const int st = generateFromPlugin(pluginPath, outputDir, moduleOnly, out, err); + // No --events-from sidecar in the multi-dep iteration + // path (each dep would need its own sidecar — out of + // scope here; --events-from is consumed by the + // per-plugin path below, invoked from buildHeaders.nix). + const int st = generateFromPlugin(pluginPath, outputDir, moduleOnly, apiStyle, QJsonArray(), out, err); if (st != 0) { overallStatus = st; // remember last non-zero } @@ -834,13 +756,37 @@ int legacy_main(int argc, char* argv[]) } if (args.size() < 2) { - err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " /absolute/path/to/plugin [--output-dir /path/to/output] [--module-only]\n"; + err << "Usage: " << QFileInfo(app.applicationFilePath()).fileName() << " /absolute/path/to/plugin [--output-dir /path/to/output] [--module-only] [--events-from /path/to/.lidl]\n"; err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /absolute/path/to/metadata.json [--output-dir /path/to/output] [--module-only] [--general-only]\n"; err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --metadata /absolute/path/to/metadata.json --general-only [--output-dir /path/to/output]\n"; err << " or: " << QFileInfo(app.applicationFilePath()).fileName() << " --provider-header /path/to/impl.h [--output-dir /path/to/output]\n"; return 1; } + // --events-from : load typed event prototypes from a LIDL + // sidecar shipped alongside a dep's pre-built headers. When set, + // the consumer wrapper (_api.{h,cpp}) gains typed + // `on(callback)` accessors next to the existing + // generic `onEvent(name, callback)` channel. + QJsonArray eventsFromSidecar; + { + const int evIdx = args.indexOf("--events-from"); + QString evPath; + if (evIdx != -1 && evIdx + 1 < args.size()) { + evPath = args.at(evIdx + 1); + } else { + for (const QString& a : args) { + if (a.startsWith("--events-from=")) { + evPath = a.section('=', 1); + break; + } + } + } + if (!evPath.isEmpty() && QFileInfo(evPath).exists()) { + eventsFromSidecar = loadEventsFromLidl(evPath, err); + } + } + QString argPath = args.at(1); - return generateFromPlugin(argPath, outputDir, moduleOnly, out, err); + return generateFromPlugin(argPath, outputDir, moduleOnly, apiStyle, eventsFromSidecar, out, err); } diff --git a/cpp-generator/main.cpp b/cpp-generator/main.cpp index 04b8363..925fcf4 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_serializer.h" #include "experimental/impl_header_parser.h" #include @@ -110,6 +111,36 @@ int main(int argc, char* argv[]) 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; } diff --git a/cpp/logos_module_context.h b/cpp/logos_module_context.h index 1c7c308..59c5a26 100644 --- a/cpp/logos_module_context.h +++ b/cpp/logos_module_context.h @@ -1,10 +1,32 @@ #ifndef LOGOS_MODULE_CONTEXT_H #define LOGOS_MODULE_CONTEXT_H +#include #include #include #include +// --------------------------------------------------------------------------- +// `logos_events:` — Qt-`signals:`-style declaration of events the module +// can emit. The macro expands to `public:` so the compiler treats the +// declarations as ordinary public-method signatures (never called from +// outside the impl, but harmless to leave public — mirrors Qt's +// `#define signals public`). The cpp-generator's impl_header_parser +// recognises the raw `logos_events:` token before preprocessing and +// emits typed method bodies for each declaration in a sidecar +// `_events.cpp` that calls `emitEventImpl_()` underneath. +// +// class MyImpl : public LogosModuleContext { +// public: +// void doStuff() { userLoggedIn("alice", 12345); } // typed emit +// logos_events: +// void userLoggedIn(const std::string& userId, int64_t timestamp); +// }; +// --------------------------------------------------------------------------- +#ifndef logos_events +#define logos_events public +#endif + // --------------------------------------------------------------------------- // LogosModuleContext — opt-in mixin for codegen-generated modules // @@ -43,6 +65,17 @@ // emitted. // --------------------------------------------------------------------------- +// Per-module aggregate of dependency wrappers. Each module's codegen +// emits `struct LogosModules { ... };` at global scope in its own +// `generated_code/logos_sdk.h` (one accessor per `metadata.json# +// dependencies` entry — nothing else; apps that need to manage the +// core itself reach for liblogos' C API instead). Forward-declared +// here so the SDK header stays decoupled from per-module codegen — +// the impl's translation unit makes the type complete via its own +// `#include "logos_sdk.h"`, at which point the inline +// `LogosModuleContext::modules()` body below compiles. +struct LogosModules; + class LogosModuleContext { public: virtual ~LogosModuleContext() = default; @@ -64,30 +97,39 @@ public: // impl directly), so always null-check before using. const std::string& instancePersistencePath() const { return m_instancePersistencePath; } + // True once the framework has populated the three getters above. + // Flipped inside `_logosCoreSetContext_` *before* the + // `onContextReady()` hook fires, so derived impls can use this as + // a guard from helper methods that may run earlier in the impl's + // life (e.g. during construction in tests that bypass the + // framework). Stays false when the impl is constructed outside a + // framework-provisioned context, matching the empty-string + // fallback for the path getters. + bool isContextReady() const { return m_contextReady; } + // Typed access to this module's per-build `LogosModules` aggregate, // which the codegen emits in `generated_code/logos_sdk.h`. It owns // one strongly-typed client wrapper per entry in `metadata.json`'s - // `dependencies` list (plus an always-present `core_manager`), so - // an impl can call other modules' methods without ever touching - // the raw `LogosAPI`: + // `dependencies` list — nothing else. An impl can call those + // declared deps' methods without ever touching the raw `LogosAPI`: // // #include "logos_sdk.h" // generated at build time // // void MyModuleImpl::doWork() { - // logos().some_dep.someMethod(arg); + // modules().some_dep.someMethod(arg); // } // // The pointer is set by the codegen-generated provider's `onInit`, // which constructs the `LogosModules` from the `LogosAPI`. The - // type parameter is required because the SDK header is shared by - // every module and can't know each module's specific aggregator - // type (its accessor names come straight from that module's - // declared dependencies). Returns a reference; calling before the - // framework has populated the pointer (e.g. from a unit test - // bypassing the provider) is undefined. - template - Modules& logos() const { - return *static_cast(m_logosModulesPtr); + // return type is forward-declared above so this header stays + // decoupled from per-module codegen; call sites need to have + // `logos_sdk.h` included (which defines `LogosModules` as a + // concrete `struct` in their translation unit) for the inline + // body below to compile. Calling before the framework has + // populated the pointer (e.g. from a unit test bypassing the + // provider) is undefined. + LogosModules& modules() const { + return *static_cast(m_logosModulesPtr); } // Framework-only entry point — invoked by the generated provider's @@ -102,6 +144,10 @@ public: m_modulePath = std::move(modulePath); m_instanceId = std::move(instanceId); m_instancePersistencePath = std::move(instancePersistencePath); + // Flip BEFORE invoking the hook so derived impls' onContextReady + // overrides — and anything they call out to — see a "true" + // isContextReady() as expected. + m_contextReady = true; onContextReady(); } @@ -113,6 +159,30 @@ public: m_logosModulesPtr = ptr; } + // Framework-only — installs the callback that the codegen-emitted + // bodies of `logos_events:` methods invoke. The `void*` carries a + // `QVariantList*` constructed inside the .cpp; keeping the + // signature Qt-free here lets impl headers stay pure C++. The + // codegen-emitted provider plugs in a lambda that casts the + // pointer back to QVariantList and forwards through + // `LogosProviderBase::emitEvent(QString, QVariantList)`. + void _logosCoreSetEmitEvent_(std::function cb) { + m_emitEventCallback = std::move(cb); + } + +protected: + // Invoked from `_events.cpp` (codegen-emitted method bodies) + // to dispatch a typed event. `args` is the address of a stack- + // local `QVariantList` constructed by the generated body; the + // callback the provider installs casts it back and forwards. + // No-op when called outside a framework context (the callback + // stays default-constructed and empty) — same fallback shape as + // the property getters above. + void emitEventImpl_(const std::string& eventName, void* args) const { + if (m_emitEventCallback) + m_emitEventCallback(eventName, args); + } + protected: // Hook for derived impls. Fires exactly once, after the three // getters above become readable, before any method dispatch. The @@ -126,12 +196,20 @@ private: std::string m_modulePath; std::string m_instanceId; std::string m_instancePersistencePath; + // Tracks whether the framework has called `_logosCoreSetContext_` + // at least once. Read by `isContextReady()`. + bool m_contextReady = false; // Type-erased so the SDK header doesn't need the per-module // LogosModules definition. Reinterpreted via the typed `logos()` // accessor above. Stays null when the impl is constructed outside // a framework-provisioned context (e.g. lgpd CLI / unit tests), // matching the empty-string fallback for the other getters. void* m_logosModulesPtr = nullptr; + // Set by the codegen-generated provider in onInit() via the SFINAE'd + // _logos_codegen_::maybeSetEmitEvent helper below. Default-empty + // when the impl is constructed outside a framework-provisioned + // context, in which case `emitEventImpl_` becomes a no-op. + std::function m_emitEventCallback; }; // --------------------------------------------------------------------------- @@ -192,6 +270,24 @@ inline auto maybeSetLogosModules(T&, void*) // Module impl didn't opt into LogosModuleContext; nothing to do. } +// Sets the typed-event callback that codegen-emitted `_events.cpp` +// bodies dispatch through. Same tag-dispatch trick as the two above — +// impls that don't inherit LogosModuleContext fall through to the no-op +// overload and compile unchanged. +template +inline auto maybeSetEmitEvent(T& impl, std::function cb) + -> std::enable_if_t> +{ + static_cast(impl)._logosCoreSetEmitEvent_(std::move(cb)); +} + +template +inline auto maybeSetEmitEvent(T&, std::function) + -> std::enable_if_t> +{ + // Module impl didn't opt into LogosModuleContext; nothing to do. +} + } // namespace _logos_codegen_ #endif // LOGOS_MODULE_CONTEXT_H diff --git a/tests/sdk/test_logos_module_context.cpp b/tests/sdk/test_logos_module_context.cpp index ef5d95c..e69a951 100644 --- a/tests/sdk/test_logos_module_context.cpp +++ b/tests/sdk/test_logos_module_context.cpp @@ -6,8 +6,11 @@ // // 1. The opt-in base class itself: getters default-empty, the // framework setter populates them in one shot and fires -// onContextReady() exactly once, the typed `logos()` accessor -// reinterprets the stored void* correctly. +// onContextReady() exactly once, the typed `modules()` accessor +// reinterprets the stored void* correctly against a translation- +// unit-local `struct LogosModules` (mirroring how each real +// module's codegen-emitted `logos_sdk.h` defines that struct +// at global scope). // // 2. The SFINAE-dispatched helpers (`_logos_codegen_::maybeSetContext` // and `maybeSetLogosModules`): the inheriting overload writes @@ -28,6 +31,16 @@ #include #include +// Stand-in for a module-specific `LogosModules` struct. The real one +// is generated per-module in `generated_code/logos_sdk.h` at global +// scope (one accessor per dep). Defined here at global scope so it +// matches the forward declaration in `logos_module_context.h` and +// makes the inline `modules()` accessor compile against this TU's +// translation-unit-local definition. +struct LogosModules { + int sentinel = 42; +}; + namespace { // Impl that opts in to the context. Tracks whether onContextReady fired @@ -50,13 +63,6 @@ protected: } }; -// Stand-in for a module-specific `LogosModules` struct. The real one -// is generated per-module (one accessor per dep) — for the SDK-side -// pointer plumbing we only need a type whose address we can compare. -struct FakeLogosModules { - int sentinel = 42; -}; - // Impl that intentionally does NOT inherit LogosModuleContext, used // to verify that the SFINAE helpers compile (and silently no-op) // against arbitrary impl types. If the helpers ever regress to a @@ -121,14 +127,17 @@ TEST(LogosModuleContextTest, SetContextCalledTwiceFiresHookTwice) TEST(LogosModuleContextTest, SetLogosModulesPtrAndTypedAccessor) { ContextInherit ctx; - FakeLogosModules modules; - modules.sentinel = 7; + LogosModules mods; + mods.sentinel = 7; // Type-erased framework write… - ctx._logosCoreSetLogosModulesPtr_(&modules); - // …typed read on the way out. - FakeLogosModules& got = ctx.logos(); - EXPECT_EQ(&got, &modules); + ctx._logosCoreSetLogosModulesPtr_(&mods); + // …typed read on the way out via the non-template `modules()` + // accessor. The accessor's inline body static_casts the stored + // void* to this TU's `struct LogosModules` (forward-declared in + // logos_module_context.h, defined at file scope above). + LogosModules& got = ctx.modules(); + EXPECT_EQ(&got, &mods); EXPECT_EQ(got.sentinel, 7); // Pointer aliasing: mutating through the original object is @@ -136,8 +145,8 @@ TEST(LogosModuleContextTest, SetLogosModulesPtrAndTypedAccessor) // copy). Demonstrates the generator's lifetime model — the // provider owns the LogosModules and the context holds a non- // owning pointer for the impl's lifetime. - modules.sentinel = 99; - EXPECT_EQ(ctx.logos().sentinel, 99); + mods.sentinel = 99; + EXPECT_EQ(ctx.modules().sentinel, 99); } TEST(LogosModuleContextTest, LogosModulesPointerIndependentOfContext) @@ -148,12 +157,12 @@ TEST(LogosModuleContextTest, LogosModulesPointerIndependentOfContext) // don't want a future re-ordering or refactor to silently couple // them. ContextInherit ctx; - FakeLogosModules modules; + LogosModules mods; - ctx._logosCoreSetLogosModulesPtr_(&modules); + ctx._logosCoreSetLogosModulesPtr_(&mods); ctx._logosCoreSetContext_("/m", "id", "/p"); - EXPECT_EQ(&ctx.logos(), &modules); + EXPECT_EQ(&ctx.modules(), &mods); EXPECT_EQ(ctx.modulePath(), "/m"); } @@ -184,17 +193,17 @@ TEST(LogosModuleContextHelpersTest, MaybeSetContextNoOpForNonInheritingImpl) TEST(LogosModuleContextHelpersTest, MaybeSetLogosModulesWritesForInheritingImpl) { ContextInherit impl; - FakeLogosModules modules; - _logos_codegen_::maybeSetLogosModules(impl, &modules); - EXPECT_EQ(&impl.logos(), &modules); + LogosModules mods; + _logos_codegen_::maybeSetLogosModules(impl, &mods); + EXPECT_EQ(&impl.modules(), &mods); } TEST(LogosModuleContextHelpersTest, MaybeSetLogosModulesNoOpForNonInheritingImpl) { NonInheritingImpl impl; - FakeLogosModules modules; + LogosModules mods; // Same compile-time test as above, separate helper. - _logos_codegen_::maybeSetLogosModules(impl, &modules); + _logos_codegen_::maybeSetLogosModules(impl, &mods); EXPECT_EQ(impl.touched, 0); }