The project includes a Nix flake for reproducible builds with a modular structure:
# Build everything (default)
nix build
# Or explicitly
nix build '.#logos-cpp-sdk'
nix build '.#default'The result will include:
/bin/logos-cpp-generator- Code generator binary/lib/- SDK libraries/include/- Headers (core/ and cpp/)
# Build only the generator binary (outputs to /bin)
nix build '.#logos-cpp-bin'
# Build only the SDK library (outputs to /lib)
nix build '.#logos-cpp-lib'
# Build only the headers (outputs to /include)
nix build '.#logos-cpp-include'
# Legacy alias for generator
nix build '.#cpp-generator'# Enter development shell with all dependencies
nix developNote: In zsh, you need to quote the target (e.g., '.#logos-cpp-sdk') to prevent glob expansion.
If you don't have flakes enabled globally, add experimental flags:
nix build '.#logos-cpp-sdk' --extra-experimental-features 'nix-command flakes'The compiled artifacts can be found at result/
The nix build system is organized into modular files in the /nix directory:
nix/default.nix- Common configuration (dependencies, flags, metadata)nix/bin.nix- Generator binary compilationnix/lib.nix- SDK library compilationnix/include.nix- Header installationnix/tests.nix- Test suite (build + run vianix build '.#tests')
# Build and run all tests (build fails if any test fails)
nix build '.#tests'The test binaries are available in result/bin/ and can be re-run with filters:
./result/bin/sdk_tests --gtest_filter="LogosResultTest.*"
./result/bin/generator_tests --gtest_filter="*PascalCase*"cd cpp
./compile.shcd cpp-generator
./compile.shThe generator binary will be available at build/bin/logos-cpp-generator.
The logos-cpp-generator tool generates C++ wrapper code for Logos plugins.
# Generate wrapper for a single plugin (uses default output directory)
logos-cpp-generator /path/to/plugin.dylib
# Specify custom output directory
logos-cpp-generator /path/to/plugin.dylib --output-dir /custom/output/path
# Generate only the module files (no core manager or umbrella headers)
logos-cpp-generator /path/to/plugin.dylib --module-only
# Combine options
logos-cpp-generator /path/to/plugin.dylib --output-dir /custom/output --module-only# List dependencies from metadata.json
logos-cpp-generator --metadata /path/to/metadata.json
# Generate wrappers for all dependencies
logos-cpp-generator --metadata /path/to/metadata.json --module-dir /path/to/modules
# Generate with custom output directory
logos-cpp-generator --metadata /path/to/metadata.json --module-dir /path/to/modules --output-dir /custom/output
# Generate only module files (no core manager or umbrella headers)
logos-cpp-generator --metadata /path/to/metadata.json --module-dir /path/to/modules --module-only
# Generate only core manager and umbrella files (assumes module files already exist)
logos-cpp-generator --metadata /path/to/metadata.json --general-only
# Generate general files with custom output directory
logos-cpp-generator --metadata /path/to/metadata.json --general-only --output-dir /custom/output--output-dir /path/to/output
- Default: If not specified, generated files are placed in
logos-cpp-sdk/cpp/generated/ - Custom: Specify any directory for the generated files
- The output directory will be created automatically if it doesn't exist
--module-only
- When specified, generates only the requested module's
.hand.cppfiles - Skips generation of
core_manager_api.*and umbrella headers (logos_sdk.*) - Useful when you only need wrapper code for specific modules
--general-only
- When specified with
--metadata, generates only the core manager and umbrella SDK files - Assumes module wrapper files already exist in the output directory
- Generates:
core_manager_api.h,core_manager_api.cpp,logos_sdk.h,logos_sdk.cpp - The umbrella headers will include references to all modules listed in the metadata's
dependenciesarray - For each dependency (e.g.,
"waku_module"), it will:- Include
waku_module_api.hin the header - Include
waku_module_api.cppin the source - Create a
WakuModule waku_module;member in theLogosModulesstruct
- Include
- Does not require
--module-dirsince it doesn't process plugins
By default (without --module-only or --general-only):
<module>_api.hand<module>_api.cpp- Wrapper code for each modulecore_manager_api.handcore_manager_api.cpp- Core manager wrapperlogos_sdk.handlogos_sdk.cpp- Umbrella headers including all modules
With --module-only:
- Only
<module>_api.hand<module>_api.cpp- Wrapper code for the requested module(s)
With --general-only:
- Only
core_manager_api.handcore_manager_api.cpp- Core manager wrapper - Only
logos_sdk.handlogos_sdk.cpp- Umbrella headers that reference existing module files
A common workflow is to generate module wrappers separately, then generate the umbrella SDK:
# Step 1: Generate individual module wrappers
logos-cpp-generator /path/to/plugin1.dylib --module-only --output-dir ./generated
logos-cpp-generator /path/to/plugin2.dylib --module-only --output-dir ./generated
# Step 2: Generate core manager and umbrella SDK (references modules from step 1)
logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generatedThis approach gives you fine-grained control over which modules to include and allows rebuilding just the umbrella headers without regenerating all module wrappers.
Universal (codegen-driven) modules — those built from a package_xxx_impl.h header rather than a handcrafted QObject plugin — don't see the raw LogosAPI at all. Instead, the codegen-generated provider populates a narrow LogosModuleContext base class with everything an impl typically needs:
- Three host-injected properties exposed as typed getters
- A
LogosModulesaggregate for calling other modules
An impl opts in by inheriting from LogosModuleContext (defined in logos_module_context.h):
#include <logos_module_context.h>
#include <logos_json.h>
#include "logos_sdk.h" // generated at build time; defines LogosModules
class MyModuleImpl : public LogosModuleContext {
public:
LogosMap doWork(const std::string& 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 <Dep>
// wrapper takes/returns std types — no Qt at the call site.
std::string reply = modules().some_dep.echo(input);
// ...
}
protected:
void onContextReady() override {
// One-time setup: the getters below are now readable.
// Fires exactly once, before any method dispatch.
std::string dataDir = instancePersistencePath();
// open files, prime caches, etc.
}
};Available getters:
| Getter | Description |
|---|---|
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. |
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). |
Each module's build picks one API style for the generated <Module> 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<std::string>, 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:
// Universal module (api-style=std):
std::string reply = modules().some_dep.echo("hi");…and in a handcrafted Qt module the same call is:
// 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 tointerface: "universal"— there's no per-flag override onmetadata.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.
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 <name>_events.cpp (Qt-MOC style), and ships a <name>.lidl file describing them so consumer-side codegen can produce typed subscribers:
#include <logos_module_context.h>
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<EventName>(...) accessors are generated on the dep's <Module> wrapper. The generic onEvent(name, cb) channel stays available as a forward-compat escape hatch:
// 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<void(const std::string&, const std::string&)> emitEvent member working. The codegen still detects it and wires the lambda in the provider constructor. New code should prefer logos_events:.
LogosResult provides a structured way to return either a value or an error from synchronous method calls.
If the success attribute is true, you can retrieve the value using a cast. Otherwise, retrieve the error which should be a string (though not enforced).
The success attribute should ALWAYS be asserted. Accessing the value of an errored LogosResult or the error of a valid LogosResult will result in a LogosResultException being thrown.
LogosResult result = m_logos->my_module.someMethod();
if (result.success) {
// Use shorthand
QString value = result.getString();
// Or
QString value = result.getValue<QString>();
} else {
// Use shorthand
QString error = result.getError();
// Or
QString error = result.getError<QString>();
}Let's say you need to return a complex object. In the SDK, you have to build your type with primitive like QVariantMap:
// Received JSON: {"cid": "QmXyz...", "filename": "photo.jpg", "size": 2048576, "mimetype": "image/jpeg"}
QVariantMap manifest;
manifest["cid"] = "QmXyz...";
manifest["filename"] = "photo.jpg";
manifest["size"] = 2048576;
manifest["mimetype"] = "image/jpeg";
return {true, manifest};And then to consume by using the shorthand function:
LogosResult result = m_logos->my_plugin.someMethod(cid);
if (result.success) {
QString cid = result.getString("cid");
// You can define a default value as well
QString cid = result.getString("cid", "unknown");
}Or you can use the value directly:
LogosResult result = m_logos->my_plugin.someMethod(cid);
if (result.success) {
QVariantMap manifest = result.getMap();
QString cid = manifest["cid"].toString();
}Same thing for a list, you can use QVariantList:
QVariantList manifests;
QVariantMap m1;
m1["cid"] = "QmAbc...";
m1["filename"] = "document.pdf";
m1["size"] = 1024000;
manifests.append(m1);
QVariantMap m2;
m2["cid"] = "QmDef...";
m2["filename"] = "image.png";
m2["size"] = 512000;
manifests.append(m2);
return {true, manifests};To consume it using the shorthand function:
LogosResult result = m_logos->my_plugin.someMethod();
if (result.success) {
for (int i = 0; i < list.size(); ++i) {
QString cid = result.getString(i, "cid");
// You can define a default value as well
QString cid = result.getString(0, "cid", "unknown");
}
}Or you can use the value directly:
LogosResult result = m_logos->my_plugin.someMethod();
if (result.success) {
QVariantList list = result.getList();
for (const QVariant& item : list) {
QVariantMap manifest = item.toMap();
QString cid = manifest["cid"].toString();
}
}The SDK installs a CMake package. Consumers use find_package:
find_package(logos-cpp-sdk REQUIRED)
target_link_libraries(my_target PRIVATE logos-cpp-sdk::logos_sdk)The package config re-resolves transitive dependencies (Qt6 Core/RemoteObjects, Boost system, OpenSSL, nlohmann_json), so consumers don't have to wire them up manually. The static archive references OpenSSL SSL_CTX_*/X509_* and Boost system::error_code; without find_package's imported target the link step fails.
The SDK supports multiple transports, selected via LogosTransportConfig:
| Protocol | Backend | Use case |
|---|---|---|
LocalSocket |
Qt Remote Objects over QLocalSocket |
In-host, module-to-module (default) |
Tcp |
Boost.Asio + JSON/CBOR framing | Cross-host or container-to-host |
TcpSsl |
Boost.Asio + OpenSSL + JSON/CBOR framing | Same as TCP, with TLS |
A LogosTransportSet (= std::vector<LogosTransportConfig>) lets a single provider publish on multiple endpoints simultaneously (e.g. local socket for in-process clients + TCP+SSL for remote ones):
LogosTransportConfig local; // protocol = LocalSocket (default)
LogosTransportConfig tls;
tls.protocol = LogosProtocol::TcpSsl;
tls.host = "0.0.0.0";
tls.port = 7443;
tls.caFile = "/etc/logos/ca.pem";
tls.certFile = "/etc/logos/server.pem";
tls.keyFile = "/etc/logos/server.key";
LogosAPI* api = new LogosAPI("core_service", LogosTransportSet{local, tls}, this);For processes that want to override the SDK-wide default, use LogosTransportConfigGlobal::setDefault() once at startup before any LogosAPI is constructed.
- CMake (3.x or later)
- Ninja build system
- pkg-config
- Qt6 (qtbase)
- Qt6 Remote Objects (qtremoteobjects)
- Boost (system)
- OpenSSL
- nlohmann_json
- macOS (aarch64-darwin, x86_64-darwin)
- Linux (aarch64-linux, x86_64-linux)