Files
logos-cpp-sdk/README.md
Dario LipicarandClaude Opus 4.8 87abcd8044 Cdylib authoring: --backend cdylib emits the common C ABI wrapper + uniform Qt glue (#84)
* Extract the protocol layer into logos-protocol; consume it as a flake input

The transport/token/IPC layer (transports incl. QRO + plain TCP/TLS,
consumer core LogosAPIClient/LogosAPIConsumer with the capability
auto-requestModule flow, ModuleProxy, token manager, QVariant<->JSON
conversion, the abstract LogosProviderObject interface) now lives in the
logos-protocol repo behind the versioned lp_* C ABI.

This SDK keeps the typed C++ developer layer (LogosAPI, provider base
classes + Qt provider glue, module context, code generator) and still
compiles the protocol sources INTO liblogos_sdk.a from the flake input,
so the installed artifact (archive symbols, include/ + include/cpp
layouts, cmake config) stays byte-compatible: existing consumers need
no changes. Public headers are unchanged; logos_provider_object.h keeps
its name and now re-exports the abstract interface from
logos_provider_interface.h.

Transport/protocol component tests moved to logos-protocol with the
code; the remaining sdk/generator/experimental suites are unchanged
(432/432 green against the local protocol checkout).

* lock: add logos-protocol input

* Make the base SDK Qt-free: move the Qt developer layer to logos-qt-sdk

LogosAPI, LogosAPIProvider, LogosProviderBase/LOGOS_PROVIDER macros, the
QObject provider glue (QtProviderObject) and the legacy PluginInterface
(core/interface.h) move to the new logos-qt-sdk repo. The protocol
sources are no longer compiled into a monolithic archive — consumers
link logos-qt-sdk (which layers on logos-protocol) instead.

What remains here is header-only std C++: logos_module_context.h,
logos_result.h (StdLogosResult), logos_json.h — exported as the CMake
INTERFACE target logos-cpp-sdk::logos_headers — plus the code generator
(a build-time tool; its introspection mode now includes
logos_provider_interface.h from logos-protocol, where
LogosProviderPlugin moved).

Mechanically verified Qt-free: the logos-cpp-lib / logos-cpp-include
closures contain only nlohmann_json. Tests: 245/245 (module-context std
suite + generator + experimental).

* Cdylib authoring backend: --backend cdylib emits the common C ABI wrapper + uniform Qt glue

From a module's LIDL contract the generator now emits:
- <name>_module_impl.cpp — the Qt-FREE logos_module_impl.h export
  wrapper (dispatch/get_methods/set_context/set_emit_callback/
  accept_token/get_protocol_version/string_free) around the universal
  impl class; compiled into the module's cdylib. Tagged {"_bytes"}
  bytes, StdLogosResult -> {success,value,error}, context via the
  existing _logos_codegen_::maybeSet* SFINAE helpers.
- <name>_events_cdylib.cpp — typed logos_events: bodies marshalling
  into nlohmann::json (the cdylib flavor of the events sidecar).
- <name>_cdylib_glue.{h,cpp} — the UNIFORM Qt-plugin glue forwarding
  LogosProviderObject to the C symbols; identical regardless of the
  module's source language (the Rust SDK emits the same exports).
- the .lidl sidecar.

Qt-container types are rejected at generation time (a cdylib impl is
Qt-free by definition). Verified end-to-end by dlopen smoke: typed
dispatch, typed events through the emit callback, result returns,
introspection, and the protocol-version handshake — and the SAME C
harness passes against a Rust cdylib generated by logos-lidl-gen
--provider.

* fix: accept the installed source-export layout in the protocol-root check

The fail-fast only tested <root>/cpp/logos_protocol.h, but the LP_SRC
selection right below (and the error message itself) support the
installed export layout <root>/include/cpp as well. Pointing
LOGOS_PROTOCOL_ROOT at an installed export tripped the FATAL_ERROR
before that fallback could apply.

Caught by Copilot review on #82.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* lock: pin logos-protocol to the qt-free-split branch head

The Qt-free SDK (and the cdylib backend stacked on it) reference
LogosProviderPlugin from protocol's logos_provider_interface.h, which
lands on feat/qt-free-split — the P1-branch pin no longer compiles
standalone. Temporary — drop when the chain PRs merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* lock: pin logos-protocol to the qt-free-split branch head

The Qt-free SDK (and the cdylib backend stacked on it) reference
LogosProviderPlugin from protocol's logos_provider_interface.h, which
lands on feat/qt-free-split — the P1-branch pin no longer compiles
standalone. Temporary — drop when the chain PRs merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: pin the logoscore runtime via its {release} placeholder

The spec built logoscore-cli at bare master with only the cpp-sdk inputs
overridden — master's stack cannot compile against the qt-free SDK, so
the suite failed on the chain branches. With the placeholder, CI's
--release-for pins expand it to the workspace's logoscore commit (and
local runs without a pin still fall back to master, unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: override the nested module builders to {release} too

capability_module (via logoscore's lock) and the cloned accounts module
resolve module-builder from their own locks — pre-split revs whose
LogosModule.cmake still detects the SDK by logos_api.h, which the
qt-free SDK no longer ships ('logos-cpp-sdk not found'). Overriding the
builder itself to the workspace-pinned chain rev (keeping the nested
cpp-sdk override) builds both modules with the split-aware builder.
Verified end-to-end locally with the exact doctest command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: apply the {release} + nested-builder overrides to all three specs

The runtime spec got the treatment in 210eea1; the composition and
worker-thread specs have the same logoscore/module build commands and
failed identically (pre-split builders from the modules' own locks).
All executed run: blocks now pin logoscore-cli{release} and override
the nested module builders to logos-module-builder{release}; the
displayed code_block: variants stay in their generic master form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* generator: glue-only cdylib mode for non-C++ impls

--lidl x.lidl --backend cdylib (no --impl-class) emits just the uniform
Qt-plugin glue; the C exports come from the module's own language
backend (e.g. the Rust SDK's lidl-gen --provider). This is the entry
point logos-module-builder's cdylib interface uses for Rust modules.

* cdylib glue: root plugin implements PluginInterface

logos_host's module_initializer hard-requires PluginInterface on the
root plugin before any provider detection — same bases as the qt glue
(QObject, PluginInterface, LogosProviderPlugin). Caught by the first
host-loaded run of a cdylib-authored module; the dlopen smoke harness
exercised only the C seam.

* cdylib glue: seed the cdylib's protocol stack with the host auth token

The glue's init() reads the authToken property module_initializer now
surfaces on the LogosAPI object and forwards it across
logos_module_accept_token under the initializer's own keys
(core / capability_module). Without it the cdylib's TokenManager (a
separate static copy of the singleton) is empty and every outbound
call — including the capability requestModule bootstrap — is rejected
as unauthorized.

* cdylib glue: provider derives LogosProviderBase; tokens land in BOTH stacks

informModuleToken = base save (host-stack TokenManager — what ModuleProxy
validates inbound calls against, incl. the grant the daemon pushes after
a capability requestModule) + C-ABI forward (the cdylib's own stack, for
outbound auth). init folds into onInit; the authToken property seeding
stays.

* lock: protocol integer-fidelity fix

* codegen: typed wrappers throw on call failure; dispatch catches escapes

Generated sync client wrappers call the new err-out invokeRemoteMethod
overload and throw logos::LogosCallError when the call fails (e.g. the
bound module is missing) — previously the empty QVariant silently
degraded to the return type's default and a caller could not tell
failure from a legitimate 0 / "". Both generators (legacy + LIDL),
both API styles. Async paths unchanged.

Generated provider dispatch (universal qt glue + LOGOS_PROVIDER) wraps
the method body in a catch-all that logs and returns an invalid QVariant
— an escaped exception becomes an ordinary METHOD_FAILED instead of
unwinding through Qt event dispatch and killing the module process.

* fix: restore a clean flake.lock after the merge conflict (protocol 176fbc8)

* codegen: CallError out-param instead of throwing wrappers

Per review, the generated sync wrappers expose the error channel as an
optional trailing parameter — add(a, b, &err) — rather than throwing:
explicit, stateless, works on temporaries, and existing call sites
compile unchanged (they keep default-on-failure, now with a qWarning so
failures are visible in the module log). The dispatch catch-all from the
previous commit stays: it contains author exceptions, it doesn't
introduce any.

* generator: contract-first C++ cdylib modules from --lidl

--lidl x.lidl --backend cdylib with --impl-class/--impl-header now emits
the FULL set (C-ABI export wrapper + events + uniform glue) around the
named hand-written Qt-free impl class — the C++ mirror of declaring the
contract in .lidl and implementing the Rust trait. Without --impl-class
the glue-only mode is unchanged.

* glue: fire onContextReady AFTER modules()/event wiring

The generated onInit set the context (which fires the impl's
onContextReady hook) before constructing the LogosModules aggregate and
wiring typed event emission — so an impl doing its documented one-time
setup there (typed dependency calls, event subscriptions) dereferenced
a null aggregate and crashed the module process (signal 11). Found by
the first module to subscribe to a dependency's typed event from
onContextReady. Context now goes last.

* cdylib: fire the context-ready hook at module LOAD, not first dispatch

The impl-exports wrapper used to fire maybeSetContext (stamp + hook)
immediately inside logos_module_set_context — which the glue calls during
onInit, BEFORE the auth token is seeded and BEFORE ModuleProxy wires the
emit callback. A hook that made outbound calls or emitted events ran
half-wired; and the Rust scaffold deferred its hook to first dispatch
entirely, so a Rust module couldn't subscribe/emit/act until someone
called it — a capability gap vs the universal C++ path.

Two changes, applied uniformly:
- The glue's onInit now seeds the auth token FIRST and forwards the
  context LAST (the same context-last convention as the universal onInit
  ordering fix).
- The impl-exports gained a ready-latch (lidlTryFireContext): the context
  is stored on set_context and the hook fires ONCE as soon as both the
  context AND the emit callback have been delivered — during module
  registration, before the module is published for inbound calls. Hosts
  that never wire an emit callback still get the hook before the first
  dispatch (requireEmit=false fallback in logos_module_dispatch).

The Rust scaffold (logos-rust-sdk lidl-gen) implements the same latch, so
on_context_ready now matches C++ onContextReady semantics: subscriptions,
authenticated outbound calls and typed emission all work from the hook at
load time.

* ci: run workflows on stacked PRs + workflow_dispatch

Both workflows filtered pull_request to master-based PRs, so stacked PRs
(feat/qt-free-sdk -> feat/extract-logos-protocol, feat/cdylib-authoring
-> feat/qt-free-sdk) ran NO checks at all. Drop the base-branch filter
for pull_request and add workflow_dispatch for manual runs. Same fix as
logos-module-builder 232b8a2.

* generator: --backend ui — universal authoring for UI plugin backends

New emitters (lidl_gen_ui.{h,cpp}) for type=ui_qml + interface=universal
modules: from the author's single clean impl class (optionally deriving
LogosModuleContext), generate
  <name>.rep            — the view contract, one SLOT per public method
                          (framework hooks like onContextReady excluded)
  <name>_ui_interface.h — PluginInterface subclass + IID
  <name>_ui_glue.{h,cpp}— plugin deriving <Cls>SimpleSource +
                          <Cls>Interface + <Cls>ViewPluginBase; slots
                          forward to the impl (std<->Qt at the boundary);
                          Q_INVOKABLE initLogos(LogosAPI*) builds
                          LogosModules, wires modules(), stamps context
                          (fires onContextReady — same order as the
                          universal core glue), then setBackend(this).

Typed dependency callers, typed event subscriptions and bind_<interface>
binders come from the existing umbrella pass (logos_sdk.h), which already
runs for UI modules. ui-host's reflection-based initLogos call site is
unchanged; legacy LogosAPI*-based UI plugins are untouched. v1 view API
types: void/int/uint/float64/bool/string; logos_events: rejected for ui
backends (views talk to QML via .rep, not module events).

* generator: distribute the LIDL frontend for external generators

First step of moving ALL Qt glue emission out of this repo into
logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the
Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib
impl-exports, LIDL derivation).

- Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd,
  lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used
  by both generators.
- The frontend set (AST, lexer, parser, serializer, validator,
  impl-header parser, emit-common) is installed under
  share/lidl-frontend/ — the qt generator compiles these sources in
  directly, so the two tools share one frontend without a binary ABI.

* generator: Qt glue emission removed — logos-qt-generator owns it

The deletion half of the generator split (counterpart: logos-qt-sdk
3b37474, builder d7a2272). This tool now emits ONLY Qt-free outputs:

  kept     std typed wrappers + logos_sdk umbrella (--general-only),
           cdylib C-ABI impl-exports + typed event emitters
           (--lidl/--from-header --backend cdylib --impl-class),
           LIDL derivation/serialization (--header-to-lidl), client stubs
  removed  universal Qt glue (--backend qt), the uniform cdylib Qt glue,
           the ui backend emitters — all relocated verbatim (byte-identical
           output verified) to logos-qt-generator; invocations here now
           fail with a pointer to the right tool

The provider-glue golden tests travel with the emitters (to be re-homed
in logos-qt-sdk's test suite); test_lidl_type_mapping stays — it covers
lidl_emit_common, which both generators compile.

* lock: protocol at the typed-requestModule port (3de5398)

* lock: protocol at the typed-requestModule port (3de5398)

* ci: chain pins for the doc-tests (drop at merge)

In repo CI only cpp-sdk's {release} is the commit under test —
logoscore-cli and module-builder expanded to master, which doesn't link
against the chain SDK the specs override in ('Build the CLI with the SDK
override' failed on every run since the stacked-PR triggers were
enabled). Pin both to the extraction-chain heads; the workspace pipeline
is unaffected (it pins every repo itself).

* generator: distribute the LIDL frontend for external generators

First step of moving ALL Qt glue emission out of this repo into
logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the
Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib
impl-exports, LIDL derivation).

- Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd,
  lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used
  by both generators.
- The frontend set (AST, lexer, parser, serializer, validator,
  impl-header parser, emit-common) is installed under
  share/lidl-frontend/ — the qt generator compiles these sources in
  directly, so the two tools share one frontend without a binary ABI.

* docs: note the generator split (Qt glue emission lives in logos-qt-sdk)

* lock: protocol#3 merged — pin advances to protocol master

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:10:16 -03:00

19 KiB

Layering note: since the protocol extraction and the Qt split, this repo is the Qt-free base SDK — header-only developer surface for universal module implementations (logos_module_context.h, logos_result.h, logos_json.h) plus the logos-cpp-generator code generator (Qt-free outputs: std typed wrappers, the logos_sdk umbrella, cdylib C-ABI impl-exports, LIDL derivation — ALL generated Qt glue comes from logos-qt-sdk's logos-qt-generator). Transports, the consumer core and the lp_* C ABI live in logos-protocol; the Qt developer layer (LogosAPI, provider base classes, QObject glue) lives in logos-qt-sdk.

logos-cpp-sdk

How to Build

The project includes a Nix flake for reproducible builds with a modular structure:

Build Complete SDK (Library + Headers + Generator)

# 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 Individual Components

# 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'

Development Shell

# Enter development shell with all dependencies
nix develop

Note: 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/

Modular Architecture

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 compilation
  • nix/lib.nix - SDK library compilation
  • nix/include.nix - Header installation
  • nix/tests.nix - Test suite (build + run via nix build '.#tests')

Run 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*"

Manual Build

Building the C++ SDK

cd cpp
./compile.sh

Building the Code Generator

cd cpp-generator
./compile.sh

The generator binary will be available at build/bin/logos-cpp-generator.

Usage

Code Generator

The logos-cpp-generator tool generates C++ wrapper code for Logos plugins.

Basic Usage

# 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

Generate from Metadata

# 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

Options

--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 .h and .cpp files
  • 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 dependencies array
  • For each dependency (e.g., "waku_module"), it will:
    • Include waku_module_api.h in the header
    • Include waku_module_api.cpp in the source
    • Create a WakuModule waku_module; member in the LogosModules struct
  • Does not require --module-dir since it doesn't process plugins

Generated Files

By default (without --module-only or --general-only):

  • <module>_api.h and <module>_api.cpp - Wrapper code for each module
  • core_manager_api.h and core_manager_api.cpp - Core manager wrapper
  • logos_sdk.h and logos_sdk.cpp - Umbrella headers including all modules

With --module-only:

  • Only <module>_api.h and <module>_api.cpp - Wrapper code for the requested module(s)

With --general-only:

  • Only core_manager_api.h and core_manager_api.cpp - Core manager wrapper
  • Only logos_sdk.h and logos_sdk.cpp - Umbrella headers that reference existing module files

Typical Workflow

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 ./generated

This approach gives you fine-grained control over which modules to include and allows rebuilding just the umbrella headers without regenerating all module wrappers.

Universal modules: LogosModuleContext

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 LogosModules aggregate 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.
    }
};

Documenting methods: a doc comment (/// or /** … */) directly above a method declaration becomes that method's description in the generated getMethods() output, so it surfaces in lm methods, logoscore module-info, and Basecamp's Methods list — no describe call needed:

/// Processes the input and returns a result map.
LogosMap doWork(const std::string& input);

Plain // and /* … */ comments are ignored (so section separators don't leak into the API). The same applies to interface: "provider" modules whose methods are marked with LOGOS_METHOD. See cpp-generator/docs/spec.mdMethod documentation for details.

Documenting events: events are the other half of a module's API — declared in a logos_events: section and surfaced the same way. A doc comment above an event declaration becomes that event's description. Events are reported inside the generated getMethods() output (each entry tagged type: "event", methods tagged type: "method"); the framework exposes filtered views — getPluginMethods(), getPluginEvents(), getPluginInterface() — so the event surfaces in lm events, logoscore module-info's Events section, and Basecamp's Interface screen:

logos_events:
    /// Emitted once the user has authenticated.
    /// Carries the freshly issued session token.
    void userLoggedIn(const std::string& userId, const std::string& token);

Event entries carry type: "event", name, signature, parameters[], and description (no returnType — events are fire-and-forget). Folding events into getMethods() rather than adding a getEvents() vtable method keeps the provider ABI stable across SDK versions. See cpp-generator/docs/spec.mdEvent documentation for details.

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).

API style: Qt vs std

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 to interface: "universal" — there's no per-flag override on metadata.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.

Events: logos_events:

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).

API

LogosResult

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.

Example

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>();
}

Complex objects

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();
    }
}

Consuming the SDK

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.

Transports

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.

Requirements

Build Tools

  • CMake (3.x or later)
  • Ninja build system
  • pkg-config

Dependencies

  • Qt6 (qtbase)
  • Qt6 Remote Objects (qtremoteobjects)
  • Boost (system)
  • OpenSSL
  • nlohmann_json

Supported Platforms

  • macOS (aarch64-darwin, x86_64-darwin)
  • Linux (aarch64-linux, x86_64-linux)