2026-03-06 14:05:53 +00:00
# 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:**
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
- 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
2026-05-28 10:38:04 -04:00
- **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"`
2026-03-06 14:05:53 +00:00
- **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++.
---
2026-03-06 15:55:00 +00:00
## Step 1: Scaffold the Module Project
2026-03-06 14:05:53 +00:00
2026-03-24 18:36:10 +01:00
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.
2026-03-06 14:05:53 +00:00
2026-03-06 15:55:00 +00:00
### 1.1 Create the project using the module builder template
2026-03-06 14:05:53 +00:00
2026-05-29 09:00:47 -04:00
For a module that wraps an external C library:
`mkdir logos-calc-module && cd logos-calc-module`
2026-03-06 14:05:53 +00:00
```bash
2026-04-02 11:02:19 +02:00
nix flake init -t github:logos-co/logos-module-builder#with-external-lib
2026-03-06 15:55:00 +00:00
# Or for a plain module (no external library):
2026-04-02 11:02:19 +02:00
# nix flake init -t github:logos-co/logos-module-builder
2026-03-06 14:05:53 +00:00
```
2026-03-24 18:36:10 +01:00
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.
2026-03-06 15:55:00 +00:00
2026-05-28 10:38:04 -04:00
> **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.
2026-03-06 15:55:00 +00:00
> **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.
2026-05-28 10:38:04 -04:00
---
## 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
2026-03-06 15:55:00 +00:00
```bash
2026-03-31 23:30:01 +01:00
mkdir -p lib
2026-03-06 15:55:00 +00:00
```
2026-05-28 10:38:04 -04:00
### 2.2 Write the C header
2026-03-06 14:05:53 +00:00
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.
2026-05-28 10:38:04 -04:00
### 2.3 Write the C implementation
2026-03-06 14:05:53 +00:00
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" ;
}
```
2026-05-28 10:38:04 -04:00
### 2.4 Build the shared library
2026-03-06 14:05:53 +00:00
```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
```
2026-04-01 12:21:06 +02:00
You should see each symbol marked with `T` (text/code section). Addresses will vary:
2026-03-06 14:05:53 +00:00
```
2026-04-01 12:21:06 +02:00
0000000000001139 T calc_add
0000000000001179 T calc_factorial
00000000000011f5 T calc_fibonacci
0000000000001159 T calc_multiply
0000000000001299 T calc_version
2026-03-06 14:05:53 +00:00
```
> **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/`.
---
2026-05-28 10:38:04 -04:00
## Step 3: Configure the Logos Module
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
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:
2026-03-26 13:45:12 +01:00
2026-03-31 23:30:01 +01:00
| 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) |
2026-03-26 13:45:12 +01:00
| `src/*.h` , `src/*.cpp` | Rename files, replace class/method names, add your wrapping logic |
2026-03-06 14:05:53 +00:00
```
logos-calc-module/
├── flake.nix # Nix build configuration (~10 lines)
2026-05-28 10:38:04 -04:00
├── metadata.json # Module metadata, build settings, and runtime config
├── CMakeLists.txt # CMake build file
2026-03-06 14:05:53 +00:00
├── lib/
│ ├── libcalc.h # C library header
2026-05-28 10:38:04 -04:00
│ └── libcalc.c # C library source (compiled by CMake)
2026-03-06 14:05:53 +00:00
└── src/
├── calc_module_interface.h # Interface declaration
├── calc_module_plugin.h # Plugin header
└── calc_module_plugin.cpp # Plugin implementation (wrapping logic)
```
2026-05-28 10:38:04 -04:00
### 3.1 `metadata.json` — Module Configuration
2026-03-06 14:05:53 +00:00
2026-03-26 13:45:12 +01:00
> **Edit:** Change `name`, `description`, `main`, `nix.external_libraries[].name`, and `nix.cmake.extra_include_dirs` to match your module and library.
2026-03-25 10:37:15 +01:00
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.
2026-03-06 14:05:53 +00:00
```json
{
"name" : "calc_module" ,
"version" : "1.0.0" ,
"type" : "core" ,
"category" : "general" ,
2026-03-24 18:36:10 +01:00
"description" : "Calculator module wrapping libcalc C library" ,
2026-03-06 14:05:53 +00:00
"main" : "calc_module_plugin" ,
2026-03-24 18:36:10 +01:00
"dependencies" : [],
"nix" : {
"packages" : {
"build" : [],
"runtime" : []
},
"external_libraries" : [
{
"name" : "calc" ,
"vendor_path" : "lib"
}
],
"cmake" : {
"find_packages" : [],
"extra_sources" : [],
"extra_include_dirs" : [ "lib" ],
"extra_link_libraries" : []
}
}
2026-03-06 14:05:53 +00:00
}
```
2026-03-24 18:36:10 +01:00
**Key fields explained:**
2026-03-31 23:30:01 +01:00
| Field | What it does |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | Module name — must be a valid C identifier (used in filenames, method calls) |
2026-05-28 10:38:04 -04:00
| `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"` |
2026-03-24 18:36:10 +01:00
2026-05-28 10:38:04 -04:00
### 3.2 `CMakeLists.txt` — Build File
2026-03-06 14:05:53 +00:00
2026-03-26 13:45:12 +01:00
> **Edit:** Change `project()` name, `NAME`, `SOURCES` filenames, and `EXTERNAL_LIBS` to match your module and library.
2026-03-06 14:05:53 +00:00
```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
)
```
2026-03-25 10:37:15 +01:00
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`.
2026-03-06 14:05:53 +00:00
**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.
2026-05-28 10:38:04 -04:00
### 3.3 `flake.nix` — Nix Build Config
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
Change `description` . Add flake inputs here if your module depends on other modules or fetches a library from source.
2026-03-26 13:45:12 +01:00
2026-03-06 14:05:53 +00:00
```nix
{
description = "Calculator module - wraps libcalc C library for Logos" ;
inputs = {
2026-04-02 11:02:19 +02:00
logos-module-builder . url = "github:logos-co/logos-module-builder" ;
2026-03-06 14:05:53 +00:00
};
2026-03-24 18:36:10 +01:00
outputs = inputs @ { logos-module-builder , ... }:
2026-03-06 14:05:53 +00:00
logos-module-builder . lib . mkLogosModule {
src = ./. ;
2026-03-24 18:36:10 +01:00
configFile = ./metadata.json ;
flakeInputs = inputs ;
2026-03-06 14:05:53 +00:00
};
}
```
2026-03-25 10:37:15 +01:00
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.
2026-05-28 10:38:04 -04:00
> **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"`.
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
### 3.4 `src/calc_module_interface.h` — Interface Declaration
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
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` .
2026-03-06 14:05:53 +00:00
```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:**
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
- 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
2026-05-28 10:38:04 -04:00
### 3.5 `src/calc_module_plugin.h` — Plugin Header
2026-03-26 13:45:12 +01:00
2026-03-06 14:05:53 +00:00
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 ;
2026-05-28 10:38:04 -04:00
// PluginInterface
2026-03-06 14:05:53 +00:00
QString name () const override { return "calc_module" ; }
QString version () const override { return "1.0.0" ; }
Q_INVOKABLE void initLogos ( LogosAPI * api );
2026-05-28 10:38:04 -04:00
// CalcModuleInterface
2026-03-06 14:05:53 +00:00
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 ;
2026-04-14 15:30:52 +02:00
Q_INVOKABLE void libVersionNotify ();
2026-03-06 14:05:53 +00:00
signals :
void eventResponse ( const QString & eventName , const QVariantList & args );
};
#endif // CALC_MODULE_PLUGIN_H
```
**Critical details:**
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
- `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`
2026-04-14 15:30:52 +02:00
- `eventResponse` signal is required for event forwarding between modules. Emit it to push data to subscribers (e.g., QML UIs listening via `logos.onModuleEvent()` )
2026-03-24 18:36:10 +01:00
- `name()` must return the same string as the `name` field in `metadata.json`
2026-05-28 10:38:04 -04:00
- **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.
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
### 3.6 `src/calc_module_plugin.cpp` — Plugin Implementation
2026-03-26 13:45:12 +01:00
2026-03-06 14:05:53 +00:00
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 )
{
2026-03-06 15:55:00 +00:00
logosAPI = api ;
2026-03-06 14:05:53 +00:00
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 ;
}
2026-04-14 15:30:52 +02:00
void CalcModulePlugin :: libVersionNotify ()
{
const char * ver = calc_version ();
QString result = QString :: fromUtf8 ( ver );
qDebug () << "CalcModulePlugin::libVersionNotify" << result ;
emit eventResponse ( "versionReady" , { result });
}
2026-03-06 14:05:53 +00:00
```
**The wrapping pattern** is always the same:
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
1. Call the C function with the arguments
2026-05-28 10:38:04 -04:00
2. Convert the C result to a Qt type if needed (e.g., `const char*` → `QString` )
2026-03-06 14:05:53 +00:00
3. Return the Qt type
---
2026-05-28 10:38:04 -04:00
## Step 4: Build the Module
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
### 4.1 Initialize the Git repo
2026-03-06 14:05:53 +00:00
2026-03-27 16:45:30 +11:00
Nix flakes require a git repository.
2026-04-01 20:34:09 +02:00
Before staging files, create a `.gitignore` to exclude build artifacts:
2026-03-27 16:45:30 +11:00
2026-05-28 10:38:04 -04:00
```text
2026-03-27 16:45:30 +11:00
# Nix build output
result
result-*
# CMake build directory
build/
```
Then initialise the repo:
2026-03-06 14:05:53 +00:00
```bash
git init
2026-05-28 10:38:04 -04:00
```
```bash
2026-03-06 14:05:53 +00:00
git add -A
2026-05-28 10:38:04 -04:00
```
```bash
2026-04-01 17:57:24 +02:00
nix flake update
2026-05-28 10:38:04 -04:00
```
```bash
2026-04-01 17:57:24 +02:00
git add flake.lock
2026-03-06 14:05:53 +00:00
```
2026-05-28 10:38:04 -04:00
### 4.2 Build the plugin library
Build just the plugin library (`.so` / `.dylib` ):
2026-03-06 14:05:53 +00:00
```bash
2026-03-06 15:55:00 +00:00
nix build '.#lib'
2026-05-28 10:38:04 -04:00
```
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
> **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 (5– 15 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
2026-03-06 14:05:53 +00:00
nix build
```
2026-05-28 10:38:04 -04:00
### 4.4 Inspect the output
2026-03-06 14:05:53 +00:00
```bash
ls -la result/lib/
```
2026-03-06 15:55:00 +00:00
You should see two files (extensions depend on your platform):
2026-03-06 14:05:53 +00:00
```
2026-03-06 15:55:00 +00:00
# Linux
2026-03-06 14:05:53 +00:00
calc_module_plugin.so # Your Logos module plugin
libcalc.so # The C library (copied alongside)
2026-03-06 15:55:00 +00:00
# macOS
calc_module_plugin.dylib
libcalc.dylib
2026-03-06 14:05:53 +00:00
```
2026-03-26 11:48:27 +01:00
Both library files are placed together so the plugin can find the C library at runtime via RPATH.
2026-03-06 14:05:53 +00:00
---
2026-05-28 10:38:04 -04:00
## Step 5: Inspect the Module
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
Use the `lm` CLI tool (from `logos-module` ) to inspect the compiled module binary.
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
### 5.1 Build the `lm` tool
The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo:
2026-03-06 14:05:53 +00:00
```bash
2026-04-02 11:02:19 +02:00
nix build 'github:logos-co/logos-module#lm' --out-link ./lm
2026-03-06 14:05:53 +00:00
```
2026-05-28 10:38:04 -04:00
### 5.2 View metadata
2026-03-06 14:05:53 +00:00
```bash
2026-03-06 15:55:00 +00:00
# Linux
2026-03-06 14:05:53 +00:00
./lm/bin/lm metadata result/lib/calc_module_plugin.so
2026-03-06 15:55:00 +00:00
# macOS
./lm/bin/lm metadata result/lib/calc_module_plugin.dylib
2026-03-06 14:05:53 +00:00
```
Output:
```
Plugin Metadata:
================
Name: calc_module
Version: 1.0.0
Description: Calculator module wrapping libcalc C library
Author:
Type: core
Dependencies: (none)
```
2026-05-28 10:38:04 -04:00
### 5.3 List methods
2026-03-06 14:05:53 +00:00
```bash
2026-03-06 15:55:00 +00:00
# Linux
2026-03-06 14:05:53 +00:00
./lm/bin/lm methods result/lib/calc_module_plugin.so
2026-03-06 15:55:00 +00:00
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib
2026-03-06 14:05:53 +00:00
```
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.
2026-05-28 10:38:04 -04:00
### 5.4 JSON output
2026-03-06 14:05:53 +00:00
For scripting and CI, use `--json` :
```bash
2026-03-06 15:55:00 +00:00
# Linux
2026-03-06 14:05:53 +00:00
./lm/bin/lm methods result/lib/calc_module_plugin.so --json
2026-03-06 15:55:00 +00:00
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib --json
2026-03-06 14:05:53 +00:00
```
```json
[
{
"isInvokable" : true ,
"name" : "add" ,
"parameters" : [
{ "name" : "a" , "type" : "int" },
{ "name" : "b" , "type" : "int" }
],
"returnType" : "int" ,
"signature" : "add(int,int)"
},
...
]
```
---
2026-05-28 10:38:04 -04:00
## Step 6: Test with `logoscore`
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
### 6.1 Build logoscore
2026-03-06 14:05:53 +00:00
```bash
2026-04-02 11:02:19 +02:00
nix build 'github:logos-co/logos-logoscore-cli' --out-link ./logos
2026-03-06 14:05:53 +00:00
```
2026-05-28 10:38:04 -04:00
### 6.2 Set up the modules directory
2026-03-06 14:05:53 +00:00
2026-03-26 11:48:27 +01:00
`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:
2026-03-06 14:05:53 +00:00
```bash
2026-03-26 11:48:27 +01:00
nix build '.#lgx'
2026-05-28 10:38:04 -04:00
```
2026-03-06 15:55:00 +00:00
2026-05-28 10:38:04 -04:00
```bash
2026-04-02 11:02:19 +02:00
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
2026-05-28 10:38:04 -04:00
```
```bash
2026-03-07 11:50:55 +00:00
mkdir -p modules
2026-05-28 10:38:04 -04:00
```
```bash
2026-03-26 11:48:27 +01:00
./pm/bin/lgpm --modules-dir ./modules install --file result/*.lgx
2026-03-06 14:05:53 +00:00
```
2026-03-07 11:50:55 +00:00
This extracts the plugin, external libraries, and manifest into the correct directory structure:
2026-03-06 14:05:53 +00:00
2026-03-07 11:50:55 +00:00
```
modules/calc_module/
├── calc_module_plugin.dylib # (or .so on Linux)
├── libcalc.dylib # (or .so on Linux)
2026-03-25 10:37:15 +01:00
├── manifest.json # Auto-generated by lgx
└── variant # Platform variant identifier
2026-03-06 14:05:53 +00:00
```
2026-05-28 10:38:04 -04:00
### 6.3 Call methods
2026-03-06 14:05:53 +00:00
2026-03-25 10:37:15 +01:00
Start the daemon and call methods:
2026-03-06 14:05:53 +00:00
```bash
2026-03-25 10:37:15 +01:00
./logos/bin/logoscore -D -m ./modules &
2026-05-28 10:38:04 -04:00
```
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
```bash
sleep 3
```
```bash
2026-03-25 10:37:15 +01:00
./logos/bin/logoscore load-module calc_module
2026-05-28 10:38:04 -04:00
```
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
```bash
2026-03-25 10:37:15 +01:00
./logos/bin/logoscore call calc_module add 3 5
2026-05-28 10:38:04 -04:00
```
2026-03-06 14:05:53 +00:00
2026-05-28 10:38:04 -04:00
```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
2026-03-25 10:37:15 +01:00
./logos/bin/logoscore stop
2026-03-06 14:05:53 +00:00
```
2026-03-25 10:37:15 +01:00
> For inline (legacy) mode and other logoscore options, see the [Developer Guide -- Running with logoscore](logos-developer-guide.md#51-running-with-logoscore).
2026-03-31 23:30:01 +01:00
**What happens under the hood:**
2026-03-06 14:05:53 +00:00
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`
2026-03-25 10:37:15 +01:00
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]`
2026-03-06 14:05:53 +00:00
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: ...
```
---
2026-05-28 10:38:04 -04:00
## Package for Distribution (Optional)
2026-03-06 14:05:53 +00:00
2026-03-26 11:48:27 +01:00
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:
2026-03-06 14:05:53 +00:00
```bash
2026-03-26 11:48:27 +01:00
nix build '.#lgx-portable'
2026-03-06 14:05:53 +00:00
```
2026-03-07 11:50:55 +00:00
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.
2026-03-06 14:05:53 +00:00
2026-04-01 17:57:24 +02:00
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:
2026-03-26 11:48:27 +01:00
```bash
2026-04-01 17:57:24 +02:00
nix build '.#lgx' --out-link result-lgx
nix build '.#lgx-portable' --out-link result-lgx-portable
2026-03-26 11:48:27 +01:00
```
> 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).
2026-03-07 11:50:55 +00:00
To install a portable package on another machine:
2026-03-06 14:05:53 +00:00
```bash
2026-04-02 11:02:19 +02:00
nix build 'github:logos-co/logos-package-manager#cli' --out-link ./pm
2026-04-01 17:57:24 +02:00
./pm/bin/lgpm --modules-dir ./modules install --file result-lgx-portable/*.lgx
2026-03-06 14:05:53 +00:00
```
2026-03-18 14:22:43 +01:00
> **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.
2026-03-07 11:50:55 +00:00
2026-03-06 14:05:53 +00:00
## 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
2026-03-06 16:17:19 +00:00
| 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) | — | — |
2026-03-06 14:05:53 +00:00
## 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 = {
2026-04-02 11:02:19 +02:00
logos-module-builder . url = "github:logos-co/logos-module-builder" ;
2026-03-06 14:05:53 +00:00
# Fetch the library source (non-flake)
libfoo-src = {
url = "github:example/libfoo" ;
flake = false ;
};
};
2026-03-24 18:36:10 +01:00
outputs = inputs @ { logos-module-builder , libfoo-src , ... }:
2026-03-06 14:05:53 +00:00
logos-module-builder . lib . mkLogosModule {
src = ./. ;
2026-03-24 18:36:10 +01:00
configFile = ./metadata.json ;
flakeInputs = inputs ;
2026-03-06 14:05:53 +00:00
# Pass the fetched source to the builder
externalLibInputs = {
foo = libfoo-src ;
};
};
}
```
2026-03-24 18:36:10 +01:00
### metadata.json for flake input
2026-03-06 14:05:53 +00:00
2026-03-24 18:36:10 +01:00
```json
{
"name" : "foo_module" ,
"version" : "1.0.0" ,
"type" : "core" ,
"description" : "Module wrapping libfoo" ,
"main" : "foo_module_plugin" ,
"dependencies" : [],
2026-03-06 14:05:53 +00:00
2026-03-24 18:36:10 +01:00
"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" : []
}
}
}
2026-03-06 14:05:53 +00:00
```
2026-03-24 18:36:10 +01:00
**Key difference:** The `externalLibInputs` key in flake.nix (`foo` ) must match the `name` field in `nix.external_libraries` (`foo` ). The builder will:
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
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
2026-03-24 18:36:10 +01:00
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` :
2026-03-06 14:05:53 +00:00
2026-03-24 18:36:10 +01:00
```json
{
"nix" : {
"external_libraries" : [
{
"name" : "mygolib" ,
"flake_input" : "github:example/mygolib" ,
"go_build" : true ,
"output_pattern" : "libmygolib.*"
}
]
}
}
2026-03-06 14:05:53 +00:00
```
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:
2026-03-06 16:17:19 +00:00
- `**flake.nix**` — Uses `externalLibInputs` to fetch the nim-libp2p C bindings from a GitHub flake
2026-03-24 18:36:10 +01:00
- `**metadata.json**` — Declares `nim_libp2p` as an external library with `go_build: false` in the `nix` section
2026-03-06 16:17:19 +00:00
- `**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
2026-03-06 14:05:53 +00:00
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
```
2026-03-07 11:50:55 +00:00
**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.
2026-03-06 15:55:00 +00:00
### `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 ;
}
```
2026-03-06 14:05:53 +00:00
### Plugin not discovered by logoscore
**Check:**
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
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` )
2026-03-06 15:55:00 +00:00
### `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
```
2026-03-06 14:05:53 +00:00
### 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:
2026-03-06 16:17:19 +00:00
2026-03-06 14:05:53 +00:00
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`