logos-qt-sdk#23 deleted that function -- it was a second, parallel top-level pipeline over the same emitters main.cpp already drives, with no callers anywhere in the workspace. This doc bullet was its only surviving reference. The .lidl sidecar the bullet described is emitted elsewhere and is unaffected: main.cpp:92/:184 here, and module-builder's modulePreConfigure.nix:75. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Logos Code Generator — Project Description
Project Structure
cpp-generator/
├── main.cpp # Entry point — dispatches to legacy or experimental
├── CMakeLists.txt # Build config
├── compile.sh # Standalone build script
├── legacy/ # Original generator (unchanged from master)
│ ├── main.cpp # legacy_main() — plugin/metadata/provider-header modes
│ ├── generator_lib.h/cpp # Shared utilities, type mapping, header parser
│ └── legacy_main.h # Forward declaration
├── experimental/ # C++/Qt-specific generator backends
│ ├── lidl_compat.h # Bridges the backends onto logos-lidl's std AST
│ ├── lidl_emit_common.h/cpp # LIDL type → Qt/std type-name mapping
│ ├── lidl_gen_client.h/cpp # Typed client stub generation (+ Doxygen /// docs)
│ ├── lidl_gen_cdylib.h/cpp # cdylib module-impl C-ABI export generation
│ └── impl_header_parser.h/cpp # C++ impl header → lidl::ModuleDecl
│ # The lexer/parser/AST/serializer/validator now live in the standalone
│ # logos-lidl repo (linked via find_package(logos-lidl)); the Qt glue
│ # emitters live in logos-qt-sdk's logos-qt-generator.
└── docs/ # This documentation
Components
Entry Point (main.cpp)
Checks for --from-header or --lidl flags before creating QCoreApplication. If neither is present, falls through to legacy_main().
LIDL frontend — logos-lidl (consumed as a library)
The lexer, parser, AST, serializer, and validator are no longer embedded here — they live in the standalone logos-lidl repo, the language-neutral (Qt-free) common frontend every Logos SDK shares (C++ here, Rust in logos-rust-sdk, …). cpp-generator links it via find_package(logos-lidl) and reaches it through experimental/lidl_compat.h.
logos-lidl exposes (namespace lidl):
lidl::parse(std::string) → ParseResult(ModuleDecl+ error/line/column)lidl::serialize(ModuleDecl) → std::stringlidl::validate(ModuleDecl) → ValidationResult- the AST:
TypeExpr(Kind: Primitive/Array/Map/Optional/Named,name,elements),ParamDecl,FieldDecl,MethodDecl(name, params, returnType,description,jsonReturn,resultReturn),EventDecl(name, params,description),TypeDecl,ModuleDecl. (logos-lidl also exposes an AST↔JSON bridge and a C ABI that the Rust SDK consumes over FFI — not used by this generator.)
The .lidl grammar (defined in logos-lidl):
module = "module" IDENT "{" body "}"
body = (metadata | type_def | method_def | event_def)*
metadata = "version" STRING | "description" STRING | "category" STRING
| "depends" "[" (IDENT ("," IDENT)*)? "]"
type_def = "type" IDENT "{" field* "}"
field = "?"? IDENT ":" type_expr
method_def = "method" IDENT "(" params ")" "->" type_expr ("description" STRING)?
event_def = "event" IDENT "(" params ")" ("description" STRING)?
params = (IDENT ":" type_expr ("," IDENT ":" type_expr)*)?
type_expr = IDENT | "[" type_expr "]" | "{" type_expr ":" type_expr "}"
| "?" type_expr
Validation (in logos-lidl) checks: empty module name, duplicate type/method/event names, builtin type shadowing, unknown named type references, duplicate parameter names. Serialization round-trips ModuleDecl back to .lidl text (incl. the trailing description "…" clause).
Compat shim (lidl_compat.h)
Bridges the existing Qt-flavored backends onto logos-lidl's std AST so they compile unchanged:
- brings the
lidl::AST types into the global scope the backends use (viausing) qs(std::string) → QStringplus aQTextStream << std::stringoverload, so emission of AST string fields just works- name-compatible shims
lidlParse/lidlSerialize/lidlValidateoverlidl::parse/serialize/validate
Type Mapping (lidl_emit_common.h/cpp)
lidlTypeToQt(TypeExpr)/lidlTypeToStd(TypeExpr)— LIDL type → Qt / std type-name stringslidlIsStdConvertible(TypeExpr)— whether a type has a pure-C++ (Qt-free) representationlidlToPascalCase(name)— convertssnake_casetoPascalCase
Client stubs (lidl_gen_client.h/cpp)
lidlMakeHeader(ModuleDecl)/lidlMakeSource(ModuleDecl)— typed<Module>client wrapper; each method (and its…Asynctwin) carries a Doxygen///comment generated from the method'sdescriptionlidlGenerateMetadataJson(ModuleDecl)— generates metadata.json content
cdylib backend (lidl_gen_cdylib.h/cpp)
Emits the Qt-free half of a universal C++ cdylib module:
lidlCdylibSupported(ModuleDecl)— gate to the std-convertible (Qt-free) type subsetlidlMakeModuleImplExports(...)— thelogos_module_impl.hC-ABI export wrapper around the universal impl class (compiled into the module's cdylib; dispatches via nlohmann::json)lidlMakeEventsSourceCdylib(...)— typedlogos_events:bodies marshalling into nlohmann::json
Per-build API-style choice (legacy/generator_lib.{h,cpp})
The codegen exposes one wrapper class per module — <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 |
lp |
std::string / std::vector<std::string> / LogosMap / LogosList / int64_t / StdLogosResult, over the Qt-free logos-protocol C ABI |
(A third value, std — std signatures over a QVariant / LogosAPIClient body — was retired; the generator now rejects --api-style=std instead of aliasing it.)
Both styles emit:
- A
<Module>client class with sync method shapes + matching<method>Async(...)overloads. - The std variant additionally inlines Qt↔std conversion in its
.cppso 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:
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, Lp }— passed to every wrapper-emitting function.- File-local
mapParamTypeStd/mapReturnTypeStd— the std-side type-mapping table thelpsurface exposes. Hidden fromgenerator_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 onapiStyleinternally to emit the right include block, signature shape, and conversion bridges.eventsis loaded from a<name>.lidlsidecar via--events-from; when non-empty, the wrapper also gets one typedon<EventName>(callback)adapter per declared event (callback arg types followapiStyle).
Flag plumbing:
metadata.json#interface == "universal"(or"cdylib") →mkLogosModule.nixadds-DLOGOS_API_STYLE=lptoextraCmakeFlags. Anything else ("legacy","provider", absent) leaves the defaultqt.LogosModule.cmakereads${LOGOS_API_STYLE}(defaultqt) and forwards--api-style=${LOGOS_API_STYLE}to thelogos-cpp-generator --general-onlyinvocation that writes the umbrella. Each module's Nix build emits two header derivations (<name>.headers-qtand<name>.headers-lp) viabuildHeaders.nix— onelogos-cpp-generator --api-style=…run per style, at the dep's build time. A consumer'sbuildPlugin.nixpicksdep.headers-${apiStyle}and copies itsinclude/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.legacy/main.cppparses--api-styleonce (rejecting the retiredstd) and threads the resultingApiStylethroughgenerateFromPlugin,writeUmbrellaHeader{,FromDeps}. No per-style filenames are ever emitted; each module gets a single<name>_api.h+<name>_api.cpppair regardless of style.
Provider Generation (logos-qt-generator)
The Qt provider glue (
lidl_gen_provider.{h,cpp}) is emitted by logos-qt-sdk'slogos-qt-generator, not this binary — it consumes the samelogos-lidlfrontend (+ the sharedlidl_emit_common/impl_header_parser/lidl_compat.hhelpers, distributed undershare/lidl-frontend). Documented here for reference.
lidlMakeProviderHeader(ModuleDecl, implClass, implHeader)— generates Qt glue header- Emits
nlohmannToQVariant()helper when any method hasjsonReturn = true - Always emits an
onInit(LogosAPI*) overridethat, via SFINAE'd helpers inlogos_module_context.h, (a) copies the three runtime-injected properties (modulePath,instanceId,instancePersistencePath) into the impl, (b) constructs a per-moduleLogosModulesaggregate and threads its pointer through the same base, and (c) installs the typed-event callback (maybeSetEmitEvent) consumed by<name>_events.cppmethod bodies. Impls that don't inheritLogosModuleContextcompile unchanged — the helper overloads collapse to no-ops. The fullLogosAPIis never exposed past the provider boundary. - Always emits
#include "logos_sdk.h"and astd::unique_ptr<LogosModules> m_logosModulesmember; ownership lives on the provider, the context base sees only a non-owningvoid*reinterpreted inLogosModuleContext::modules()(which depends on the impl's TU having includedlogos_sdk.h).
- Emits
lidlMakeProviderDispatch(ModuleDecl)— generates callMethod/getMethods dispatch.getMethods()emits the full interface: each method taggedtype: "method", then eachmodule.eventsentry taggedtype: "event"(name, signature, parameters, escapeddescription; no returnType/isInvokable). There is no separategetEvents()— folding events intogetMethods()keeps the provider vtable ABI-stable.lidlMakeEventsSource(ModuleDecl, implClass, implHeader)— generates<name>_events.cpp: Qt-MOC-style method bodies for prototypes declared in the impl'slogos_events:block. Each body marshals typed args into aQVariantListand callsthis->emitEventImpl_("<name>", &args)on the LogosModuleContext base.
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/InLogosEvents - The literal
logos_events:token (defined inlogos_module_context.has#define logos_events public) opens an events section; bare prototypes inside becomeEventDecl{name, params, description}entries appended toModuleDecl.events(thedescriptionis the doc comment immediately above the declaration, captured viajoinDocLinesexactly as for methods) - Skips: constructors, destructors, typedefs, using, friend, enum, struct,
std::functiondeclarations - Recognizes
LogosMapandLogosListreturn types (nlohmann::json aliases) and setsMethodDecl.jsonReturn = true - Template-aware parameter splitting (handles
std::vector<std::string>correctly)
CLI Usage
From C++ impl header (primary use case for universal modules)
logos-cpp-generator --from-header src/my_module_impl.h \
--backend qt \
--impl-class MyModuleImpl \
--impl-header my_module_impl.h \
--metadata metadata.json \
--output-dir ./generated_code
Generates: my_module_qt_glue.h, my_module_dispatch.cpp
From LIDL file — provider glue
logos-cpp-generator --lidl my_module.lidl \
--backend qt \
--impl-class MyModuleImpl \
--impl-header my_module_impl.h \
--output-dir ./generated_code
From LIDL file — client stubs
logos-cpp-generator --lidl my_module.lidl \
--output-dir ./generated_code \
--module-only
Legacy modes (unchanged)
logos-cpp-generator /path/to/plugin.so --output-dir ./generated
logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generated
logos-cpp-generator --provider-header src/provider.h --output-dir ./generated
Consumer wrapper with typed event accessors
The --events-from <path> flag points the legacy <plugin>.dylib --module-only codegen at a LIDL sidecar shipped alongside the dep's pre-built headers. When set, the generated <name>_api.{h,cpp} gains one typed on<EventName>(callback) accessor per declared event (callback arg types match --api-style):
logos-cpp-generator /path/to/plugin.dylib \
--module-only --api-style lp \
--events-from /path/to/dep/share/logos/my_module.lidl \
--output-dir ./generated
In Nix builds this is wired automatically: buildHeaders.nix looks for <pluginLib>/share/logos/<name>.lidl (which buildPlugin.nix's installPhase placed there) and threads it through.
Building
The generator is built as part of logos-cpp-sdk:
ws build logos-cpp-sdk # builds everything including the generator
The generator binary is available as logos-cpp-generator in module build environments (provided by logos-module-builder's nativeBuildInputs).
Testing
Tests are in tests/experimental/:
ws test logos-cpp-sdk # runs all tests including experimental
The frontend tests (lexer/parser/validator/serializer) moved to the logos-lidl repo along with the code; only the C++/Qt-specific backends are tested here:
| Test file | What it tests |
|---|---|
test_lidl_type_mapping.cpp |
lidlTypeToQt, lidlTypeToStd, lidlIsStdConvertible, lidlToPascalCase |
test_lidl_gen_client.cpp |
Client stub generation: sync/async methods, events, metadata JSON, edge cases |
test_impl_header_parser.cpp |
Header parsing: type mapping, access specifiers, skipping private/protected, error cases |
(The lexer/parser/AST/serializer/validator round-trip + description tests live in logos-lidl's own tests/test_lidl.cpp.)
Fixture files in tests/experimental/fixtures/:
sample_impl.h— module with all supported type variationssample_metadata.json— metadata with dependenciescomplex_impl.h— module with multiple access specifier sectionsempty_class_impl.h— class with no public methodsempty_metadata.json— minimal metadata
Known Limitations
- The impl header parser is lightweight (regex + state machine). It does not handle:
- Multi-line method declarations
- Default parameter values
- Method definitions in the header (only declarations ending with
;) - Nested classes
- Template methods
std::functionmembers are silently skipped (never treated as methods)
- LIDL does not support generic/parameterized types or inheritance
--from-headeremits the cdylib backend here (theqtglue backend moved to logos-qt-generator); the Rust backend lives in logos-rust-sdk'slidl-gen, generating over logos-lidl's C ABI- Client stub generation (
lidlMakeHeader/lidlMakeSource) is only available from LIDL files, not from--from-header