Skip to content

logos-co/logos-view-module-runtime

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 (recommended)

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.

About

taking care of running UI apps backend /Qt plugin in its own separate process

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors