3.9 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 via logos-cpp-generator --from-header.
You write: A C++ class with std::string, int64_t, bool, std::vector<T>.
The generator produces: Qt plugin class, method dispatch, introspection metadata.
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
.cppare OK if needed for internal use, but the public API must be pure C++) - Set
"interface": "universal"inmetadata.json - Name the impl class
<PascalCaseName>Impl(e.g.,CryptoUtilsImpl) - Name the impl header
<name>_impl.h(e.g.,crypto_utils_impl.h) - Only
publicmethods 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
The flake.nix preConfigure hook runs the generator before CMake:
logos-cpp-generator --from-header src/<name>_impl.h \
--backend qt \
--impl-class <ImplClassName> \
--impl-header <name>_impl.h \
--metadata metadata.json \
--output-dir ./generated_code
This produces generated_code/<name>_qt_glue.h and generated_code/<name>_dispatch.cpp. These files are listed in CMakeLists.txt under the SOURCES of logos_module().
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:
logoscore -m ./result/lib -l my_module -c "my_module.doSomething(test)"