Files
logos-tutorial/tutorial-wrapping-c-library.md
2026-05-29 09:01:48 -04:00

1088 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
mkdir -p ~/.config/nix
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
```
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
- **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: Scaffold the Module Project
Before writing any C code, scaffold the Logos module project using the official template. This gives you the correct `flake.nix`, `metadata.json`, directory structure, and build configuration out of the box.
### 1.1 Create the project using the module builder template
For a module that wraps an external C library:
`mkdir logos-calc-module && cd logos-calc-module`
```bash
nix flake init -t github:logos-co/logos-module-builder#with-external-lib
# Or for a plain module (no external library):
# nix flake init -t github:logos-co/logos-module-builder
```
This generates the skeleton files (`flake.nix`, `metadata.json`, `CMakeLists.txt`, etc.) pre-configured for the logos-module-builder. You then customize them for your specific library.
> **Note:** The generated `flake.nix` uses an unpinned `logos-module-builder` URL. Replace it with the pinned version shown in the flake.nix step below to ensure reproducible builds.
> **Alternative approach:** You can also create the C library as a separate project, build it there, then copy the resulting `.so`/`.dylib` and header files into the module's `lib/` directory. This can be cleaner for larger libraries with their own build systems.
---
## Step 2: Write the C Library
Create the C library that your module will wrap. Place the header and implementation in the `lib/` directory.
### 2.1 Create the lib directory
```bash
mkdir -p lib
```
### 2.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.
### 2.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";
}
```
### 2.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 each symbol marked with `T` (text/code section). Addresses will vary:
```
0000000000001139 T calc_add
0000000000001179 T calc_factorial
00000000000011f5 T calc_fibonacci
0000000000001159 T calc_multiply
0000000000001299 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 3: Configure the Logos Module
The template generated skeleton files with placeholder names (`external_lib`, `example_lib`). Now rename and customize them for your library. You need to edit **every generated file**.
After editing, your project should look like this:
| File | What to change |
| ---------------------- | ----------------------------------------------------------------- |
| `metadata.json` | Module name, description, library name, include dirs |
| `CMakeLists.txt` | Project name, module name, source filenames, library name |
| `flake.nix` | Description (and dependency inputs if needed) |
| `src/*.h`, `src/*.cpp` | Rename files, replace class/method names, add your wrapping logic |
```
logos-calc-module/
├── flake.nix # Nix build configuration (~10 lines)
├── metadata.json # Module metadata, build settings, and runtime config
├── CMakeLists.txt # CMake build file
├── lib/
│ ├── libcalc.h # C library header
│ └── libcalc.c # C library source (compiled by CMake)
└── src/
├── calc_module_interface.h # Interface declaration
├── calc_module_plugin.h # Plugin header
└── calc_module_plugin.cpp # Plugin implementation (wrapping logic)
```
### 3.1 `metadata.json` — Module Configuration
> **Edit:** Change `name`, `description`, `main`, `nix.external_libraries[].name`, and `nix.cmake.extra_include_dirs` to match your module and library.
This is the single source of truth for your module. It is embedded into the plugin binary by Qt's `Q_PLUGIN_METADATA` macro (for runtime metadata), read by `logos-module-builder` to configure the Nix build, used by CMake to resolve external dependencies and link libraries (via the `nix` section), and used by `nix-bundle-lgx` to generate the LGX manifest.
```json
{
"name": "calc_module",
"version": "1.0.0",
"type": "core",
"category": "general",
"description": "Calculator module wrapping libcalc C library",
"main": "calc_module_plugin",
"dependencies": [],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"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) |
| `nix.external_libraries` | Declares C/C++ libraries vendored in the repo. Each entry has a `name` (used for the Nix derivation and CMake target) and `vendor_path` (directory containing the source). The build system compiles the library and makes it available as a CMake target |
| `nix.cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "libcalc.h"` |
### 3.2 `CMakeLists.txt` — Build File
> **Edit:** Change `project()` name, `NAME`, `SOURCES` filenames, and `EXTERNAL_LIBS` to match your module and library.
```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
)
```
The template generates this with default names (e.g., `external_lib`). You **must** update:
- **`project()`** — rename to match your module (e.g., `CalcModulePlugin`)
- **`NAME`** — your module name (must match `name` in `metadata.json`, e.g., `calc_module`)
- **`SOURCES`** — your renamed source files
- **`EXTERNAL_LIBS`** — names of external libraries to link (must match `nix.external_libraries[].name` in `metadata.json`)
The `if/elseif/else` block above it is boilerplate — don't change it.
> **Common mistake:** If `NAME` doesn't match `name` in `metadata.json`, the build will succeed but the install phase will fail because it looks for `<name>_plugin.dylib` based on `metadata.json`.
**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.
### 3.3 `flake.nix` — Nix Build Config
Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source.
```nix
{
description = "Calculator module - wraps libcalc C library for Logos";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
```
That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, setting up include paths, etc.). Note that `configFile` points to `metadata.json` (the single source of truth) and `flakeInputs = inputs` passes all flake inputs to the builder so that dependencies declared in `metadata.json` are resolved automatically.
> **Naming flake inputs:** When adding module dependencies, the flake input attribute name **must match** the `name` field in that dependency's `metadata.json`. For example, if you depend on a module whose `metadata.json` has `"name": "waku_module"`, your flake input must be `waku_module.url = "github:logos-co/logos-waku-module"`.
### 3.4 `src/calc_module_interface.h` — Interface Declaration
This declares the methods your module exposes. It inherits from `PluginInterface` (provided by the Logos C++ SDK). Every method you want callable by other modules must be `Q_INVOKABLE` and `virtual`.
```cpp
#ifndef CALC_MODULE_INTERFACE_H
#define CALC_MODULE_INTERFACE_H
#include <QObject>
#include <QString>
#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
### 3.5 `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 <QObject>
#include <QString>
#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
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;
Q_INVOKABLE void libVersionNotify();
signals:
void eventResponse(const QString& eventName, const QVariantList& args);
};
#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. Emit it to push data to subscribers (e.g., QML UIs listening via `logos.onModuleEvent()`)
- `name()` must return the same string as the `name` field in `metadata.json`
- **No `m_logosAPI` member variable** — the `LogosAPI*` pointer is stored in the global `logosAPI` variable defined in `liblogos`, not in a class member. See the `initLogos` implementation below.
### 3.6 `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 <QDebug>
CalcModulePlugin::CalcModulePlugin(QObject* parent)
: QObject(parent)
{
qDebug() << "CalcModulePlugin: created";
}
CalcModulePlugin::~CalcModulePlugin()
{
qDebug() << "CalcModulePlugin: destroyed";
}
void CalcModulePlugin::initLogos(LogosAPI* api)
{
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;
}
void CalcModulePlugin::libVersionNotify()
{
const char* ver = calc_version();
QString result = QString::fromUtf8(ver);
qDebug() << "CalcModulePlugin::libVersionNotify" << result;
emit eventResponse("versionReady", {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 4: Build the Module
### 4.1 Initialize the Git repo
Nix flakes require a git repository.
Before staging files, create a `.gitignore` to exclude build artifacts:
```text
# Nix build output
result
result-*
# CMake build directory
build/
```
Then initialise the repo:
```bash
git init
```
```bash
git add -A
```
```bash
nix flake update
```
```bash
git add flake.lock
```
### 4.2 Build the plugin library
Build just the plugin library (`.so` / `.dylib`):
```bash
nix build '.#lib'
```
> **Quoting matters:** Use `'.#lib'` (with quotes) rather than bare `nix build .#lib`. Some shells (especially zsh) may interpret the `#` as a comment character.
The first build takes a while (515 minutes) as Nix downloads Qt, the Logos SDK, and other dependencies. Subsequent builds are fast due to caching.
### 4.3 Build the full package
Build everything (library + generated SDK headers):
```bash
nix build
```
### 4.4 Inspect the output
```bash
ls -la result/lib/
```
You should see two files (extensions depend on your platform):
```
# Linux
calc_module_plugin.so # Your Logos module plugin
libcalc.so # The C library (copied alongside)
# macOS
calc_module_plugin.dylib
libcalc.dylib
```
Both library files are placed together so the plugin can find the C library at runtime via RPATH.
---
## Step 5: Inspect the Module
Use the `lm` CLI tool (from `logos-module`) to inspect the compiled module binary.
### 5.1 Build the `lm` tool
The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo:
```bash
nix build 'github:logos-co/logos-module#lm' --out-link ./lm
```
### 5.2 View metadata
```bash
# Linux
./lm/bin/lm metadata result/lib/calc_module_plugin.so
# macOS
./lm/bin/lm metadata result/lib/calc_module_plugin.dylib
```
Output:
```
Plugin Metadata:
================
Name: calc_module
Version: 1.0.0
Description: Calculator module wrapping libcalc C library
Author:
Type: core
Dependencies: (none)
```
### 5.3 List methods
```bash
# Linux
./lm/bin/lm methods result/lib/calc_module_plugin.so
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib
```
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.
### 5.4 JSON output
For scripting and CI, use `--json`:
```bash
# Linux
./lm/bin/lm methods result/lib/calc_module_plugin.so --json
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib --json
```
```json
[
{
"isInvokable": true,
"name": "add",
"parameters": [
{ "name": "a", "type": "int" },
{ "name": "b", "type": "int" }
],
"returnType": "int",
"signature": "add(int,int)"
},
...
]
```
---
## Step 6: Test with `logoscore`
### 6.1 Build logoscore
```bash
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
```
### 6.2 Set up the modules directory
`logoscore` expects modules in subdirectories, each with a `manifest.json`. Rather than copying files and writing the manifest manually, use the Nix derivation to create an LGX package and install it with the package manager:
```bash
nix build '.#lgx'
```
```bash
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
```
```bash
mkdir -p modules
```
```bash
./pm/bin/lgpm --modules-dir ./modules install --file result/*.lgx
```
This extracts the plugin, external libraries, and manifest into the correct directory structure:
```
modules/calc_module/
├── calc_module_plugin.dylib # (or .so on Linux)
├── libcalc.dylib # (or .so on Linux)
├── manifest.json # Auto-generated by lgx
└── variant # Platform variant identifier
```
### 6.3 Call methods
Start the daemon and call methods:
```bash
./logos/bin/logoscore -D -m ./modules &
```
```bash
sleep 3
```
```bash
./logos/bin/logoscore load-module calc_module
```
```bash
./logos/bin/logoscore call calc_module add 3 5
```
```bash
./logos/bin/logoscore call calc_module factorial 5
```
```bash
./logos/bin/logoscore call calc_module fibonacci 10
```
```bash
./logos/bin/logoscore call calc_module libVersion
```
```bash
./logos/bin/logoscore stop
```
> For inline (legacy) mode and other logoscore options, see the [Developer Guide -- Running with logoscore](logos-developer-guide.md#51-running-with-logoscore).
**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 call 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: ...
```
---
## Package for Distribution (Optional)
The LGX package created in Step 5.2 is a **local** package — its libraries still reference `/nix/store` paths, so it only works on the machine that built it. To create a **portable** package that can be distributed to other machines:
```bash
nix build '.#lgx-portable'
```
Portable LGX packages are fully self-contained with no `/nix/store` references at runtime. These are the packages used by the Logos App Package Manager UI and published to [logos-modules](https://github.com/logos-co/logos-modules) releases.
To create both dev and portable variants (the dev variant works with local `nix build` of basecamp; the portable variant works with standalone basecamp builds), use `--out-link` to avoid overwriting the `result` symlink:
```bash
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
```
> For more bundling options (standalone bundler syntax, cross-platform packaging), see the [Developer Guide — Bundling with nix-bundle-lgx](logos-developer-guide.md#32-bundling-with-nix-bundle-lgx).
To install a portable package on another machine:
```bash
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
./pm/bin/lgpm --modules-dir ./modules install --file result-lgx-portable/*.lgx
```
> **Note:** Local builds of `logoscore` / `logos-basecamp` (via `nix build`) expect **local** `.lgx` packages. Portable builds (via `nix build '.#bin-bundle-dir'`, `.#bin-appimage`, or `.#bin-macos-app`) expect **portable** `.lgx` packages. See the [logos-basecamp README](https://github.com/logos-co/logos-basecamp/blob/master/README.md) for details.
## 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<MyPlugin*>(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";
# Fetch the library source (non-flake)
libfoo-src = {
url = "github:example/libfoo";
flake = false;
};
};
outputs = inputs@{ logos-module-builder, libfoo-src, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
# Pass the fetched source to the builder
externalLibInputs = {
foo = libfoo-src;
};
};
}
```
### metadata.json for flake input
```json
{
"name": "foo_module",
"version": "1.0.0",
"type": "core",
"description": "Module wrapping libfoo",
"main": "foo_module_plugin",
"dependencies": [],
"nix": {
"packages": { "build": [], "runtime": [] },
"external_libraries": [
{
"name": "foo",
"flake_input": "github:example/libfoo",
"build_command": "make shared",
"output_pattern": "build/libfoo.*"
}
],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": ["lib"],
"extra_link_libraries": []
}
}
}
```
**Key difference:** The `externalLibInputs` key in flake.nix (`foo`) must match the `name` field in `nix.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`), set `go_build: true` in the `nix.external_libraries` entry within `metadata.json`:
```json
{
"nix": {
"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
- `**metadata.json**` — Declares `nim_libp2p` as an external library with `go_build: false` in the `nix` section
- `**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` / `libcalc.dylib` is in the same directory as the plugin. The build system sets RPATH to `$ORIGIN` (Linux) / `@loader_path` (macOS) so the plugin looks for libraries in its own directory.
### `initLogos` stores API pointer in wrong variable
If inter-module calls or API features silently fail, check that `initLogos` assigns to the **global** `logosAPI` variable (defined in the Logos SDK / liblogos), not to a class member like `m_logosAPI`:
```cpp
// CORRECT — uses the global variable from liblogos
void MyPlugin::initLogos(LogosAPI* api)
{
logosAPI = api;
}
// WRONG — stores in a local member, API calls won't work
void MyPlugin::initLogos(LogosAPI* api)
{
m_logosAPI = api;
}
```
### 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`)
### `nix build .#lib` does nothing or fails silently
Some shells (notably zsh) treat `#` as a comment character. Always quote the flake reference:
```bash
# Correct
nix build '.#lib'
# May fail in zsh
nix build .#lib
```
### 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`