* extend universal modules with module context * implement module calls and events for universal modules * pr comments
17 KiB
Logos Code Generator — Experimental
Overall Description
The experimental code generator extends logos-cpp-generator with two new capabilities: a lightweight Interface Definition Language (LIDL) for declaring module contracts, and a C++header parser that can infer module interfaces directly from pure C++ implementation classes. Both paths produce the same output: Qt plugin glue code that bridges pure C++ module implementations to the Logos runtime's Qt Remote Objects transport.
The goal is to decouple module business logic from the Qt framework. Module authors write standard C++ using std::string, int64_t, std::vector<T>, and the build system generates all Qt boilerplate (QObject, Q_PLUGIN_METADATA, QString conversions, method dispatch) automatically.
Definitions & Acronyms
| Term | Definition |
|---|---|
| LIDL | Logos Interface Definition Language — a lightweight DSL for declaring module interfaces |
| Universal Module | A module whose implementation is pure C++ (no Qt types) with all Qt glue generated at build time |
| Provider Glue | Generated code that wraps a pure C++ impl class in a LogosProviderObject with callMethod() dispatch and getMethods() introspection |
| Client Stub | Generated type-safe C++ wrapper class that callers use to invoke a module's methods without string-based dispatch |
| Dispatch | The generated callMethod() function that maps string method names to typed method calls on the provider object |
| Impl Header | The pure C++header file (_impl.h) that declares a module's public methods using standard C++ types |
| TypeExpr | The AST node representing a type in the LIDL type system |
| ModuleDecl | The AST node representing a complete module declaration (name, version, methods, events, types, hasEmitEvent flag) |
Domain Model
Two Paths to the Same Output
Path 1: LIDL file Path 2: C++ impl header
│ │
▼ ▼
lidlTokenize() parseImplHeader()
│ │
▼ │
lidlParse() │
│ │
▼ │
lidlValidate() │
│ │
▼ ▼
ModuleDecl ◄────── same AST ──────► ModuleDecl
│ │
├──► lidlMakeProviderHeader() ◄──────┤
│ → <name>_qt_glue.h │
│ │
├──► lidlMakeProviderDispatch() ◄─────┤
│ → <name>_dispatch.cpp │
│ │
├──► lidlMakeHeader() │
│ → <name>_api.h │
│ │
└──► lidlMakeSource() │
→ <name>_api.cpp │
Both paths converge at ModuleDecl, the shared AST. From there, the same generation functions produce identical output regardless of the input format.
LIDL Language
LIDL is a minimal interface definition language. A module declaration contains metadata, type definitions, method signatures, and event signatures:
module wallet_module {
version "1.0.0"
description "Wallet operations"
category "finance"
depends [crypto_module]
type Account {
address: tstr
balance: uint
? label: tstr ; optional field
}
method createAccount(passphrase: tstr) -> tstr
method getBalance(address: tstr) -> uint
method listAccounts() -> [tstr]
method transfer(from: tstr, to: tstr, amount: uint) -> result
event onTransfer(from: tstr, to: tstr, amount: uint)
}
Comments start with ; and run to end of line.
Type System
Built-in primitive types:
| LIDL type | Meaning | Qt mapping | C++ std mapping |
|---|---|---|---|
tstr |
Text string | QString |
std::string |
bstr |
Binary data | QByteArray |
std::vector<uint8_t> |
int |
Signed 64-bit integer | int |
int64_t |
uint |
Unsigned 64-bit integer | int |
uint64_t |
float64 |
Double precision float | double |
double |
bool |
Boolean | bool |
bool |
result |
Structured result (success/value/error) | LogosResult |
LogosResult |
any |
Untyped value | QVariant |
QVariant |
void |
No return value | void |
void |
Composite types:
[T]— Array of T (e.g.,[tstr]→QStringList/std::vector<std::string>){K: V}— Map from K to V (e.g.,{tstr: int}→QVariantMap)?T— Optional T (→QVariant)
Named types reference type definitions within the same module.
C++ Header Parsing
The --from-header mode parses a C++implementation header to extract public method signatures. It maps C++ types to LIDL types:
| C++ type | LIDL type |
|---|---|
std::string / const std::string& |
tstr |
bool |
bool |
int64_t |
int |
uint64_t |
uint |
double |
float64 |
void |
void |
std::vector<std::string> |
[tstr] |
std::vector<uint8_t> |
bstr |
std::vector<int64_t> |
[int] |
std::vector<uint64_t> |
[uint] |
std::vector<double> |
[float64] |
std::vector<bool> |
[bool] |
LogosMap |
{tstr: any} (Map) — nlohmann::json alias; sets jsonReturn flag |
LogosList |
[any] (Array) — nlohmann::json alias; sets jsonReturn flag |
QVariantMap |
{tstr: any} (Map) — legacy Qt type |
QVariantList |
[any] (Array) — legacy Qt type |
QStringList |
[tstr] (Array) — legacy Qt type |
| Anything else | any |
LogosMap and LogosList are using aliases for nlohmann::json defined in logos_json.h (part of the SDK). They allow module implementations to remain completely Qt-free while returning rich structured data. The parser maps them to the same LIDL shapes as QVariantMap/QVariantList, but sets the jsonReturn flag on the method so the generator emits an nlohmannToQVariant() conversion in the glue layer.
The parser uses a state machine to find the target class, track access specifiers (public/private/protected), and extract method declarations. It skips constructors, destructors, typedefs, using declarations, std::function members, and non-method statements.
Module metadata (name, version, description, dependencies) comes from metadata.json, not from the header.
Event Emission via logos_events:
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:
#include <logos_module_context.h>
class MyModuleImpl : public LogosModuleContext {
public:
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);
};
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:
-
<name>_events.cpp— Qt-MOC-style definitions of each declared event method on the impl class. Bodies marshal typed args into aQVariantListand callthis->emitEventImpl_("<event>", &args), a protected helper onLogosModuleContext:void MyModuleImpl::userLoggedIn(const std::string& userId, int64_t timestamp) { QVariantList _args{ QVariant(QString::fromStdString(userId)), QVariant(static_cast<qlonglong>(timestamp)) }; this->emitEventImpl_("userLoggedIn", &_args); } -
Provider
onInitwiring —<name>_qt_glue.hadds a_logos_codegen_::maybeSetEmitEventcall alongside the existingmaybeSetContext/maybeSetLogosModules. The lambda casts the void* back to QVariantList and forwards toLogosProviderBase::emitEvent(QString, QVariantList)(same wire as before):_logos_codegen_::maybeSetEmitEvent(m_impl, [this](const std::string& name, void* args) { emitEvent(QString::fromStdString(name), *static_cast<QVariantList*>(args)); }); -
<name>.lidlsidecar — a serialised view of the module's declared events (using the existinglidlSerializefromlidl_serializer.cpp):module my_module { event userLoggedIn(userId: tstr, timestamp: int) event messageReceived(from: tstr, body: tstr) }buildPlugin.nixships this at$out/share/logos/<name>.lidl.buildHeaders.nixpasses it to the consumer-side codegen via--events-from, which adds typedon<EventName>(callback)accessors to the generated<Module>wrapper (one per declared event, callback-arg types respect--api-style).
Legacy backward-compat: the older std::function<void(const std::string&, const std::string&)> 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
Provider Glue (<name>_qt_glue.h)
Contains two classes:
- ProviderObject — inherits
LogosProviderBase, holds an instance of the impl class (m_impl). Each public method is wrapped with type conversion:
- Qt parameters → C++ std parameters (e.g.,
QString.toStdString()) - Call
m_impl.method(...) - C++ std return → Qt return (e.g.,
QString::fromStdString(result)) - For
jsonReturnmethods (returningLogosMap/LogosList), the glue calls a generatednlohmannToQVariant()recursive helper to convertnlohmann::json→QVariant/QVariantMap/QVariantList - If the impl declares an
emitEventcallback (hasEmitEvent), the constructor wires it toLogosProviderBase::emitEvent - Always overrides
onInit(LogosAPI*)to (a) copy the three runtime-injected properties (modulePath,instanceId,instancePersistencePath) into the impl when it inherits fromLogosModuleContext, and (b) construct a per-moduleLogosModules(fromgenerated_code/logos_sdk.h) owned by the provider, threading its pointer through the same context base. Both wire-ups go through SFINAE'd helpers inlogos_module_context.h(_logos_codegen_::maybeSetContext/maybeSetLogosModules), so non-inheriting impls compile unchanged and theLogosAPInever escapes the provider.
- Plugin —
QObjectsubclass implementingPluginInterfaceandLogosProviderPlugin. CarriesQ_PLUGIN_METADATAandQ_INTERFACES. ItscreateProviderObject()factory returns a new ProviderObject instance.
Dispatch (<name>_dispatch.cpp)
Implements two methods on the ProviderObject:
**callMethod(methodName, args)** — string-based dispatch table. For each method, extracts args fromQVariantList, calls the typed wrapper, returns result asQVariant. Void methods returnQVariant(true).**getMethods()**— returnsQJsonArrayof method metadata. Each entry hasname,signature,returnType,isInvokable, andparameters[](withtypeandname).
Client Stubs (<name>_api.h + <name>_api.cpp)
Generated from LIDL (not from --from-header). Each module gets one <Module> wrapper class whose signature shape is picked by the consumer's build via --api-style:
--api-style |
Wrapper signatures |
|---|---|
qt (default) |
QString / QStringList / QVariantList / QVariantMap / int / LogosResult |
std |
std::string / std::vectorstd::string / LogosMap / LogosList / int64_t / StdLogosResult |
Both styles provide:
- Typed sync methods that call
invokeRemoteMethod()and convert theQVariantresult. - 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 (<name>_api.h / <name>_api.cpp) and the same class name (<Module>) — 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:
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
LIDL Pipeline
- Lexer (
lidlTokenize) — tokenizes source into keywords, identifiers, string literals, symbols - Parser (
lidlParse) — recursive descent parser producing aModuleDeclAST - Validator (
lidlValidate) — checks for duplicate names, unknown type references, builtin shadowing, duplicate parameters - Serializer (
lidlSerialize) — pretty-prints aModuleDeclback to LIDL text (useful for roundtrip testing)
Impl Header Pipeline
- parseImplHeader — reads
metadata.json+ C++ header, produces aModuleDecl - Same generation functions as LIDL path
Backwards Compatibility
- All existing generator modes (
--provider-header,--metadata, plugin path) continue to work unchanged vialegacy_main() - The new
--from-headerand--lidlmodes are additive - Generated plugins implement both
PluginInterface(forlmintrospection) andLogosProviderPlugin(for new-API provider creation) - The runtime (
logos-liblogos) already supports both old and new plugin types viaqobject_castdetection
Conversion Helper Generation
Conversion helpers are only emitted when needed:
- String vector helpers (
lidlToQStringList,lidlToStdStringVector) — emitted when the module uses[tstr]parameters or return types - nlohmann→Qt helper (
nlohmannToQVariant) — emitted when any method hasjsonReturn = true(i.e., the impl returnsLogosMaporLogosList). This recursive function convertsnlohmann::jsonobjects, arrays, strings, numbers, and booleans to theirQVariantequivalents.