diff --git a/logos-calc-module/CMakeLists.txt b/logos-calc-module/CMakeLists.txt new file mode 100644 index 0000000..e796327 --- /dev/null +++ b/logos-calc-module/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.14) +project(CalcModulePlugin LANGUAGES CXX) + +# Include the Logos Module CMake helper +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake") + include(cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found") +endif() + +# Define the module with its external library dependency +logos_module( + NAME calc_module + SOURCES + src/calc_module_interface.h + src/calc_module_plugin.h + src/calc_module_plugin.cpp + EXTERNAL_LIBS + calc +) diff --git a/logos-calc-module/flake.lock b/logos-calc-module/flake.lock new file mode 100644 index 0000000..f46d117 --- /dev/null +++ b/logos-calc-module/flake.lock @@ -0,0 +1,406 @@ +{ + "nodes": { + "logos-capability-module": { + "inputs": { + "logos-cpp-sdk": "logos-cpp-sdk_2", + "logos-liblogos": "logos-liblogos_2", + "nixpkgs": [ + "logos-module-builder", + "logos-liblogos", + "logos-capability-module", + "logos-liblogos", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767809111, + "narHash": "sha256-jehjsB+BpDJlVu3I7x+vFVOdXmy9MDmFTJtRqzFUONo=", + "owner": "logos-co", + "repo": "logos-capability-module", + "rev": "7b35383e0aa4e28a4633ed18a87efb57636939b1", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-capability-module", + "type": "github" + } + }, + "logos-cpp-sdk": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1772028960, + "narHash": "sha256-BDWFjaKeoJW8oWDlPphNINt5U3P1xt1z1Y4f9jyC7uU=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "95f763b48d74bcdc63093b05159f43500cab139e", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_2": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1761230734, + "narHash": "sha256-CMRUwXH7pJZ1OI6bd/TDDDXKqQ1tQZHQEOOwK8TgYHI=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "4b143922c190df00bb3835441c9f0075cb28283b", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_3": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1761230734, + "narHash": "sha256-CMRUwXH7pJZ1OI6bd/TDDDXKqQ1tQZHQEOOwK8TgYHI=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "4b143922c190df00bb3835441c9f0075cb28283b", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_4": { + "inputs": { + "nixpkgs": "nixpkgs_4" + }, + "locked": { + "lastModified": 1772028960, + "narHash": "sha256-BDWFjaKeoJW8oWDlPphNINt5U3P1xt1z1Y4f9jyC7uU=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "95f763b48d74bcdc63093b05159f43500cab139e", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-cpp-sdk_5": { + "inputs": { + "nixpkgs": "nixpkgs_5" + }, + "locked": { + "lastModified": 1767724329, + "narHash": "sha256-UPkqxqxbKwU5Dmu00TnjiJVXUmfVylF3p1qziEuYwIE=", + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "rev": "32f1d7080d784ff044d91d076ef2f0c7305d4784", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-cpp-sdk", + "type": "github" + } + }, + "logos-liblogos": { + "inputs": { + "logos-capability-module": "logos-capability-module", + "logos-cpp-sdk": "logos-cpp-sdk_4", + "logos-module": "logos-module", + "nix-bundle-appimage": "nix-bundle-appimage", + "nix-bundle-dir": "nix-bundle-dir_2", + "nixpkgs": [ + "logos-module-builder", + "logos-liblogos", + "logos-cpp-sdk", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1772115748, + "narHash": "sha256-sPdAuYiLOjsulrk+uKMT7EG05ZlGT7OYEpgUh+f0nME=", + "owner": "logos-co", + "repo": "logos-liblogos", + "rev": "07780444deb99f10e600247e3696ba495f2f071a", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-liblogos", + "type": "github" + } + }, + "logos-liblogos_2": { + "inputs": { + "logos-cpp-sdk": "logos-cpp-sdk_3", + "nixpkgs": [ + "logos-module-builder", + "logos-liblogos", + "logos-capability-module", + "logos-liblogos", + "logos-cpp-sdk", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1761845775, + "narHash": "sha256-ulK8xq05ejK6qIgZ7WtWb/MJt2rk5BKfDA2z7mM3wq8=", + "owner": "logos-co", + "repo": "logos-liblogos", + "rev": "a92c2c1268bc70764c8f73c7bce07d21024f5af9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-liblogos", + "type": "github" + } + }, + "logos-module": { + "inputs": { + "logos-cpp-sdk": "logos-cpp-sdk_5", + "nixpkgs": [ + "logos-module-builder", + "logos-liblogos", + "logos-module", + "logos-cpp-sdk", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770999556, + "narHash": "sha256-anpsEniGTTwUAwknRxjaT9GP4avHzIsolEHdHDTV9rM=", + "owner": "logos-co", + "repo": "logos-module", + "rev": "d1b35f335f938bb5de21a2a6010f1104075bdb1c", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module", + "type": "github" + } + }, + "logos-module-builder": { + "inputs": { + "logos-cpp-sdk": "logos-cpp-sdk", + "logos-liblogos": "logos-liblogos", + "nixpkgs": [ + "logos-module-builder", + "logos-cpp-sdk", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1770934129, + "narHash": "sha256-RjeB17MQh4HcS5vKtSrDM/J7fz3K8ZDNpswCGr5JF/k=", + "owner": "logos-co", + "repo": "logos-module-builder", + "rev": "a86eea50450511f4474b35032e5fd218a0f17c3c", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "logos-module-builder", + "type": "github" + } + }, + "nix-bundle-appimage": { + "inputs": { + "nix-bundle-dir": "nix-bundle-dir", + "nixpkgs": "nixpkgs_6" + }, + "locked": { + "lastModified": 1772047346, + "narHash": "sha256-RUsTUxKCxuQ3+D2LfBbK0EX1vF7HNMkpWgOGFfZbrEg=", + "owner": "logos-co", + "repo": "nix-bundle-appimage", + "rev": "4d68437c97ac59c3c70c1b2b116235c434d571a8", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-appimage", + "type": "github" + } + }, + "nix-bundle-dir": { + "inputs": { + "nixpkgs": [ + "logos-module-builder", + "logos-liblogos", + "nix-bundle-appimage", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771971384, + "narHash": "sha256-fq0H+sxQhkGN054jdN+ZfHZibbOjHA+KD5SpRH78T1g=", + "owner": "logos-co", + "repo": "nix-bundle-dir", + "rev": "1ecb9662145a1ad84007a970b4bef50a4af159c9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-dir", + "type": "github" + } + }, + "nix-bundle-dir_2": { + "inputs": { + "nixpkgs": "nixpkgs_7" + }, + "locked": { + "lastModified": 1771971384, + "narHash": "sha256-fq0H+sxQhkGN054jdN+ZfHZibbOjHA+KD5SpRH78T1g=", + "owner": "logos-co", + "repo": "nix-bundle-dir", + "rev": "1ecb9662145a1ad84007a970b4bef50a4af159c9", + "type": "github" + }, + "original": { + "owner": "logos-co", + "repo": "nix-bundle-dir", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_3": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_4": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_5": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1771848320, + "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_7": { + "locked": { + "lastModified": 1770562336, + "narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d6c71932130818840fc8fe9509cf50be8c64634f", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "logos-module-builder": "logos-module-builder", + "nixpkgs": [ + "logos-module-builder", + "nixpkgs" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/logos-calc-module/flake.nix b/logos-calc-module/flake.nix new file mode 100644 index 0000000..137f680 --- /dev/null +++ b/logos-calc-module/flake.nix @@ -0,0 +1,14 @@ +{ + description = "Calculator module - wraps libcalc C library for Logos"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + nixpkgs.follows = "logos-module-builder/nixpkgs"; + }; + + outputs = { self, logos-module-builder, nixpkgs }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./module.yaml; + }; +} diff --git a/logos-calc-module/lib/libcalc.c b/logos-calc-module/lib/libcalc.c new file mode 100644 index 0000000..07231ba --- /dev/null +++ b/logos-calc-module/lib/libcalc.c @@ -0,0 +1,41 @@ +#include "libcalc.h" + +int calc_add(int a, int b) +{ + return a + b; +} + +int calc_multiply(int a, int b) +{ + return a * b; +} + +int calc_factorial(int n) +{ + if (n < 0) return -1; + if (n <= 1) return 1; + int result = 1; + for (int i = 2; i <= n; i++) { + result *= i; + } + return result; +} + +int calc_fibonacci(int n) +{ + if (n < 0) return -1; + if (n == 0) return 0; + if (n == 1) return 1; + int a = 0, b = 1; + for (int i = 2; i <= n; i++) { + int tmp = a + b; + a = b; + b = tmp; + } + return b; +} + +const char* calc_version(void) +{ + return "1.0.0"; +} diff --git a/logos-calc-module/lib/libcalc.h b/logos-calc-module/lib/libcalc.h new file mode 100644 index 0000000..8365297 --- /dev/null +++ b/logos-calc-module/lib/libcalc.h @@ -0,0 +1,34 @@ +/** + * libcalc - A tiny calculator C library + * + * This is a minimal C library used to demonstrate + * wrapping a C library as a Logos module. + */ + +#ifndef LIBCALC_H +#define LIBCALC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** Add two integers. */ +int calc_add(int a, int b); + +/** Multiply two integers. */ +int calc_multiply(int a, int b); + +/** Compute factorial of n (n must be >= 0). Returns -1 on error. */ +int calc_factorial(int n); + +/** Compute the nth Fibonacci number (n must be >= 0). Returns -1 on error. */ +int calc_fibonacci(int n); + +/** Return the library version string. Caller must NOT free. */ +const char* calc_version(void); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBCALC_H */ diff --git a/logos-calc-module/lib/libcalc.so b/logos-calc-module/lib/libcalc.so new file mode 100755 index 0000000..801d018 Binary files /dev/null and b/logos-calc-module/lib/libcalc.so differ diff --git a/logos-calc-module/metadata.json b/logos-calc-module/metadata.json new file mode 100644 index 0000000..f9720ec --- /dev/null +++ b/logos-calc-module/metadata.json @@ -0,0 +1,10 @@ +{ + "name": "calc_module", + "version": "1.0.0", + "description": "Calculator module wrapping libcalc C library", + "author": "", + "type": "core", + "category": "general", + "main": "calc_module_plugin", + "dependencies": [] +} diff --git a/logos-calc-module/module.yaml b/logos-calc-module/module.yaml new file mode 100644 index 0000000..f9f6628 --- /dev/null +++ b/logos-calc-module/module.yaml @@ -0,0 +1,22 @@ +name: calc_module +version: 1.0.0 +type: core +category: general +description: "Calculator module wrapping libcalc C library" + +dependencies: [] + +nix_packages: + build: [] + runtime: [] + +external_libraries: + - name: calc + vendor_path: "lib" + +cmake: + find_packages: [] + extra_sources: [] + extra_include_dirs: + - lib + extra_link_libraries: [] diff --git a/logos-calc-module/src/calc_module_interface.h b/logos-calc-module/src/calc_module_interface.h new file mode 100644 index 0000000..e01215c --- /dev/null +++ b/logos-calc-module/src/calc_module_interface.h @@ -0,0 +1,23 @@ +#ifndef CALC_MODULE_INTERFACE_H +#define CALC_MODULE_INTERFACE_H + +#include +#include +#include "interface.h" + +class CalcModuleInterface : public PluginInterface +{ +public: + virtual ~CalcModuleInterface() = default; + + Q_INVOKABLE virtual int add(int a, int b) = 0; + Q_INVOKABLE virtual int multiply(int a, int b) = 0; + Q_INVOKABLE virtual int factorial(int n) = 0; + Q_INVOKABLE virtual int fibonacci(int n) = 0; + Q_INVOKABLE virtual QString libVersion() = 0; +}; + +#define CalcModuleInterface_iid "org.logos.CalcModuleInterface" +Q_DECLARE_INTERFACE(CalcModuleInterface, CalcModuleInterface_iid) + +#endif // CALC_MODULE_INTERFACE_H diff --git a/logos-calc-module/src/calc_module_plugin.cpp b/logos-calc-module/src/calc_module_plugin.cpp new file mode 100644 index 0000000..4b2cca8 --- /dev/null +++ b/logos-calc-module/src/calc_module_plugin.cpp @@ -0,0 +1,56 @@ +#include "calc_module_plugin.h" +#include "logos_api.h" +#include + +CalcModulePlugin::CalcModulePlugin(QObject* parent) + : QObject(parent) +{ + qDebug() << "CalcModulePlugin: created"; +} + +CalcModulePlugin::~CalcModulePlugin() +{ + qDebug() << "CalcModulePlugin: destroyed"; +} + +void CalcModulePlugin::initLogos(LogosAPI* api) +{ + m_logosAPI = api; + qDebug() << "CalcModulePlugin: LogosAPI initialized"; +} + +int CalcModulePlugin::add(int a, int b) +{ + int result = calc_add(a, b); + qDebug() << "CalcModulePlugin::add" << a << "+" << b << "=" << result; + return result; +} + +int CalcModulePlugin::multiply(int a, int b) +{ + int result = calc_multiply(a, b); + qDebug() << "CalcModulePlugin::multiply" << a << "*" << b << "=" << result; + return result; +} + +int CalcModulePlugin::factorial(int n) +{ + int result = calc_factorial(n); + qDebug() << "CalcModulePlugin::factorial" << n << "! =" << result; + return result; +} + +int CalcModulePlugin::fibonacci(int n) +{ + int result = calc_fibonacci(n); + qDebug() << "CalcModulePlugin::fibonacci fib(" << n << ") =" << result; + return result; +} + +QString CalcModulePlugin::libVersion() +{ + const char* ver = calc_version(); + QString result = QString::fromUtf8(ver); + qDebug() << "CalcModulePlugin::libVersion" << result; + return result; +} diff --git a/logos-calc-module/src/calc_module_plugin.h b/logos-calc-module/src/calc_module_plugin.h new file mode 100644 index 0000000..5d7723e --- /dev/null +++ b/logos-calc-module/src/calc_module_plugin.h @@ -0,0 +1,42 @@ +#ifndef CALC_MODULE_PLUGIN_H +#define CALC_MODULE_PLUGIN_H + +#include +#include +#include "calc_module_interface.h" + +// Include the C library header +#include "lib/libcalc.h" + +class LogosAPI; + +class CalcModulePlugin : public QObject, public CalcModuleInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID CalcModuleInterface_iid FILE "metadata.json") + Q_INTERFACES(CalcModuleInterface PluginInterface) + +public: + explicit CalcModulePlugin(QObject* parent = nullptr); + ~CalcModulePlugin() override; + + // PluginInterface + QString name() const override { return "calc_module"; } + QString version() const override { return "1.0.0"; } + Q_INVOKABLE void initLogos(LogosAPI* api); + + // CalcModuleInterface — each wraps a libcalc C function + Q_INVOKABLE int add(int a, int b) override; + Q_INVOKABLE int multiply(int a, int b) override; + Q_INVOKABLE int factorial(int n) override; + Q_INVOKABLE int fibonacci(int n) override; + Q_INVOKABLE QString libVersion() override; + +signals: + void eventResponse(const QString& eventName, const QVariantList& args); + +private: + LogosAPI* m_logosAPI = nullptr; +}; + +#endif // CALC_MODULE_PLUGIN_H diff --git a/logos-developer-guide.md b/logos-developer-guide.md new file mode 100644 index 0000000..644bd5a --- /dev/null +++ b/logos-developer-guide.md @@ -0,0 +1,1113 @@ +# Logos Module Developer Guide + +A comprehensive guide to creating, building, testing, packaging, and distributing modules for the Logos platform. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Prerequisites](#prerequisites) +- [Part 1: Creating a Module](#part-1-creating-a-module) + - [1.1 Scaffold with logos-module-builder](#11-scaffold-with-logos-module-builder) + - [1.2 Project Structure](#12-project-structure) + - [1.3 The module.yaml Configuration](#13-the-moduleyaml-configuration) + - [1.4 Writing Module Code](#14-writing-module-code) + - [1.5 Building Your Module](#15-building-your-module) +- [Part 2: Inspecting and Testing Your Module](#part-2-inspecting-and-testing-your-module) + - [2.1 The lm CLI Tool](#21-the-lm-cli-tool) + - [2.2 Running with logoscore](#22-running-with-logoscore) + - [2.3 The logos-module-viewer](#23-the-logos-module-viewer) +- [Part 3: Packaging Your Module](#part-3-packaging-your-module) + - [3.1 The LGX Package Format](#31-the-lgx-package-format) + - [3.2 Creating a Package with lgx](#32-creating-a-package-with-lgx) + - [3.3 Verifying Packages](#33-verifying-packages) +- [Part 4: Installing and Managing Modules](#part-4-installing-and-managing-modules) + - [4.1 The lgpm CLI](#41-the-lgpm-cli) + - [4.2 Installing from Local Files](#42-installing-from-local-files) + - [4.3 Installing from a Registry](#43-installing-from-a-registry) +- [Part 5: Running in the Logos App](#part-5-running-in-the-logos-app) + - [5.1 Building logos-app](#51-building-logos-app) + - [5.2 Module Types in logos-app](#52-module-types-in-logos-app) + - [5.3 Development Mode](#53-development-mode) +- [Part 6: Inter-Module Communication](#part-6-inter-module-communication) + - [6.1 The LogosAPI](#61-the-logosapi) + - [6.2 The C++ SDK Code Generator](#62-the-c-sdk-code-generator) + - [6.3 LogosResult](#63-logosresult) + - [6.4 Communication Modes](#64-communication-modes) +- [Part 7: Advanced Topics](#part-7-advanced-topics) + - [7.1 Wrapping External Libraries](#71-wrapping-external-libraries) + - [7.2 UI Modules (C++ Widgets)](#72-ui-modules-c-widgets) + - [7.3 UI Modules (QML)](#73-ui-modules-qml) + - [7.4 Module Dependencies](#74-module-dependencies) +- [Reference: Repository Map](#reference-repository-map) +- [Reference: CLI Tools Summary](#reference-cli-tools-summary) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The **Logos platform** is a modular application framework built in C++ on top of Qt 6. Applications are composed of dynamically loaded **modules** (plugins) that communicate via an IPC layer. The platform provides: + +- **Process isolation** -- each module runs in its own host process (on desktop), communicating via Qt Remote Objects +- **Cross-platform support** -- macOS (arm64, x86_64) and Linux (arm64, x86_64) +- **A package format** (`.lgx`) for distributing modules with platform-specific variants +- **A desktop application shell** (`logos-app`) with a sidebar, tabbed workspace, and plugin management UI +- **A CLI runtime** (`logoscore`) for running modules headlessly + +## Architecture + +``` ++---------------------------------------------------------------+ +| Application Layer | +| logos-app (Desktop GUI) or logoscore (CLI Runtime) | ++---------------------------------------------------------------+ + | | | + v v v ++---------------+ +------------------+ +------------------+ +| Module A | | Module B | | Package Manager | +| (logos_host) | | (logos_host) | | Module | ++-------+-------+ +--------+---------+ +--------+---------+ + | | | + | Qt Remote Objects (IPC) | + +--------------------------------------------+ + | + +--------v---------+ + | liblogos | (Core Runtime) + | logos-liblogos | + +--------+---------+ + | + +--------v---------+ + | logos-cpp-sdk | (SDK: LogosAPI, + | | Code Generator, + | | Types, IPC) + +------------------+ +``` + +**Key components:** + +| Component | Repository | Role | +|-----------|-----------|------| +| **logos-module-builder** | [logos-co/logos-module-builder](https://github.com/logos-co/logos-module-builder) | Scaffolding and build system for new modules | +| **logos-module** | [logos-co/logos-module](https://github.com/logos-co/logos-module) | Plugin loading/introspection library + `lm` CLI | +| **logos-cpp-sdk** | [logos-co/logos-cpp-sdk](https://github.com/logos-co/logos-cpp-sdk) | C++ SDK, types, IPC layer, code generator | +| **logos-liblogos** | [logos-co/logos-liblogos](https://github.com/logos-co/logos-liblogos) | Core runtime (`logoscore`, `logos_host`, `liblogos_core`) | +| **logos-package** | [logos-co/logos-package](https://github.com/logos-co/logos-package) | LGX package format library + `lgx` CLI | +| **logos-package-manager-module** | [logos-co/logos-package-manager-module](https://github.com/logos-co/logos-package-manager-module) | Package manager module + `lgpm` CLI | +| **logos-app** | [logos-co/logos-co/logos-app](https://github.com/logos-co/logos-app) | Desktop application shell | + +## Prerequisites + +### Required + +- **Nix** with flakes enabled. This is the primary build tool for the entire ecosystem. Install Nix from [nixos.org](https://nixos.org/download.html), then enable flakes: + + ```bash + # If you need experimental features enabled per-command: + nix --extra-experimental-features "nix-command flakes" + + # Or enable globally in ~/.config/nix/nix.conf: + experimental-features = nix-command flakes + ``` + +### Recommended Knowledge + +- C++ (C++17) +- Qt 6 basics (`QObject`, `Q_INVOKABLE`, `Q_PLUGIN_METADATA`, signals/slots) +- Basic CMake +- Basic Nix concepts (flakes, derivations) + +--- + +## Part 1: Creating a Module + +### 1.1 Scaffold with logos-module-builder + +The fastest way to create a new module is using the **logos-module-builder** template: + +```bash +# Create a new directory for your module +mkdir logos-my-module && cd logos-my-module + +# Scaffold a minimal module (no external dependencies) +nix flake init -t github:logos-co/logos-module-builder + +# Or scaffold a module that wraps an external C/C++ library +nix flake init -t github:logos-co/logos-module-builder#with-external-lib +``` + +This generates a ready-to-build project with all the boilerplate handled for you. + +### 1.2 Project Structure + +After scaffolding, your module directory looks like this: + +``` +logos-my-module/ +├── flake.nix # Nix flake (build config, ~15 lines) +├── module.yaml # Declarative module configuration (~30 lines) +├── CMakeLists.txt # CMake build file (~25 lines) +├── metadata.json # Auto-generated at build time from module.yaml +└── src/ + ├── my_module_interface.h # Qt interface definition + ├── my_module_plugin.h # Plugin header + └── my_module_plugin.cpp # Plugin implementation +``` + +The key insight: **logos-module-builder** reduces ~600 lines of configuration across 5+ files down to ~70 lines across 2-3 files. + +### 1.3 The module.yaml Configuration + +The `module.yaml` file is the central configuration for your module: + +```yaml +name: my_module +version: 1.0.0 +type: core +category: general +description: "My first Logos module" +dependencies: [] + +# Nix packages needed at build/runtime (optional) +nix_packages: + build: [] + runtime: [] + +# CMake configuration (optional) +cmake: + find_packages: [] + extra_sources: [] + extra_include_dirs: [] + extra_link_libraries: [] +``` + +**Field reference:** + +| Field | Required | Default | Description | +|-------|----------|---------|-------------| +| `name` | Yes | -- | Module name (used for filenames and identifiers) | +| `version` | No | `1.0.0` | Semantic version | +| `type` | No | `core` | Module type | +| `category` | No | `general` | Category (general, network, chat, wallet, integration) | +| `description` | No | `"A Logos module"` | Human-readable description | +| `dependencies` | No | `[]` | Other Logos module names this depends on | +| `nix_packages.build` | No | `[]` | Nix packages for build time | +| `nix_packages.runtime` | No | `[]` | Nix packages for runtime | +| `cmake.find_packages` | No | `[]` | CMake `find_package()` calls | +| `cmake.extra_sources` | No | `[]` | Additional source files to compile | +| `cmake.extra_include_dirs` | No | `[]` | Additional include directories | +| `cmake.extra_link_libraries` | No | `[]` | Additional libraries to link | + +### 1.4 Writing Module Code + +A Logos module is a **Qt plugin**. It must: + +1. Inherit from `QObject` and implement the `PluginInterface` +2. Declare an interface with `Q_INTERFACES` +3. Embed metadata with `Q_PLUGIN_METADATA` +4. Mark callable methods with `Q_INVOKABLE` + +#### The Interface Header (`src/my_module_interface.h`) + +```cpp +#pragma once + +#include +#include +#include // From logos-cpp-sdk: provides PluginInterface + +class MyModuleInterface : public PluginInterface +{ +public: + virtual ~MyModuleInterface() {} + + // Declare your module's public methods here + virtual QString doSomething(const QString& input) = 0; + virtual int compute(int a, int b) = 0; +}; + +#define MyModuleInterface_iid "com.logos.MyModuleInterface" +Q_DECLARE_INTERFACE(MyModuleInterface, MyModuleInterface_iid) +``` + +#### The Plugin Header (`src/my_module_plugin.h`) + +```cpp +#pragma once + +#include +#include "my_module_interface.h" + +class MyModulePlugin : public QObject, public MyModuleInterface +{ + Q_OBJECT + Q_INTERFACES(MyModuleInterface PluginInterface) + Q_PLUGIN_METADATA(IID MyModuleInterface_iid FILE "metadata.json") + +public: + explicit MyModulePlugin(QObject* parent = nullptr); + ~MyModulePlugin(); + + // PluginInterface + QString name() const override { return "my_module"; } + QString version() const override { return "1.0.0"; } + + // Your methods -- mark with Q_INVOKABLE for remote access + Q_INVOKABLE void initLogos(LogosAPI* logosAPIInstance); + Q_INVOKABLE QString doSomething(const QString& input) override; + Q_INVOKABLE int compute(int a, int b) override; + +signals: + // For event forwarding to other modules + void eventResponse(const QString& eventName, const QVariantList& data); +}; +``` + +#### The Plugin Implementation (`src/my_module_plugin.cpp`) + +```cpp +#include "my_module_plugin.h" +#include + +MyModulePlugin::MyModulePlugin(QObject* parent) : QObject(parent) +{ + qDebug() << "MyModulePlugin: created"; +} + +MyModulePlugin::~MyModulePlugin() +{ + qDebug() << "MyModulePlugin: destroyed"; +} + +void MyModulePlugin::initLogos(LogosAPI* logosAPIInstance) +{ + // Store the API pointer for inter-module communication + logosAPI = logosAPIInstance; + qDebug() << "MyModulePlugin: LogosAPI initialized"; +} + +QString MyModulePlugin::doSomething(const QString& input) +{ + return "Processed: " + input; +} + +int MyModulePlugin::compute(int a, int b) +{ + return a + b; +} +``` + +**Key rules:** + +- Every `Q_INVOKABLE` method is discoverable and callable by other modules at runtime +- `initLogos(LogosAPI*)` is called by the host when your module is loaded -- store the pointer for later use +- The `eventResponse` signal is used for event forwarding between modules +- `name()` must match the `name` field in your `module.yaml` / `metadata.json` + +### 1.5 Building Your Module + +```bash +# Build everything (library + generated SDK headers) +nix build + +# Build just the plugin shared library (.so / .dylib) +nix build .#lib + +# Build just the generated SDK headers (for other modules to use) +nix build .#include + +# Enter the development shell (provides cmake, ninja, Qt, etc.) +nix develop + +# Inside the dev shell, you can also build directly with CMake: +cmake -B build -GNinja +cmake --build build +``` + +**Build outputs:** + +``` +result/ +├── lib/ +│ └── my_module_plugin.so # (or .dylib on macOS) +├── include/ +│ └── ... # Generated SDK headers +└── share/ + └── metadata.json # Runtime metadata +``` + +--- + +## Part 2: Inspecting and Testing Your Module + +### 2.1 The `lm` CLI Tool + +The **`lm`** tool (from `logos-module`) lets you inspect compiled module binaries without loading them into the full runtime. It reads metadata and enumerates methods via Qt's meta-object system. + +#### Building lm + +```bash +nix build 'github:logos-co/logos-module#lm' --out-link ./lm +``` + +#### Viewing Metadata + +```bash +# Human-readable metadata +./lm/bin/lm metadata ./result/lib/my_module_plugin.so + +# JSON output +./lm/bin/lm metadata ./result/lib/my_module_plugin.so --json +``` + +Example JSON output: + +```json +{ + "name": "my_module", + "version": "1.0.0", + "description": "My first Logos module", + "author": "", + "type": "core", + "dependencies": [] +} +``` + +#### Viewing Methods + +```bash +# Human-readable method list +./lm/bin/lm methods ./result/lib/my_module_plugin.so + +# JSON output +./lm/bin/lm methods ./result/lib/my_module_plugin.so --json +``` + +Example JSON output: + +```json +[ + { + "name": "initLogos", + "signature": "initLogos(LogosAPI*)", + "returnType": "void", + "isInvokable": true, + "parameters": [ + { "name": "logosAPIInstance", "type": "LogosAPI*" } + ] + }, + { + "name": "doSomething", + "signature": "doSomething(QString)", + "returnType": "QString", + "isInvokable": true, + "parameters": [ + { "name": "input", "type": "QString" } + ] + } +] +``` + +### 2.2 Running with `logoscore` + +The **`logoscore`** CLI (from `logos-liblogos`) is a headless runtime that can load modules and invoke their methods from the command line. + +#### Building logoscore + +```bash +nix build 'github:logos-co/logos-liblogos' --out-link ./logos +``` + +#### Running a Module + +```bash +# Load a module from a directory +./logos/bin/logoscore \ + -m ./modules \ + --load-modules my_module + +# Load a module and call a method +./logos/bin/logoscore \ + -m ./modules \ + --load-modules my_module \ + -c "my_module.doSomething(hello)" + +# Load a module and call a method with a JSON config file +./logos/bin/logoscore \ + -m ./modules \ + --load-modules my_module \ + -c "my_module.configure(@config.json)" +``` + +**Flags:** + +| Flag | Description | +|------|-------------| +| `-m ` | Directory containing module libraries | +| `--load-modules ` | Comma-separated list of modules to load | +| `-c ".(args)"` | Command to execute after loading | +| `@file.json` | Pass a JSON file as a method argument | + +### 2.3 The logos-module-viewer + +The **logos-module-viewer** is a graphical tool for inspecting loaded modules. + +```bash +# Build the viewer +nix build 'github:logos-co/logos-module-viewer#app' --out-link ./logos-viewer + +# Run it with your module +./logos-viewer/bin/logos-module-viewer -m ./result/lib/my_module_plugin.so +``` + +This opens a window showing the module's metadata, methods, and allows interactive method invocation. + +--- + +## Part 3: Packaging Your Module + +### 3.1 The LGX Package Format + +Logos modules are distributed as **`.lgx` packages**. An LGX file is a gzip-compressed tar archive with a specific internal structure: + +``` +manifest.json # Package metadata (required) +manifest.cose # Optional cryptographic signature +variants/ # Platform-specific builds (required) + linux-x86_64/ + my_module_plugin.so + darwin-arm64/ + my_module_plugin.dylib +docs/ # Optional documentation +licenses/ # Optional license files +``` + +The **manifest.json** declares the package name, version, and maps each variant to its main entry point (the shared library file): + +```json +{ + "name": "my_module", + "version": "1.0.0", + "description": "My first Logos module", + "author": "Developer Name", + "type": "core", + "category": "general", + "manifestVersion": "0.1", + "main": { + "linux-x86_64": "my_module_plugin.so", + "darwin-arm64": "my_module_plugin.dylib" + }, + "dependencies": [] +} +``` + +### 3.2 Creating a Package with `lgx` + +The **`lgx`** CLI tool (from `logos-package`) creates and manages LGX packages. + +#### Building lgx + +```bash +nix build 'github:logos-co/logos-package#lgx' --out-link ./lgx +``` + +#### Creating a New Package + +```bash +# Create an empty package skeleton +./lgx/bin/lgx create my_module.lgx --name my_module +``` + +#### Adding Platform Variants + +```bash +# Add a single-file variant (the library binary) +./lgx/bin/lgx add-variant my_module.lgx \ + --variant linux-x86_64 \ + --files ./result/lib/my_module_plugin.so + +# Add a macOS variant +./lgx/bin/lgx add-variant my_module.lgx \ + --variant darwin-arm64 \ + --files ./result-macos/lib/my_module_plugin.dylib + +# Add a directory variant (if your module has multiple files) +./lgx/bin/lgx add-variant my_module.lgx \ + --variant linux-x86_64 \ + --files ./result/lib/ \ + --main my_module_plugin.so +``` + +**Variant naming convention:** `-` (lowercase). Common variants: + +| Variant | Platform | +|---------|----------| +| `linux-x86_64` | Linux Intel/AMD 64-bit | +| `linux-arm64` | Linux ARM 64-bit | +| `darwin-arm64` | macOS Apple Silicon | +| `darwin-x86_64` | macOS Intel | + +#### Removing a Variant + +```bash +./lgx/bin/lgx remove-variant my_module.lgx --variant linux-x86_64 +``` + +#### Listing Package Contents + +```bash +./lgx/bin/lgx list my_module.lgx +``` + +#### Extracting a Package + +```bash +# Extract a specific variant +./lgx/bin/lgx extract my_module.lgx --variant linux-x86_64 --output ./extracted/ + +# Extract all variants +./lgx/bin/lgx extract my_module.lgx --all --output ./extracted/ +``` + +### 3.3 Verifying Packages + +```bash +./lgx/bin/lgx verify my_module.lgx +``` + +This checks: +- Package structure is valid (manifest.json exists, variants/ directory exists) +- Manifest fields are present and valid +- Every variant listed in `main` has a corresponding directory and file +- Every variant directory has a corresponding `main` entry +- No forbidden files (symlinks, special files) are present +- All paths are valid (no `..` traversal, no absolute paths) + +--- + +## Part 4: Installing and Managing Modules + +### 4.1 The `lgpm` CLI + +The **`lgpm`** CLI (Logos Package Manager) installs, searches, and manages module packages. + +#### Building lgpm + +```bash +nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./package-manager +``` + +#### Commands + +```bash +# Search for packages +lgpm search waku + +# List all available packages +lgpm list + +# List only installed packages +lgpm list --installed + +# List packages in a category +lgpm list --category networking + +# Show package details +lgpm info my_module + +# List available categories +lgpm categories + +# Install a package (with dependency resolution) +lgpm install my_module + +# Install multiple packages +lgpm install my_module another_module + +# Install from a local .lgx file +lgpm install --file ./my_module.lgx +``` + +#### Global Options + +| Option | Description | +|--------|-------------| +| `--modules-dir ` | Target directory for installed core modules | +| `--ui-plugins-dir ` | Target directory for UI plugins | +| `--release ` | GitHub release tag to use (default: `latest`) | +| `--json` | Output in JSON format | +| `-h, --help` | Show help | + +### 4.2 Installing from Local Files + +```bash +# Install a locally built .lgx package +./package-manager/bin/lgpm --modules-dir ./modules install --file ./my_module.lgx +``` + +### 4.3 Installing from a Registry + +```bash +# Install a published package (lgpm fetches from GitHub Releases) +./package-manager/bin/lgpm --modules-dir ./modules install my_module + +# Install from a specific release +./package-manager/bin/lgpm --modules-dir ./modules --release v2.0.0 install my_module +``` + +The package manager automatically: +1. Resolves transitive dependencies +2. Downloads the correct platform variant for your OS/architecture +3. Extracts the LGX package +4. Copies the library to the target directory + +--- + +## Part 5: Running in the Logos App + +### 5.1 Building logos-app + +```bash +# Build the full application +nix build 'github:logos-co/logos-app#app' --out-link ./logos-app + +# Run it +./logos-app/bin/logos-app + +# Or build platform-specific distributions: +nix build 'github:logos-co/logos-app#bin-appimage' # Linux AppImage +nix build 'github:logos-co/logos-app#bin-macos-app' # macOS .app bundle +nix build 'github:logos-co/logos-app#bin-macos-dmg' # macOS DMG +``` + +### 5.2 Module Types in logos-app + +The application supports three types of modules: + +#### Core Modules (Backend) + +These are non-UI modules that provide backend functionality. They run in isolated `logos_host` processes and communicate via Qt Remote Objects. + +- Loaded via `logos_core_load_plugin()` +- Placed in the **modules directory** (`--modules-dir`) +- Have `"type": "core"` in metadata + +#### C++ UI Modules (Native Widgets) + +These provide native Qt widget UIs. They implement the `IComponent` interface: + +```cpp +class IComponent { +public: + virtual ~IComponent() = default; + virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0; + virtual void destroyWidget(QWidget* widget) = 0; +}; +``` + +- Loaded via `QPluginLoader` +- Placed in the **plugins directory** (`--ui-plugins-dir`) +- Their widget appears as a tab in the MDI workspace + +#### QML UI Modules (Sandboxed) + +These provide QML-based UIs in a sandboxed environment: + +- Have `"type": "ui_qml"` in their manifest +- Entry point is `Main.qml` +- Network access is denied +- Filesystem access is restricted to the module's own directory +- Can call core modules via the `logos` bridge: `logos.callModule("module", "method", [args])` + +### 5.3 Development Mode + +For rapid iteration on QML UI modules, use the development mode launcher: + +```bash +# Build once +nix build 'github:logos-co/logos-app' + +# Run with live QML reloading (edits to .qml files take effect immediately) +./run-dev.sh +``` + +This sets `QML_UI` to point to the source directory and disables QML caching, so you can edit QML files and see changes without rebuilding. + +--- + +## Part 6: Inter-Module Communication + +### 6.1 The LogosAPI + +Every module receives a `LogosAPI*` pointer when `initLogos()` is called. This is your gateway to communicating with other modules. + +```cpp +void MyModulePlugin::initLogos(LogosAPI* logosAPIInstance) +{ + logosAPI = logosAPIInstance; + + // Get a client for calling another module + LogosAPIClient* client = logosAPI->getClient("other_module"); + + // Call a method on that module + QVariant result = client->invokeRemoteMethod( + "other_module", // target module name + "someMethod", // method name + arg1, arg2 // arguments (up to 5 positional args) + ); +} +``` + +### 6.2 The C++ SDK Code Generator + +The `logos-cpp-generator` tool (from `logos-cpp-sdk`) inspects a compiled module and generates typed C++ wrapper classes, so you get compile-time type safety instead of raw `invokeRemoteMethod` calls. + +#### Generating Wrappers + +```bash +# Generate wrappers for a single module +logos-cpp-generator /path/to/my_module_plugin.so --output-dir ./generated + +# Generate wrappers for all dependencies listed in metadata.json +logos-cpp-generator --metadata metadata.json --module-dir /path/to/modules --output-dir ./generated + +# Generate only module files (no umbrella headers) +logos-cpp-generator /path/to/plugin.so --module-only --output-dir ./generated + +# Generate only umbrella SDK files (assumes module files exist) +logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generated +``` + +#### Using Generated Wrappers + +After generation, you get typed wrapper classes: + +```cpp +#include "logos_sdk.h" // Umbrella header + +// In your module's initLogos(): +void MyModulePlugin::initLogos(LogosAPI* api) { + logosAPI = api; + + // Create the typed SDK wrapper + LogosModules* logos = new LogosModules(api); + + // Call other modules with type safety + QString result = logos->other_module.doSomething("hello"); + bool ok = logos->core_manager.loadPlugin("another_module"); +} +``` + +The generated `LogosModules` struct provides a member for each module, with methods matching the module's `Q_INVOKABLE` methods. + +### 6.3 LogosResult + +Many module methods return `LogosResult` for structured success/error handling: + +```cpp +LogosResult result = logos->my_module.someMethod(); + +if (result.success) { + // Access the value + QString value = result.getString(); + int number = result.getInt(); + QVariantMap map = result.getMap(); + QVariantList list = result.getList(); + + // Access nested values + QString name = result.getString("name"); + int count = result.getInt("count", 0); // with default +} else { + // Access the error + QString error = result.getError(); +} +``` + +To return a `LogosResult` from your module: + +```cpp +Q_INVOKABLE LogosResult MyModulePlugin::fetchData(const QString& id) { + if (id.isEmpty()) { + return {false, QVariant(), "ID cannot be empty"}; + } + + QVariantMap data; + data["id"] = id; + data["name"] = "Example"; + data["count"] = 42; + return {true, data}; +} +``` + +### 6.4 Communication Modes + +The SDK supports two communication modes: + +| Mode | Use Case | Mechanism | +|------|----------|-----------| +| **Remote** (default) | Desktop apps | Qt Remote Objects (IPC between processes) | +| **Local** | Mobile apps, single-process | In-process `PluginRegistry` | + +Set the mode before creating any `LogosAPI` instances: + +```cpp +// For mobile / embedded (all modules in one process) +LogosModeConfig::setMode(LogosMode::Local); + +// For desktop (each module in its own process) -- this is the default +LogosModeConfig::setMode(LogosMode::Remote); +``` + +--- + +## Part 7: Advanced Topics + +### 7.1 Wrapping External Libraries + +To create a module that wraps an external C/C++ library, use the external library template: + +```bash +nix flake init -t github:logos-co/logos-module-builder#with-external-lib +``` + +Then configure the external library in `module.yaml`: + +```yaml +name: my_wrapper_module +version: 1.0.0 +description: "Wraps libfoo for Logos" + +external_libraries: + - name: libfoo + flake_input: "github:example/libfoo" + output_pattern: "lib/libfoo.*" + +# Or for a vendored library: +external_libraries: + - name: libfoo + vendor_path: "vendor/libfoo" + build_command: "make" + output_pattern: "build/lib/libfoo.*" + +# Or for a Go library: +external_libraries: + - name: libfoo + vendor_path: "vendor/libfoo" + go_build: true + output_pattern: "libfoo.*" +``` + +The builder handles downloading, building, and linking the external library into your module. + +### 7.2 UI Modules (C++ Widgets) + +To create a module with a native Qt widget UI: + +1. Implement the `IComponent` interface +2. Set `"type": "ui"` in your metadata +3. Return a `QWidget*` from `createWidget()` + +```cpp +#include + +class MyUIPlugin : public QObject, public IComponent +{ + Q_OBJECT + Q_INTERFACES(IComponent) + Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json") + +public: + Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override { + auto* widget = new QWidget(); + // Build your UI here + return widget; + } + + void destroyWidget(QWidget* widget) override { + delete widget; + } +}; +``` + +### 7.3 UI Modules (QML) + +For a QML-based UI module, create a directory with: + +``` +my_qml_module/ +├── manifest.json +├── metadata.json +└── Main.qml +``` + +**manifest.json:** +```json +{ + "type": "ui_qml", + "main": "Main.qml", + "name": "my_qml_module", + "version": "1.0.0" +} +``` + +**Main.qml:** +```qml +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +Item { + width: 400 + height: 300 + + Button { + text: "Call Core Module" + onClicked: { + // logos bridge is injected by the host + var result = logos.callModule("my_module", "doSomething", ["hello"]) + console.log("Result:", result) + } + } +} +``` + +QML modules are sandboxed: no network access, no filesystem access outside the module directory. + +### 7.4 Module Dependencies + +Declare dependencies in your `module.yaml`: + +```yaml +name: my_module +dependencies: + - package_manager + - waku_module +``` + +Or in `metadata.json`: + +```json +{ + "name": "my_module", + "dependencies": ["package_manager", "waku_module"] +} +``` + +When your module is installed via `lgpm`, its dependencies are automatically resolved and installed first. When loaded via `logos-app`, core module dependencies are loaded before your module. + +--- + +## Reference: Repository Map + +| Repository | What It Provides | Key Outputs | +|------------|-----------------|-------------| +| [logos-module-builder](https://github.com/logos-co/logos-module-builder) | Build system / scaffolding | `mkLogosModule` Nix function, `LogosModule.cmake`, templates | +| [logos-module](https://github.com/logos-co/logos-module) | Plugin introspection | `liblogos_module.a` (static lib), `lm` (CLI) | +| [logos-cpp-sdk](https://github.com/logos-co/logos-cpp-sdk) | SDK + code generator | `LogosAPI`, `LogosResult`, `logos-cpp-generator`, `PluginInterface` | +| [logos-liblogos](https://github.com/logos-co/logos-liblogos) | Core runtime | `logoscore` (CLI), `logos_host`, `liblogos_core` | +| [logos-package](https://github.com/logos-co/logos-package) | Package format | `lgx` (CLI), `liblgx` (library) | +| [logos-package-manager-module](https://github.com/logos-co/logos-package-manager-module) | Package management | `lgpm` (CLI), `package_manager_plugin` | +| [logos-app](https://github.com/logos-co/logos-app) | Desktop app shell | `LogosApp` (GUI), MDI workspace, plugin loader | + +## Reference: CLI Tools Summary + +### `lm` -- Module Inspector + +```bash +lm metadata [--json] # View module metadata +lm methods [--json] # List Q_INVOKABLE methods +``` + +### `logoscore` -- Headless Runtime + +```bash +logoscore -m --load-modules [-c ".(args)"] +``` + +### `lgx` -- Package Tool + +```bash +lgx create --name # Create empty package +lgx add-variant --variant --files [--main ] +lgx remove-variant --variant +lgx list # List contents +lgx verify # Validate structure +lgx extract --variant --output # Extract +``` + +### `lgpm` -- Package Manager + +```bash +lgpm search # Search packages +lgpm list [--category ] [--installed] # List packages +lgpm install [pkgs...] # Install with dependency resolution +lgpm install --file # Install local file +lgpm info # Package details +lgpm categories # List categories +``` + +### `logos-cpp-generator` -- SDK Code Generator + +```bash +logos-cpp-generator [--output-dir ] [--module-only] +logos-cpp-generator --metadata --module-dir [--output-dir ] +logos-cpp-generator --metadata --general-only [--output-dir ] +``` + +--- + +## Troubleshooting + +### "experimental features" error with Nix + +If you see errors about experimental features, either pass the flag: + +```bash +nix --extra-experimental-features "nix-command flakes" build +``` + +Or add to `~/.config/nix/nix.conf`: + +``` +experimental-features = nix-command flakes +``` + +### Module loads but LogosAPI is not available + +This happens when running a module outside the full Logos runtime (e.g., in the module viewer). The `LogosAPI` is only available when the module is loaded by `logoscore` or `logos-app`. + +### Build fails finding Qt + +Ensure you're building inside the Nix environment: + +```bash +nix develop # Enter dev shell with all dependencies +cmake -B build -GNinja && cmake --build build +``` + +### Module not discovered by logos-app + +Check that: +1. The module binary is in the correct directory (modules dir for core, plugins dir for UI) +2. The `metadata.json` file is present alongside the binary +3. The `name` field in metadata matches the binary name (e.g., `my_module_plugin.so` for module named `my_module`) + +### lgpm install fails + +- Check your internet connection (lgpm fetches from GitHub Releases) +- Try specifying a release: `lgpm --release v1.0.0 install my_module` +- For local files: `lgpm install --file ./my_module.lgx` +- Check the target directory is writable: `lgpm --modules-dir ./modules install my_module` + +### Cross-platform builds + +Build on each target platform separately, then add each binary as a variant to the same `.lgx` package: + +```bash +# On Linux x86_64: +nix build .#lib +lgx add-variant my_module.lgx --variant linux-x86_64 --files ./result/lib/my_module_plugin.so + +# On macOS arm64: +nix build .#lib +lgx add-variant my_module.lgx --variant darwin-arm64 --files ./result/lib/my_module_plugin.dylib +``` diff --git a/tutorial-wrapping-c-library.md b/tutorial-wrapping-c-library.md new file mode 100644 index 0000000..7bdf683 --- /dev/null +++ b/tutorial-wrapping-c-library.md @@ -0,0 +1,939 @@ +# Tutorial: Wrapping a C Library as a Logos Module + +This tutorial walks you through wrapping a C shared library (`.so` on Linux, `.dylib` on macOS) as a Logos module. By the end, you will have a module that compiles, loads, and responds to method calls via `logoscore`. + +**What you'll build:** A `calc_module` that wraps a tiny C calculator library (`libcalc`), exposing arithmetic functions to the Logos platform. + +**What you'll learn:** +- How a Logos module wraps a C library +- The role of each file in the module project +- How to build, inspect, and test your module +- How `logoscore` discovers, loads, and calls your module + +## Prerequisites + +- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes globally: + + ```bash + # Add to ~/.config/nix/nix.conf: + experimental-features = nix-command flakes + ``` + +- **A C compiler** (gcc or clang) for building the C library. Only needed if you're building the `.so`/`.dylib` yourself rather than using a pre-built library. + +- Basic familiarity with C and C++. + +--- + +## Step 1: Create the C Library + +We'll create a minimal C library called `libcalc` with four arithmetic functions. In a real project, this would be whatever C library you want to wrap (e.g., a networking library, a crypto library, a codec). + +### 1.1 Create the project directory + +```bash +mkdir logos-calc-module && cd logos-calc-module +mkdir lib src +``` + +### 1.2 Write the C header + +Create `lib/libcalc.h`: + +```c +#ifndef LIBCALC_H +#define LIBCALC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** Add two integers. */ +int calc_add(int a, int b); + +/** Multiply two integers. */ +int calc_multiply(int a, int b); + +/** Compute factorial of n (n must be >= 0). Returns -1 on error. */ +int calc_factorial(int n); + +/** Compute the nth Fibonacci number (n must be >= 0). Returns -1 on error. */ +int calc_fibonacci(int n); + +/** Return the library version string. Caller must NOT free. */ +const char* calc_version(void); + +#ifdef __cplusplus +} +#endif + +#endif /* LIBCALC_H */ +``` + +The `extern "C"` block is essential — it prevents C++ name mangling so the Logos module can find the symbols. + +### 1.3 Write the C implementation + +Create `lib/libcalc.c`: + +```c +#include "libcalc.h" + +int calc_add(int a, int b) +{ + return a + b; +} + +int calc_multiply(int a, int b) +{ + return a * b; +} + +int calc_factorial(int n) +{ + if (n < 0) return -1; + if (n <= 1) return 1; + int result = 1; + for (int i = 2; i <= n; i++) { + result *= i; + } + return result; +} + +int calc_fibonacci(int n) +{ + if (n < 0) return -1; + if (n == 0) return 0; + if (n == 1) return 1; + int a = 0, b = 1; + for (int i = 2; i <= n; i++) { + int tmp = a + b; + a = b; + b = tmp; + } + return b; +} + +const char* calc_version(void) +{ + return "1.0.0"; +} +``` + +### 1.4 Build the shared library + +```bash +cd lib + +# Linux +gcc -shared -fPIC -o libcalc.so libcalc.c + +# macOS +# gcc -shared -fPIC -o libcalc.dylib libcalc.c + +cd .. +``` + +Verify the symbols are exported: + +```bash +# Linux +nm -D lib/libcalc.so | grep calc + +# macOS +# nm -gU lib/libcalc.dylib | grep calc +``` + +You should see: + +``` +T calc_add +T calc_factorial +T calc_fibonacci +T calc_multiply +T calc_version +``` + +> **Wrapping a third-party library?** If you're wrapping an existing library (e.g., from a system package or a GitHub repo), you don't need to write the C code — just place the pre-built `.so`/`.dylib` and its header file in `lib/`. + +--- + +## Step 2: Create the Logos Module + +A Logos module is a **Qt plugin** that wraps your C library functions as `Q_INVOKABLE` methods. You need five files: + +``` +logos-calc-module/ +├── flake.nix # Nix build configuration (~10 lines) +├── module.yaml # Module metadata and build settings (~20 lines) +├── CMakeLists.txt # CMake build file (~20 lines) +├── metadata.json # Runtime metadata (~10 lines) +├── lib/ +│ ├── libcalc.h # C library header +│ ├── libcalc.c # C library source +│ └── libcalc.so # Pre-built shared library +└── src/ + ├── calc_module_interface.h # Interface declaration + ├── calc_module_plugin.h # Plugin header + └── calc_module_plugin.cpp # Plugin implementation (wrapping logic) +``` + +### 2.1 `module.yaml` — Module Configuration + +This is the central configuration file. It tells `logos-module-builder` what to build and how. + +```yaml +name: calc_module +version: 1.0.0 +type: core +category: general +description: "Calculator module wrapping libcalc C library" + +dependencies: [] + +nix_packages: + build: [] + runtime: [] + +# This tells the builder that "libcalc" is a pre-built library in lib/ +external_libraries: + - name: calc + vendor_path: "lib" + +cmake: + find_packages: [] + extra_sources: [] + extra_include_dirs: + - lib + extra_link_libraries: [] +``` + +**Key fields explained:** + +| Field | What it does | +|-------|-------------| +| `name` | Module name — must be a valid C identifier (used in filenames, method calls) | +| `external_libraries[].name` | Library name — the builder looks for `lib.so` / `lib.dylib` in the directory specified by `vendor_path` | +| `external_libraries[].vendor_path` | Where to find the pre-built library. `"lib"` means the `lib/` directory in your project root | +| `cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "lib/libcalc.h"` | + +### 2.2 `metadata.json` — Runtime Metadata + +This file is embedded into the plugin binary by Qt's `Q_PLUGIN_METADATA` macro: + +```json +{ + "name": "calc_module", + "version": "1.0.0", + "description": "Calculator module wrapping libcalc C library", + "author": "", + "type": "core", + "category": "general", + "main": "calc_module_plugin", + "dependencies": [] +} +``` + +### 2.3 `CMakeLists.txt` — Build File + +```cmake +cmake_minimum_required(VERSION 3.14) +project(CalcModulePlugin LANGUAGES CXX) + +# Include the Logos Module CMake helper (provided by logos-module-builder) +if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT}) + include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake") + include(cmake/LogosModule.cmake) +else() + message(FATAL_ERROR "LogosModule.cmake not found") +endif() + +# Define the module with its external library dependency +logos_module( + NAME calc_module + SOURCES + src/calc_module_interface.h + src/calc_module_plugin.h + src/calc_module_plugin.cpp + EXTERNAL_LIBS + calc +) +``` + +**How `EXTERNAL_LIBS calc` works:** The `logos_module()` CMake function searches `lib/` for `libcalc.so` (Linux) or `libcalc.dylib` (macOS), links it to your plugin, and sets up RPATH so the library is found at runtime. + +### 2.4 `flake.nix` — Nix Build Config + +```nix +{ + description = "Calculator module - wraps libcalc C library for Logos"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + nixpkgs.follows = "logos-module-builder/nixpkgs"; + }; + + outputs = { self, logos-module-builder, nixpkgs }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./module.yaml; + }; +} +``` + +That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, setting up include paths, etc.). + +### 2.5 `src/calc_module_interface.h` — Interface Declaration + +This declares the methods your module exposes. It inherits from `PluginInterface` (provided by the Logos C++ SDK). + +```cpp +#ifndef CALC_MODULE_INTERFACE_H +#define CALC_MODULE_INTERFACE_H + +#include +#include +#include "interface.h" + +class CalcModuleInterface : public PluginInterface +{ +public: + virtual ~CalcModuleInterface() = default; + + Q_INVOKABLE virtual int add(int a, int b) = 0; + Q_INVOKABLE virtual int multiply(int a, int b) = 0; + Q_INVOKABLE virtual int factorial(int n) = 0; + Q_INVOKABLE virtual int fibonacci(int n) = 0; + Q_INVOKABLE virtual QString libVersion() = 0; +}; + +#define CalcModuleInterface_iid "org.logos.CalcModuleInterface" +Q_DECLARE_INTERFACE(CalcModuleInterface, CalcModuleInterface_iid) + +#endif // CALC_MODULE_INTERFACE_H +``` + +**Rules for the interface:** +- Every method you want callable by other modules must be `Q_INVOKABLE` and `virtual` +- Supported parameter/return types: `int`, `bool`, `QString`, `QByteArray`, `QVariant`, `QJsonArray`, `QStringList`, `LogosResult` +- The interface ID string (e.g., `"org.logos.CalcModuleInterface"`) must be unique across all modules + +### 2.6 `src/calc_module_plugin.h` — Plugin Header + +This is the actual plugin class. It inherits from both `QObject` (for Qt's meta-object system) and your interface. + +```cpp +#ifndef CALC_MODULE_PLUGIN_H +#define CALC_MODULE_PLUGIN_H + +#include +#include +#include "calc_module_interface.h" + +// Include the C library header +#include "lib/libcalc.h" + +class LogosAPI; + +class CalcModulePlugin : public QObject, public CalcModuleInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID CalcModuleInterface_iid FILE "metadata.json") + Q_INTERFACES(CalcModuleInterface PluginInterface) + +public: + explicit CalcModulePlugin(QObject* parent = nullptr); + ~CalcModulePlugin() override; + + // PluginInterface — required by every module + QString name() const override { return "calc_module"; } + QString version() const override { return "1.0.0"; } + + // Called by the Logos host when the module is loaded. + // NOT marked override — it is invoked reflectively via QMetaObject. + Q_INVOKABLE void initLogos(LogosAPI* api); + + // CalcModuleInterface — each wraps a libcalc C function + Q_INVOKABLE int add(int a, int b) override; + Q_INVOKABLE int multiply(int a, int b) override; + Q_INVOKABLE int factorial(int n) override; + Q_INVOKABLE int fibonacci(int n) override; + Q_INVOKABLE QString libVersion() override; + +signals: + void eventResponse(const QString& eventName, const QVariantList& args); + +private: + LogosAPI* m_logosAPI = nullptr; +}; + +#endif // CALC_MODULE_PLUGIN_H +``` + +**Critical details:** +- `Q_PLUGIN_METADATA(IID ... FILE "metadata.json")` — embeds the metadata into the binary +- `Q_INTERFACES(CalcModuleInterface PluginInterface)` — registers both interfaces with Qt's plugin system +- `initLogos` must be `Q_INVOKABLE` but **not** `override` — the base class `PluginInterface` does not declare it as virtual; the Logos host calls it reflectively via `QMetaObject::invokeMethod` +- `eventResponse` signal is required for event forwarding between modules +- `name()` must return the same string as the `name` field in `module.yaml` and `metadata.json` + +### 2.7 `src/calc_module_plugin.cpp` — Plugin Implementation + +This is where the wrapping happens. Each method calls the corresponding C function. + +```cpp +#include "calc_module_plugin.h" +#include "logos_api.h" +#include + +CalcModulePlugin::CalcModulePlugin(QObject* parent) + : QObject(parent) +{ + qDebug() << "CalcModulePlugin: created"; +} + +CalcModulePlugin::~CalcModulePlugin() +{ + qDebug() << "CalcModulePlugin: destroyed"; +} + +void CalcModulePlugin::initLogos(LogosAPI* api) +{ + m_logosAPI = api; + qDebug() << "CalcModulePlugin: LogosAPI initialized"; +} + +int CalcModulePlugin::add(int a, int b) +{ + // Call the C library function + int result = calc_add(a, b); + qDebug() << "CalcModulePlugin::add" << a << "+" << b << "=" << result; + return result; +} + +int CalcModulePlugin::multiply(int a, int b) +{ + int result = calc_multiply(a, b); + qDebug() << "CalcModulePlugin::multiply" << a << "*" << b << "=" << result; + return result; +} + +int CalcModulePlugin::factorial(int n) +{ + int result = calc_factorial(n); + qDebug() << "CalcModulePlugin::factorial" << n << "! =" << result; + return result; +} + +int CalcModulePlugin::fibonacci(int n) +{ + int result = calc_fibonacci(n); + qDebug() << "CalcModulePlugin::fibonacci fib(" << n << ") =" << result; + return result; +} + +QString CalcModulePlugin::libVersion() +{ + const char* ver = calc_version(); + QString result = QString::fromUtf8(ver); + qDebug() << "CalcModulePlugin::libVersion" << result; + return result; +} +``` + +**The wrapping pattern** is always the same: +1. Call the C function with the arguments +2. Convert the C result to a Qt type if needed (e.g., `const char*` → `QString`) +3. Return the Qt type + +--- + +## Step 3: Build the Module + +### 3.1 Initialize the Git repo + +Nix flakes require a git repository: + +```bash +cd logos-calc-module +git init +git add -A +git commit -m "Initial commit" +``` + +### 3.2 Build with Nix + +```bash +# Build just the plugin library (.so / .dylib) +nix build .#lib + +# Build everything (library + generated SDK headers) +nix build +``` + +The first build takes a while (5-15 minutes) as Nix downloads Qt, the Logos SDK, and other dependencies. Subsequent builds are fast due to caching. + +### 3.3 Inspect the output + +```bash +ls -la result/lib/ +``` + +You should see two files: + +``` +calc_module_plugin.so # Your Logos module plugin +libcalc.so # The C library (copied alongside) +``` + +Both files are placed together so the plugin can find the C library at runtime via RPATH. + +--- + +## Step 4: Inspect the Module + +### 4.1 Build the `lm` tool + +The `lm` CLI tool (from `logos-module`) inspects compiled module binaries: + +```bash +nix build 'github:logos-co/logos-module#lm' --out-link ./lm +``` + +### 4.2 View metadata + +```bash +./lm/bin/lm metadata result/lib/calc_module_plugin.so +``` + +Output: + +``` +Plugin Metadata: +================ +Name: calc_module +Version: 1.0.0 +Description: Calculator module wrapping libcalc C library +Author: +Type: core +Dependencies: (none) +``` + +### 4.3 List methods + +```bash +./lm/bin/lm methods result/lib/calc_module_plugin.so +``` + +Output: + +``` +Plugin Methods: +=============== + +void eventResponse(QString eventName, QVariantList args) + Signature: eventResponse(QString,QVariantList) + Invokable: no + +void initLogos(LogosAPI* api) + Signature: initLogos(LogosAPI*) + Invokable: yes + +int add(int a, int b) + Signature: add(int,int) + Invokable: yes + +int multiply(int a, int b) + Signature: multiply(int,int) + Invokable: yes + +int factorial(int n) + Signature: factorial(int) + Invokable: yes + +int fibonacci(int n) + Signature: fibonacci(int) + Invokable: yes + +QString libVersion() + Signature: libVersion() + Invokable: yes +``` + +All five wrapping methods are visible and invokable. The `initLogos` method is automatically called by the Logos host when loading the module. + +### 4.4 JSON output + +For scripting and CI, use `--json`: + +```bash +./lm/bin/lm methods result/lib/calc_module_plugin.so --json +``` + +```json +[ + { + "isInvokable": true, + "name": "add", + "parameters": [ + { "name": "a", "type": "int" }, + { "name": "b", "type": "int" } + ], + "returnType": "int", + "signature": "add(int,int)" + }, + ... +] +``` + +--- + +## Step 5: Test with `logoscore` + +### 5.1 Build logoscore + +```bash +nix build 'github:logos-co/logos-liblogos' --out-link ./logos +``` + +### 5.2 Set up the modules directory + +`logoscore` expects modules in subdirectories, each with a `manifest.json`: + +```bash +mkdir -p modules/calc_module +cp result/lib/calc_module_plugin.so modules/calc_module/ +cp result/lib/libcalc.so modules/calc_module/ +``` + +Create `modules/calc_module/manifest.json`: + +```json +{ + "name": "calc_module", + "version": "1.0.0", + "description": "Calculator module wrapping libcalc C library", + "type": "core", + "main": { + "linux-aarch64": "calc_module_plugin.so", + "linux-x86_64": "calc_module_plugin.so", + "darwin-arm64": "calc_module_plugin.dylib", + "darwin-x86_64": "calc_module_plugin.dylib" + }, + "dependencies": [] +} +``` + +The `main` object maps platform variant names to the plugin filename. `logoscore` uses this to find the right binary for your OS and architecture. + +### 5.3 Call methods + +```bash +# Call add(3, 5) +./logos/bin/logoscore \ + -m ./modules \ + --load-modules calc_module \ + -c "calc_module.add(3, 5)" + +# Call factorial(5) +./logos/bin/logoscore \ + -m ./modules \ + --load-modules calc_module \ + -c "calc_module.factorial(5)" + +# Call fibonacci(10) +./logos/bin/logoscore \ + -m ./modules \ + --load-modules calc_module \ + -c "calc_module.fibonacci(10)" + +# Call libVersion() +./logos/bin/logoscore \ + -m ./modules \ + --load-modules calc_module \ + -c "calc_module.libVersion()" +``` + +**What happens under the hood:** + +1. `logoscore` scans `./modules/` for subdirectories containing `manifest.json` +2. It finds `calc_module` and extracts metadata from the plugin binary +3. It spawns a `logos_host` process that loads `calc_module_plugin.so` +4. `logos_host` calls `initLogos()` on the plugin, providing a `LogosAPI*` for inter-module communication +5. The `-c` command is parsed: module name `calc_module`, method `add`, args `[3, 5]` +6. `logoscore` sends the call to `logos_host` via Qt Remote Objects (IPC) +7. `logos_host` invokes `CalcModulePlugin::add(3, 5)` which calls `calc_add(3, 5)` from libcalc +8. The result is returned via IPC to `logoscore` + +You'll see debug output like: + +``` +Debug: Found plugin: "./modules/calc_module/calc_module_plugin.so" +Debug: Plugin Metadata: +Debug: - Name: "calc_module" +Debug: - Version: "1.0.0" +Debug: - Description: "Calculator module wrapping libcalc C library" +Debug: Loading plugin: "calc_module" in separate process +Debug: Executing call: "calc_module" . "add" with 2 params +Method call successful. Result: ... +``` + +--- + +## Step 6: Package for Distribution (Optional) + +### 6.1 Build the `lgx` tool + +```bash +nix build 'github:logos-co/logos-package#lgx' --out-link ./lgx +``` + +### 6.2 Create a package + +```bash +# Create an empty LGX package +./lgx/bin/lgx create calc_module.lgx --name calc_module + +# Add the Linux variant +./lgx/bin/lgx add-variant calc_module.lgx \ + --variant linux-aarch64 \ + --files result/lib/ + +# Verify the package +./lgx/bin/lgx verify calc_module.lgx +``` + +On another machine, install with the Logos Package Manager: + +```bash +nix build 'github:logos-co/logos-package-manager-module#cli' --out-link ./pm +./pm/bin/lgpm --modules-dir ./modules install --file calc_module.lgx +``` + +--- + +## Common Wrapping Patterns + +### Wrapping C functions with opaque pointers + +Many C libraries use opaque pointers (handles) for state management: + +```c +// C API +typedef struct db_ctx db_ctx_t; +db_ctx_t* db_open(const char* path); +int db_get(db_ctx_t* ctx, const char* key, char* buf, int buf_len); +void db_close(db_ctx_t* ctx); +``` + +Store the handle in your plugin class: + +```cpp +class DbModulePlugin : public QObject, public DbModuleInterface +{ + // ... +private: + db_ctx_t* m_ctx = nullptr; + +public: + Q_INVOKABLE bool open(const QString& path) { + m_ctx = db_open(path.toUtf8().constData()); + return m_ctx != nullptr; + } + + Q_INVOKABLE QString get(const QString& key) { + if (!m_ctx) return QString(); + char buf[4096]; + int len = db_get(m_ctx, key.toUtf8().constData(), buf, sizeof(buf)); + if (len < 0) return QString(); + return QString::fromUtf8(buf, len); + } + + ~DbModulePlugin() { + if (m_ctx) db_close(m_ctx); + } +}; +``` + +### Wrapping C callbacks + +C libraries often use callbacks for async operations: + +```c +typedef void (*event_cb)(int code, const char* msg, void* user_data); +void lib_set_callback(void* ctx, event_cb cb, void* user_data); +``` + +Use a static method as the callback, passing `this` as `user_data`: + +```cpp +class MyPlugin : public QObject, public MyInterface +{ + // ... + static void c_callback(int code, const char* msg, void* user_data) { + auto* self = static_cast(user_data); + // Forward to Qt signal (thread-safe) + emit self->eventResponse("lib_event", + QVariantList() << code << QString::fromUtf8(msg)); + } + + Q_INVOKABLE void startListening() { + lib_set_callback(m_ctx, c_callback, this); + } +}; +``` + +### Wrapping C libraries that allocate strings + +If the C library returns allocated strings that must be freed: + +```cpp +Q_INVOKABLE QString getData() { + char* c_str = lib_get_data(m_ctx); // Library allocates + QString result = QString::fromUtf8(c_str); + lib_free_string(c_str); // Library deallocates + return result; +} +``` + +### String conversion reference + +| C type | Qt type | C → Qt | Qt → C | +|--------|---------|--------|--------| +| `const char*` | `QString` | `QString::fromUtf8(c_str)` | `str.toUtf8().constData()` | +| `const char*` (binary) | `QByteArray` | `QByteArray(data, len)` | `ba.data()`, `ba.size()` | +| `int` | `int` | direct | direct | +| `bool` / `int` | `bool` | `result != 0` | direct | +| `void*` | (store in member) | — | — | + +--- + +## Advanced: Wrapping a Library from a Flake Input + +Instead of pre-building the library and placing it in `lib/`, you can have Nix fetch and build it from source. This is useful for libraries hosted on GitHub. + +### flake.nix with external library input + +```nix +{ + description = "Module wrapping libfoo from GitHub"; + + inputs = { + logos-module-builder.url = "github:logos-co/logos-module-builder"; + nixpkgs.follows = "logos-module-builder/nixpkgs"; + + # Fetch the library source (non-flake) + libfoo-src = { + url = "github:example/libfoo"; + flake = false; + }; + }; + + outputs = { self, logos-module-builder, nixpkgs, libfoo-src }: + logos-module-builder.lib.mkLogosModule { + src = ./.; + configFile = ./module.yaml; + + # Pass the fetched source to the builder + externalLibInputs = { + foo = libfoo-src; + }; + }; +} +``` + +### module.yaml for flake input + +```yaml +name: foo_module +version: 1.0.0 +type: core +description: "Module wrapping libfoo" + +external_libraries: + - name: foo + flake_input: "github:example/libfoo" + build_command: "make shared" + output_pattern: "build/libfoo.*" + +cmake: + extra_include_dirs: + - lib +``` + +**Key difference:** The `externalLibInputs` key in flake.nix (`foo`) must match the `name` field in `external_libraries` (`foo`). The builder will: +1. Clone the source from the flake input +2. Run `build_command` (`make shared`) +3. Search for output files matching `output_pattern` +4. Copy the resulting `.so`/`.dylib` and headers to `lib/` +5. Proceed with the normal module build + +### For Go libraries + +If the external library is written in Go with C bindings (`cgo`): + +```yaml +external_libraries: + - name: mygolib + flake_input: "github:example/mygolib" + go_build: true + output_pattern: "libmygolib.*" +``` + +Setting `go_build: true` enables the Go toolchain and sets `CGO_ENABLED=1`. + +--- + +## Real-World Example: logos-libp2p-module + +The [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module) is a production module that wraps the `nim-libp2p` library (compiled to a C shared library). Key files: + +- **`flake.nix`** — Uses `externalLibInputs` to fetch the nim-libp2p C bindings from a GitHub flake +- **`module.yaml`** — Declares `nim_libp2p` as an external library with `go_build: false` +- **`src/plugin.cpp`** — Wraps ~40 C functions (`libp2p_new`, `libp2p_start`, `libp2p_connect`, `libp2p_dial`, `libp2p_gossipsub_subscribe`, etc.) as `Q_INVOKABLE` methods +- **`tests/`** — Qt test suite that exercises every wrapped function + +It follows the exact same pattern as this tutorial, just at a larger scale. + +--- + +## Troubleshooting + +### `initLogos` marked 'override', but does not override + +``` +error: 'void MyPlugin::initLogos(LogosAPI*)' marked 'override', but does not override +``` + +**Fix:** Remove the `override` keyword from `initLogos`. The base `PluginInterface` class does not declare it as virtual. The Logos host calls it reflectively via `QMetaObject::invokeMethod`. Declare it as: + +```cpp +Q_INVOKABLE void initLogos(LogosAPI* api); // No override! +``` + +### Library not found at runtime + +``` +Cannot load library calc_module_plugin.so: libcalc.so: cannot open shared object file +``` + +**Fix:** Ensure `libcalc.so` is in the same directory as `calc_module_plugin.so`. The build system sets RPATH to `$ORIGIN` (Linux) / `@loader_path` (macOS) so the plugin looks for libraries in its own directory. + +### Plugin not discovered by logoscore + +**Check:** +1. The module is in a **subdirectory** of the modules dir (e.g., `modules/calc_module/`) +2. The subdirectory contains a `manifest.json` with a valid `main` object +3. The platform key in `main` matches your OS/arch (e.g., `linux-aarch64`, `darwin-arm64`) + +### First build is slow + +The first `nix build` downloads Qt 6, the Logos C++ SDK, the code generator, and other dependencies. This is a one-time cost — subsequent builds use the Nix cache and are fast (usually under 30 seconds). + +### Symbol not found errors + +If you get "undefined symbol" errors for your C library functions: +1. Verify the `.so`/`.dylib` is in `lib/` before building +2. Verify the header has `extern "C"` guards +3. Check the symbols are exported: `nm -D lib/libcalc.so | grep calc`