Files
Dario LipicarandClaude Opus 5 4fb867a72a docs: the universal/cdylib hosting step is logos-qt-host-generator (#17)
These were already wrong before any deletion. module-builder repointed the
cdylib Qt glue onto logos-plugin-qt's logos-qt-host-generator (5081088) and the
docs kept naming logos-qt-generator, so the pipeline they describe has not been
the pipeline that runs for some time.

That matters more here than in most docs: guidelines/ and docs/ are SHIPPED —
flake.nix copies them into the dev-boost output, and generate-agents-md.ts
embeds them into every scaffolded module's AGENTS.md/CLAUDE.md. So the stale
line was being handed to module authors, and to agents reading the scaffold, as
the instruction. That is why the six doctests/outputs/ files change too: they
are the recorded scaffolder output, and they carry the same text.

Only the middle step moves. `logos-cpp-generator --backend cdylib` — the Qt-free
C-ABI export wrapper — is a different tool doing a different job and is
deliberately untouched; it remains correct in every one of these blocks.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:00:07 -03:00

4.7 KiB

Universal Module Development

The Universal Interface Pattern

Universal modules use pure C++ for their implementation. You write a single implementation class using standard C++ types. The build system generates all Qt/plugin infrastructure automatically — universal modules are header-first cdylibs (see codegen.md).

You write: A C++ class with std::string, int64_t, bool, std::vector<T>. The generator produces: a derived .lidl contract, the uniform Qt-plugin glue, and a Qt-free C-ABI export wrapper around your class — so your code never touches Qt.

Rules

  • NO Qt types in your impl header or implementation: no QString, QObject, Q_INVOKABLE, QVariant
  • NO Qt includes in your impl header (Qt headers in .cpp are OK if needed for internal use, but the public API must be pure C++)
  • Set "interface": "universal" in metadata.json
  • Name the impl class <PascalCaseName>Impl (e.g., CryptoUtilsImpl)
  • Name the impl header <name>_impl.h (e.g., crypto_utils_impl.h)
  • Only public methods become module API methods. Private/protected are ignored by the generator.
  • Constructors, destructors, typedefs, and using declarations are skipped by the generator.

Type Mapping

Use this in your C++ Generator maps to Qt type produced
std::string / const std::string& tstr QString
bool bool bool
int64_t int int
uint64_t uint int
double float64 double
void void void
std::vector<std::string> [tstr] QStringList
std::vector<uint8_t> bstr QByteArray
std::vector<int64_t> [int] QVariantList
std::vector<double> [float64] QVariantList
std::vector<bool> [bool] QVariantList
LogosMap {tstr: any} QVariantMap
LogosList [any] QVariantList

LogosMap and LogosList (from <logos_json.h>) are aliases for nlohmann::json. Use them when you need to return structured objects or arrays while keeping your impl Qt-free. The generator automatically converts them to QVariantMap/QVariantList in the glue layer.

If you use a type not in this table, the generator maps it to any (QVariant). Prefer explicit types from the table for type safety.

Emitting Events

To emit events from your module, declare a public emitEvent callback in your impl header:

#include <functional>
std::function<void(const std::string& eventName, const std::string& data)> emitEvent;

The generator detects this automatically and wires it to the Logos event system. Call it from your implementation:

if (emitEvent) {
    emitEvent("somethingHappened", someData);
}

No events array in metadata.json is needed — the generator infers everything from the header.

Impl Header Template

#pragma once
#include <string>
#include <vector>
#include <cstdint>

class MyModuleImpl {
public:
    MyModuleImpl();
    ~MyModuleImpl();

    std::string doSomething(const std::string& input);
    bool validate(const std::string& data);
    int64_t count();
    std::vector<std::string> listItems();

private:
    // Private members are not exposed as module API
};

Build Pipeline

You don't write a preConfigure or run the generator — mkLogosModule runs the universal pipeline automatically when metadata.json sets "interface": "universal". It derives a .lidl from your impl header, then emits the uniform Qt-plugin glue and a Qt-free C-ABI export wrapper around your class (run for you, you don't invoke these):

logos-cpp-generator --header-to-lidl src/<name>_impl.h \
  --impl-class <ImplClassName> --metadata metadata.json \
  -o ./generated_code/<name>.lidl
logos-qt-host-generator --lidl ./generated_code/<name>.lidl --backend cdylib \
  --output-dir ./generated_code
logos-cpp-generator --lidl ./generated_code/<name>.lidl --backend cdylib \
  --impl-class <ImplClassName> --impl-header <name>_impl.h \
  --output-dir ./generated_code

This produces generated_code/<name>.lidl, <name>_cdylib_glue.{h,cpp}, and <name>_module_impl.cpp. You do not list these in CMakeLists.txtLogosModule.cmake globs generated_code/ automatically. See codegen.md for details.

Testing

Unit tests instantiate the impl class directly — it is a plain C++ class:

#include "my_module_impl.h"
// No Qt test framework needed for basic tests
MyModuleImpl impl;
assert(impl.doSomething("test") == "expected");

Integration tests use logoscore (start a daemon, then call via the client):

logoscore -D -m ./result/lib &
logoscore load-module my_module
logoscore call my_module doSomething test
logoscore stop