Dario Gabriel LipicarandClaude Opus 5 c6933da1de chore(deps): re-pin logos-protocol to 07b0fb1 (#47)
This branch calls onEventWhenAvailable, cancelEventSubscription,
eventSubscriptionState and pendingEventSubscriptions. The lock pinned 0f26ffd,
which has none of them, so every green run of this branch until now was produced
with --override-input and the lock itself had never resolved.

07b0fb1 is #47's merge commit. Its narHash is identical to the branch tip the
bridge was verified against, so this pin is byte-for-byte the tree those runs
used, not merely a compatible one.

VERIFIED FROM THE LOCK, which is the part that was missing:

    nix build 'path:./#checks.aarch64-darwin.default'    # no override of any kind
    100% tests passed, 0 tests failed out of 6

Driving the repo's OWN flake matters here. Building the workspace flake's
logos-view-module-runtime target does not test this lock at all: the workspace
supplies logos-protocol to every consumer through `follows`, so it answers a
different question and will happily go green (or red) on a pin this repo does not
use. The first attempt at this verification made exactly that mistake and failed
with "no member named 'onEventWhenAvailable'" while the lock was already correct.

Note the closure still holds two logos-protocol revisions: root and logos-qt-sdk
resolve to 07b0fb1, while logos-cpp-sdk keeps its own, because only qt-sdk
declares `inputs.logos-protocol.follows`. That is the configuration verified
above and it builds clean. Adding the missing follows to logos-cpp-sdk would
collapse it to one revision and is worth doing -- separately, since it changes a
configuration nothing has tested yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:48:16 -03: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%