Dario Gabriel LipicarandClaude Opus 5 5002816546 ci: run the test suite on every push and pull request
This repo had no CI at all. Both open PRs (#20, #21) report "no checks
reported on the branch", so every green result claimed for the QML bridge
changes came from a local run on one machine, on one platform -- which is
how several defects in that change set shipped green in the first place.

The suite is already a nix derivation whose checkPhase runs ctest over
every test target (nix/test.nix), so building the check IS running the
tests; there is no separate run step to keep in sync. Both platforms run
because the bridge coordinates processes and sockets, and the two OSes
have already disagreed about exactly that elsewhere in the stack.

Modelled on logos-protocol's workflow, including the DeterminateSystems
installer (cachix/install-nix-action trips over pre-existing nix build
users on the macOS runners) and a non-fatal cachix push.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 16:51:14 -03:00
2026-07-23 09:34:58 +02:00
2026-07-23 09:34:58 +02:00
2026-07-14 15:41:18 +02:00
2026-07-14 15:41:18 +02:00

logos-view-module-runtime

Shared runtime for hosting Logos view modules (Qt/QML UI plugins) in a child process, isolated from the main application.

This repo exists so that logos-basecamp, logos-standalone-app, and any future Logos host application can link the same library and use the same ui-host binary instead of each one carrying its own copy.

What's in here

  • logos_view_module_runtime — static C++ library, linked into host applications (or their plugins). Provides:

    • LogosQmlBridgeQObject exposed to QML as Logos. Routes callModule(module, method, args) calls either to a regular backend module via LogosAPI (IPC), or to a view module via a private QRemoteObjectDynamicReplica. Results are serialized to JSON strings so QML always sees a string.
    • ViewModuleHost — spawns a ui-host child process for a given view module plugin, generates a unique local socket name, watches stdout for READY, and emits ready(). The parent then points LogosQmlBridge at that socket via setViewModuleSocket(name, socket).
  • ui-host — standalone executable. Loads a single Qt plugin (--path <plugin.so>), calls initLogos(LogosAPI*) on it via reflection (QMetaObject::invokeMethod), and then exposes a QObject on a QRemoteObjectHost at the socket given by --socket. Remoting strategy:

    • Typed remoting (preferred): if the plugin declares the LogosViewPlugin interface (from logos-plugin-qt) via Q_INTERFACES(LogosViewPlugin) so that qobject_cast<LogosViewPlugin*> succeeds, ui-host calls viewPlugin->enableRemoting(&host). The generated <Foo>ViewPluginBase (produced by logos_module(REP_FILE …) in logos-plugin-qt) invokes host->enableRemoting<FooSourceAPI>(backend) so typed replicas on the client side reach the Valid state. The remoted object is viewPlugin->viewObject().
    • Dynamic remoting (fallback): for plugins without a .rep / LogosViewPlugin implementation, ui-host falls back to host.enableRemoting(pluginObject, moduleName), which propagates all Q_INVOKABLEs, slots, signals, and Q_PROPERTYs (with NOTIFY) via a QRemoteObjectDynamicReplica on the client side.

    Any Q_PROPERTY on the remoted object whose value is a QAbstractItemModel* is additionally remoted as a child source named <moduleName>/<propertyName>. Prints READY once it's listening.

View object convention

A view module plugin keeps its plugin-lifecycle class separate from the QObject that QML actually talks to. The preferred path is to inherit the generated <Foo>ViewPluginBase from logos-plugin-qt (produced by logos_module(REP_FILE my_view.rep …)), which implements LogosViewPlugin and wires typed remoting:

class MyPlugin : public MyViewPluginBase {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "co.logos.MyPlugin" FILE "metadata.json")
    Q_INTERFACES(PluginInterface LogosViewPlugin)
public:
    Q_INVOKABLE void initLogos(LogosAPI* api) {
        m_backend = new MyBackend(api, this);
    }
    QObject* viewObject() override { return m_backend; }
private:
    MyBackend* m_backend = nullptr;
};

ui-host calls viewPlugin->enableRemoting(&host), which internally does host->enableRemoting<MySourceAPI>(m_backend) using the typed source generated from the .rep file. QML on the parent side talks to MyBackend via a typed replica.

For plugins without a .rep file (no LogosViewPlugin implementation), ui-host falls back to dynamic remoting of the plugin object itself — this keeps legacy modules working unchanged.

Architecture

┌────────────────────────────┐         ┌──────────────────────────┐
│ Host app (basecamp / etc.) │         │ ui-host (child process)  │
│                            │         │                          │
│   QML  ──logos.callModule──▶         │   ViewModuleProxy        │
│           │                │ QRO     │     │                    │
│   LogosQmlBridge ──────────┼────────▶│     ▼                    │
│           │                │ local   │   QPluginLoader          │
│           ▼                │ socket  │     │                    │
│   ViewModuleHost ──spawn──▶│         │     ▼                    │
│                            │         │   <view module>.so       │
└────────────────────────────┘         └──────────────────────────┘

Each view module gets its own ui-host process and its own private socket, so a crash or hang in one view module cannot take down the host app or other view modules.

Non-view backend modules continue to use the existing LogosAPI IPC path unchanged — LogosQmlBridge only switches to QRO when the requested module name was previously registered via setViewModuleSocket.

Building

nix build .#default

Outputs:

  • result/lib/liblogos_view_module_runtime.a
  • result/include/ — public headers
  • result/bin/ui-host

CMake (manual)

cmake -S . -B build -GNinja \
  -DLOGOS_CPP_SDK_ROOT=/path/to/logos-cpp-sdk
cmake --build build
cmake --install build --prefix ./out

LOGOS_CPP_SDK_ROOT is required and must point at an installed logos-cpp-sdk (provides logos_api.h and liblogos_sdk).

Consuming from another repo

In the consumer's flake.nix:

inputs.logos-view-module-runtime.url = "github:logos-co/logos-view-module-runtime";

Pass the package into the consumer's app derivation and forward it as a CMake variable:

cmakeFlags = [
  "-DLOGOS_VIEW_MODULE_RUNTIME_ROOT=${logosViewModuleRuntime}"
];

In the consumer's CMakeLists.txt:

target_include_directories(my_app PRIVATE ${LOGOS_VIEW_MODULE_RUNTIME_ROOT}/include)
target_link_directories(my_app PRIVATE ${LOGOS_VIEW_MODULE_RUNTIME_ROOT}/lib)
target_link_libraries(my_app PRIVATE logos_view_module_runtime)

The ui-host binary should be copied into the app's bin/ directory at install time so ViewModuleHost can QProcess::start("ui-host", ...) it:

cp ${logosViewModuleRuntime}/bin/ui-host $out/bin/ui-host

Using the bridge

auto* api = new LogosAPI(/* ... */);
auto* bridge = new LogosQmlBridge(api, this);
engine.rootContext()->setContextProperty("logos", bridge);

// For a view module, spawn its host process and wire the bridge to its socket
auto* host = new ViewModuleHost(this);
connect(host, &ViewModuleHost::ready, this, [bridge, host] {
    bridge->setViewModuleSocket("my_view_module", host->socketName());
});
if (!host->spawn("my_view_module", "/path/to/my_view_module.so")) {
    qWarning() << "Failed to start view module host";
}

From QML:

import QtQuick
Item {
    Component.onCompleted: {
        // Prefer the async form for view modules — the sync callModule() blocks
        // the QML/JS event loop while waiting for the QRO reply.
        logos.callModuleAsync("my_view_module", "getStatus", [], function(payload) {
            const result = JSON.parse(payload);
            console.log(result.value);
        });
    }
}

Dependencies

  • Qt 6: Core, Qml, RemoteObjects
  • logos-cpp-sdk (for LogosAPI / logos_api.h)

That's it — deliberately no dependency on logos-liblogos, logos-module, or any specific module repo, so this runtime stays a thin shared layer.

S
Description
taking care of running UI apps backend /Qt plugin in its own separate process
Readme
668 KiB
Languages
C++ 91.5%
CMake 5%
Nix 3.5%