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.
-
logos_view_module_runtime— static C++ library, linked into host applications (or their plugins). Provides:LogosQmlBridge—QObjectexposed to QML asLogos. RoutescallModule(module, method, args)calls either to a regular backend module viaLogosAPI(IPC), or to a view module via a privateQRemoteObjectDynamicReplica. Results are serialized to JSON strings so QML always sees a string.ViewModuleHost— spawns aui-hostchild process for a given view module plugin, generates a unique local socket name, watches stdout forREADY, and emitsready(). The parent then pointsLogosQmlBridgeat that socket viasetViewModuleSocket(name, socket).
-
ui-host— standalone executable. Loads a single Qt plugin (--path <plugin.so>), callsinitLogos(LogosAPI*)on it via reflection (QMetaObject::invokeMethod), and then exposes a QObject on aQRemoteObjectHostat the socket given by--socket. Remoting strategy:- Typed remoting (preferred): if the plugin declares the
LogosViewPlugininterface (fromlogos-plugin-qt) viaQ_INTERFACES(LogosViewPlugin)so thatqobject_cast<LogosViewPlugin*>succeeds,ui-hostcallsviewPlugin->enableRemoting(&host). The generated<Foo>ViewPluginBase(produced bylogos_module(REP_FILE …)inlogos-plugin-qt) invokeshost->enableRemoting<FooSourceAPI>(backend)so typed replicas on the client side reach theValidstate. The remoted object isviewPlugin->viewObject(). - Dynamic remoting (fallback): for plugins without a
.rep/LogosViewPluginimplementation,ui-hostfalls back tohost.enableRemoting(pluginObject, moduleName), which propagates allQ_INVOKABLEs, slots, signals, andQ_PROPERTYs (withNOTIFY) via aQRemoteObjectDynamicReplicaon the client side.
Any
Q_PROPERTYon the remoted object whose value is aQAbstractItemModel*is additionally remoted as a child source named<moduleName>/<propertyName>. PrintsREADYonce it's listening. - Typed remoting (preferred): if the plugin declares the
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.
┌────────────────────────────┐ ┌──────────────────────────┐
│ 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.
nix build .#defaultOutputs:
result/lib/liblogos_view_module_runtime.aresult/include/— public headersresult/bin/ui-host
cmake -S . -B build -GNinja \
-DLOGOS_CPP_SDK_ROOT=/path/to/logos-cpp-sdk
cmake --build build
cmake --install build --prefix ./outLOGOS_CPP_SDK_ROOT is required and must point at an installed
logos-cpp-sdk (provides logos_api.h and liblogos_sdk).
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-hostauto* 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);
});
}
}- Qt 6:
Core,Qml,RemoteObjects logos-cpp-sdk(forLogosAPI/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.