Files
logos-tutorial/tests/tutorial-wrapping-c-library.test.yaml

1378 lines
65 KiB
YAML
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.
name: "Tutorial: Wrapping a C Library as a Logos Module"
output: tutorial-wrapping-c-library.md
project_name: logos-calc-module
release: ""
intro: |
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_build: "A `calc_module` that wraps a tiny C calculator library (`libcalc`), exposing arithmetic functions to the Logos platform. You write a single **plain C++ class** — no Qt, no plugin boilerplate — and the build system generates the Qt plugin around it."
what_you_learn:
- How a Logos module wraps a C library using the pure-C++ (`interface: universal`) pattern
- The role of each file in the module project
- "Which C++ types the code generator maps onto the wire (`std::string`, `int64_t`, `bool`, …)"
- How to emit events from a plain C++ class with `logos_events:`
- How to build, inspect, and unit-test your module (with the Logos Test Framework)
- 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++."
sections:
# ── Step 1: Scaffold ──────────────────────────────────────────────────────
- title: "Scaffold the Module Project"
step: true
text: |
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.
steps:
- title: "Create the project using the module builder template"
text: |
For a module that wraps an external C library:
`mkdir logos-calc-module && cd logos-calc-module`
run: "nix flake init -t github:logos-co/logos-module-builder{release}#with-external-lib"
code_block: |
nix flake init -t github:logos-co/logos-module-builder{release}#with-external-lib
# Or for a plain module (no external library):
# nix flake init -t github:logos-co/logos-module-builder{release}
post_text: |
This generates skeleton files (`flake.nix`, `metadata.json`, `CMakeLists.txt`, and a `src/` directory) pre-configured for the logos-module-builder. You then customize them for your specific library.
> **Heads up — the template is the older Qt-plugin style.** As of this writing, `nix flake init` scaffolds a hand-written Qt plugin (`*_interface.h` + `*_plugin.h` + `*_plugin.cpp`). This tutorial uses the newer and simpler **pure-C++ pattern** instead: you write one plain `*_impl.h` / `*_impl.cpp` class with no Qt in it, set `"interface": "universal"` in `metadata.json`, and the build generates the Qt plugin wrapper for you. So in the steps below we **replace** the template's `src/` files entirely. We still use `nix flake init` to get the `flake.nix` / `CMakeLists.txt` skeleton and directory layout.
> **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.
- title: "Remove the template's example sources"
text: |
The `with-external-lib` template ships an example Qt plugin (`external_lib_*`). Delete those files — this tutorial supplies its own pure-C++ `src/` files:
run: "rm -f src/external_lib_interface.h src/external_lib_plugin.h src/external_lib_plugin.cpp"
# ── Step 2: Write the C library ────────────────────────────────────────────
- title: "Write the C Library"
step: true
text: |
Create the C library that your module will wrap. Place the header and implementation in the `lib/` directory.
steps:
- title: "Create the lib directory"
run: "mkdir -p lib"
- title: "Write the C header"
text: "Create `lib/libcalc.h`:"
file:
path: lib/libcalc.h
language: c
content: |
#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 */
post_text: |
The `extern "C"` block is essential — it prevents C++ name mangling so the Logos module can find the symbols.
- title: "Write the C implementation"
text: "Create `lib/libcalc.c`:"
file:
path: lib/libcalc.c
language: c
content: |
#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";
}
- title: "Build the shared library"
run: "cd lib && gcc {shared_flags} -o libcalc.{ext} libcalc.c && cd .."
code_block: |
cd lib
# Linux
gcc -shared -fPIC -o libcalc.so libcalc.c
# macOS
# gcc -shared -fPIC -o libcalc.dylib libcalc.c
cd ..
post_text: "Verify the symbols are exported:"
extra_run:
# Plain `nm`, no flags: the executed command has to work on BOTH
# platforms, and there is no shared flag that does. `-gU` is macOS
# ("defined only"); GNU nm rejects it and falls back to looking for
# a.out, which fails the step on Linux. The code_block below still
# shows the idiomatic per-platform form.
run: "nm lib/libcalc.{ext} | grep calc"
code_block: |
# Linux
nm -D lib/libcalc.so | grep calc
# macOS
# nm -gU lib/libcalc.dylib | grep calc
post_text: |
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 ──────────────────────────────────────
- title: "Configure the Logos Module"
step: true
text: |
Now write the files that turn your C library into a Logos module. With the **pure-C++ (`universal`) pattern** you only hand-write a single C++ class — `metadata.json`, `CMakeLists.txt`, and `flake.nix` tell the build system the rest, and `logos-cpp-generator` synthesizes the Qt plugin wrapper.
After this step your project will look like this:
| File | Role |
| ----------------------------- | ----------------------------------------------------------------- |
| `metadata.json` | Module metadata + nix build settings (note `interface: universal`)|
| `CMakeLists.txt` | Lists your impl source files |
| `flake.nix` | Nix build (description, dependency inputs) |
| `src/calc_module_impl.h` | Plain C++ class declaration — **no Qt** |
| `src/calc_module_impl.cpp` | Implementation: each method calls the C library |
```
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_impl.h # Plain C++ class (no Qt, no plugin macros)
└── calc_module_impl.cpp # Implementation (wrapping logic)
```
> **Where did the `*_interface.h` / `*_plugin.h` / `*_plugin.cpp` files go?** The older pattern made you hand-write a Qt `QObject` plugin, an abstract interface, and the `Q_INVOKABLE` / `Q_PLUGIN_METADATA` boilerplate. With `interface: universal`, the generator derives all of that from your plain class — so those three files no longer exist in your source tree. They are emitted into `generated_code/` at build time.
steps:
- title: "`metadata.json` — Module Configuration"
text: |
> **Edit:** Set `name`, `description`, `main`, add `"interface": "universal"`, and declare your library under `nix.external_libraries`.
This is the single source of truth for your module. It is embedded into the generated plugin binary (for runtime metadata via `lm`), read by `logos-module-builder` to configure the Nix build, used by CMake to resolve and link external libraries (via the `nix` section), and used by `nix-bundle-lgx` to generate the LGX manifest.
file:
path: metadata.json
language: json
content: |
{
"name": "calc_module",
"version": "1.0.0",
"type": "core",
"category": "general",
"description": "Calculator module wrapping libcalc C library",
"main": "calc_module_plugin",
"interface": "universal",
"dependencies": [],
"nix": {
"packages": {
"build": [],
"runtime": []
},
"external_libraries": [
{
"name": "calc",
"vendor_path": "lib"
}
],
"cmake": {
"find_packages": [],
"extra_sources": [],
"extra_include_dirs": ["lib"],
"extra_link_libraries": []
}
}
}
post_text: |
**Key fields explained:**
| Field | What it does |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name` | Module name — must be a valid C identifier (used in filenames, method calls) |
| `main` | The generated plugin's name, `<name>_plugin`. You don't write this file; the builder produces `calc_module_plugin.so` / `.dylib` |
| `interface` | `"universal"` selects the pure-C++ pattern. The builder runs `logos-cpp-generator --from-header` over `src/calc_module_impl.h` and emits the Qt plugin glue, so you never touch Qt directly |
| `nix.external_libraries` | Declares C/C++ libraries vendored in the repo. Each entry has a `name` (the CMake target) and `vendor_path` (directory with the source/binary). The build compiles the library and links it into the plugin |
| `nix.cmake.extra_include_dirs` | Added to the include path so your C++ code can `#include "lib/libcalc.h"` |
- title: "`CMakeLists.txt` — Build File"
text: |
> **Edit:** Set `project()` name, `NAME`, the `SOURCES` (your two impl files), and `EXTERNAL_LIBS`.
For a universal module you list only your plain C++ source files. The generated glue (`generated_code/*.cpp`) is picked up automatically by `LogosModule.cmake` — you don't reference it here.
file:
path: CMakeLists.txt
language: cmake
content: |
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.
# Because metadata.json sets `interface: universal`, the builder runs
# logos-cpp-generator over src/calc_module_impl.h before configuring,
# and LogosModule.cmake compiles the generated glue automatically.
logos_module(
NAME calc_module
SOURCES
src/calc_module_impl.h
src/calc_module_impl.cpp
EXTERNAL_LIBS
calc
)
post_text: |
You **must** keep these in sync with `metadata.json`:
- **`NAME`** — your module name (must match `name` in `metadata.json`, e.g., `calc_module`)
- **`SOURCES`** — your impl files (`src/calc_module_impl.h`, `src/calc_module_impl.cpp`)
- **`EXTERNAL_LIBS`** — 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 may succeed but the install phase fails because it looks for `<name>_plugin.so`/`.dylib` based on `metadata.json`.
**How `EXTERNAL_LIBS calc` works:** `logos_module()` searches `lib/` for `libcalc.so` (Linux) / `libcalc.dylib` (macOS), links it to your plugin, and sets up RPATH so the library is found at runtime.
- title: "`flake.nix` — Nix Build Config"
text: |
Change `description`. Add flake inputs here if your module depends on other modules or fetches a library from source.
file:
path: flake.nix
language: nix
content: |
{
description = "Calculator module - wraps libcalc C library for Logos";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
};
}
post_text: |
That's it — `mkLogosModule` handles all the Nix complexity (fetching Qt, the SDK, the code generator, running `logos-cpp-generator --from-header`, setting up include paths, etc.). `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"`.
- title: "`src/calc_module_impl.h` — The Module Class"
text: |
This is the **only interface you write**, and it's plain C++ — no `QObject`, no `Q_INVOKABLE`, no plugin macros, no Qt headers at all. Every `public` method becomes a method other modules (and `logoscore`) can call. The code generator parses this header as text to derive the wire signatures, so keep it to the supported types (see the table below).
We also inherit `LogosModuleContext` so the class can emit events (the `logos_events:` block) and, if needed later, call other modules — without ever touching the raw `LogosAPI`.
file:
path: src/calc_module_impl.h
language: cpp
content: |
#pragma once
#include <cstdint>
#include <string>
#include <logos_module_context.h> // LogosModuleContext base + `logos_events:`
// Include the C library header (extern "C" already in the header).
extern "C" {
#include "lib/libcalc.h"
}
class CalcModuleImpl : public LogosModuleContext {
public:
CalcModuleImpl() = default;
~CalcModuleImpl() = default;
// ── Public API — every method here is callable over IPC ──────────
// The generator maps C++ types onto the contract automatically:
// int64_t ↔ int std::string ↔ tstr bool ↔ bool
//
// A doc comment directly above a method becomes that method's
// `description` in the module's method introspection — surfaced
// by `lm`, `logoscore module-info`, and Basecamp's Methods list.
// Use `///` (one or more lines) or a `/** ... */` block; the
// comment's line breaks are preserved. (Plain `//` comments like
// this block are ignored, so they never leak into the API.)
/// Adds two integers and returns the sum.
int64_t add(int64_t a, int64_t b);
/// Multiplies two integers and returns the product.
int64_t multiply(int64_t a, int64_t b);
// A multi-line description: consecutive `///` lines keep their breaks.
/// Computes the factorial n! of a non-negative integer.
/// Defined as n * (n-1) * ... * 1, with 0! = 1.
int64_t factorial(int64_t n);
/// Returns the nth Fibonacci number (0-indexed).
int64_t fibonacci(int64_t n);
// A `/** ... */` block comment works too (line breaks preserved).
/**
* Returns the version string of the wrapped libcalc C library.
* Read straight from the linked native library, not metadata.json.
*/
std::string libVersion();
/// Looks up the library version and emits it as a `versionReady`
/// event instead of returning it. Used by the QML tutorial (Part 2).
void libVersionNotify();
// ── Events ───────────────────────────────────────────────────────
// Declared like Qt signals. The generator emits the body (in
// calc_module_events.cpp) that routes the typed args to subscribers
// via the host's `eventResponse` mechanism. QML subscribes with
// logos.onModuleEvent("calc_module", "versionReady").
//
// A `///` doc comment documents the event too — it surfaces as the
// event's `description` alongside methods (`lm events`, `logoscore
// module-info`, and Basecamp's Interface screen).
logos_events:
/// Emitted by libVersionNotify() once the library version is known.
/// Carries the version string read from libcalc.
void versionReady(const std::string& version);
};
post_text: |
**Rules for the impl class:**
- It's a normal C++ class. Any `public` method is exposed; `private` members and helpers are not.
- **Supported parameter/return types** (what the generator can translate):
| C++ type | LIDL contract type | A Qt consumer sees |
| --------------------------- | ------------------ | ------------------ |
| `void` | `void` | `void` |
| `bool` | `bool` | `bool` |
| `int64_t` | `int` | `qlonglong` |
| `uint64_t` | `uint` | `qulonglong` |
| `double` | `float64` | `double` |
| `std::string` | `tstr` | `QString` |
| `std::vector<std::string>` | `[tstr]` | `QStringList` |
| `std::vector<uint8_t>` | `bstr` | `QByteArray` |
| `LogosMap` / `LogosList` | `{tstr: any}` / `[any]` (from `<logos_json.h>`) | `QVariantMap` / `QVariantList` |
| `StdLogosResult` | `result` | `LogosResult` (from `<logos_result.h>`) — `{ success, value, error }` |
The **middle** column is the one your module publishes about itself —
it is what `lm` prints in Step 5, and what any other language's
binding of this contract sees. The right column is what a *C++/Qt*
caller of this module compiles against; a Rust or Nim caller gets
that language's spelling of the same middle column.
- Use `int64_t` for integers (not `int`) — that's the type the parser recognizes.
- **Document methods with `///`.** A doc comment (`///` or `/** … */`) directly above a method becomes its `description` in the module's introspection, surfaced by `lm`, `logoscore module-info`, and Basecamp. Plain `//` comments are ignored, so only intentional docs are exposed — you'll see this in action in Step 5.
- Events are declared in a `logos_events:` section. The token is recognized by the generator before preprocessing; under a normal compile it just expands to `public`.
- title: "`src/calc_module_impl.cpp` — Implementation"
text: |
Each method calls the corresponding C function and converts the result. No Qt types appear anywhere — you work in plain C++ and the generated glue handles the wire conversion.
file:
path: src/calc_module_impl.cpp
language: cpp
content: |
#include "calc_module_impl.h"
int64_t CalcModuleImpl::add(int64_t a, int64_t b)
{
return calc_add(static_cast<int>(a), static_cast<int>(b));
}
int64_t CalcModuleImpl::multiply(int64_t a, int64_t b)
{
return calc_multiply(static_cast<int>(a), static_cast<int>(b));
}
int64_t CalcModuleImpl::factorial(int64_t n)
{
return calc_factorial(static_cast<int>(n));
}
int64_t CalcModuleImpl::fibonacci(int64_t n)
{
return calc_fibonacci(static_cast<int>(n));
}
std::string CalcModuleImpl::libVersion()
{
return std::string(calc_version());
}
void CalcModuleImpl::libVersionNotify()
{
// Emit the event declared in `logos_events:`. When the module is
// loaded by a host, this reaches every subscriber. When the class
// is constructed outside a host (e.g. in unit tests), it is a
// safe no-op.
versionReady(std::string(calc_version()));
}
post_text: |
**The wrapping pattern** is always the same:
1. Call the C function (convert `int64_t` → `int` for libcalc's `int` API)
2. Convert the C result to a C++ type if needed (e.g., `const char*` → `std::string`)
3. Return it — the generated glue marshals it onto the wire
Notice what you **didn't** write: no `initLogos`, no `Q_INVOKABLE`, no `name()`/`version()` (read from `metadata.json`), no signal declaration. The generator produces all of it from the header.
# ── Step 4: Build the Module ────────────────────────────────────────────────
- title: "Build the Module"
step: true
steps:
- title: "Initialize the Git repo"
text: |
Nix flakes require a git repository.
Before staging files, create a `.gitignore` to exclude build artifacts:
file:
path: .gitignore
language: text
content: |
# Nix build output
result
result-*
# CMake build directory
build/
- text: "Then initialise the repo:"
run: "git init"
- run: "git add -A"
- run: "nix flake update"
- run: "git add flake.lock"
- title: "Build the plugin library"
text: |
Build just the plugin library (`.so` / `.dylib`):
run: "nix build '.#lib'"
post_text: |
> **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.
- title: "Build the full package"
text: |
Build everything (library + generated SDK headers). For a `universal` module this is also where `logos-cpp-generator --from-header` runs over `src/calc_module_impl.h` to produce the Qt plugin glue under `generated_code/` before CMake compiles it:
run: "nix build"
- title: "Inspect the output"
run: "ls -la result/lib/"
post_text: |
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.
- check_file: "result/lib/calc_module_plugin.{ext}"
# ── Step 5: Inspect the Module ──────────────────────────────────────────────
- title: "Inspect the Module"
step: true
text: |
Use the `lm` CLI tool (from `logos-module`) to inspect the compiled module binary.
steps:
- title: "Build the `lm` tool"
text: |
The `lm` CLI inspects compiled module binaries. Build it from the `logos-module` repo:
run: "nix build 'github:logos-co/logos-module{release}#lm' --out-link ./lm"
- title: "View metadata"
run: "./lm/bin/lm metadata result/lib/calc_module_plugin.{ext}"
code_block: |
# Linux
./lm/bin/lm metadata result/lib/calc_module_plugin.so
# macOS
./lm/bin/lm metadata result/lib/calc_module_plugin.dylib
expect_contains:
- "Name: calc_module"
- "Version: 1.0.0"
- "Type: core"
post_text: |
Output:
```
Plugin Metadata:
================
Name: calc_module
Version: 1.0.0
Description: Calculator module wrapping libcalc C library
Author:
Type: core
Dependencies: (none)
```
- title: "List methods"
run: "./lm/bin/lm methods result/lib/calc_module_plugin.{ext}"
code_block: |
# Linux
./lm/bin/lm methods result/lib/calc_module_plugin.so
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib
expect_contains:
- "int add(int a, int b)"
- "int multiply(int a, int b)"
- "int factorial(int n)"
- "int fibonacci(int n)"
- "tstr libVersion()"
- "Description: Adds two integers and returns the sum."
- "Defined as n * (n-1) * ... * 1, with 0! = 1."
- "Read straight from the linked native library, not metadata.json."
post_text: |
Output — each method you declared, with its doc comment as a
`Description`, plus the two identity methods (`name`, `version`) the
generator derives from `metadata.json` so every module answers them
without you writing them. A single-line comment renders inline; a
multi-line comment (`factorial`'s two `///` lines, `libVersion`'s
`/** ... */` block, and `libVersionNotify`'s two `///` lines) keeps
its line breaks:
```
Plugin Methods:
===============
int add(int a, int b)
Signature: add(int,int)
Invokable: yes
Description: Adds two integers and returns the sum.
int multiply(int a, int b)
Signature: multiply(int,int)
Invokable: yes
Description: Multiplies two integers and returns the product.
int factorial(int n)
Signature: factorial(int)
Invokable: yes
Description:
Computes the factorial n! of a non-negative integer.
Defined as n * (n-1) * ... * 1, with 0! = 1.
int fibonacci(int n)
Signature: fibonacci(int)
Invokable: yes
Description: Returns the nth Fibonacci number (0-indexed).
tstr libVersion()
Signature: libVersion()
Invokable: yes
Description:
Returns the version string of the wrapped libcalc C library.
Read straight from the linked native library, not metadata.json.
void libVersionNotify()
Signature: libVersionNotify()
Invokable: yes
Description:
Looks up the library version and emits it as a `versionReady`
event instead of returning it. Used by the QML tutorial (Part 2).
tstr name()
Signature: name()
Invokable: yes
Description: The module's name, as declared in its metadata.
tstr version()
Signature: version()
Invokable: yes
Description: The module's version, as declared in its metadata.
```
Three things to notice:
- **Signatures are in LIDL, not C++** (`int`, `tstr`) even though you wrote `int64_t` / `std::string`. `lm` reports what the module *publishes about itself*, and a module publishes its **contract** — so `int64_t add(int64_t, int64_t)` shows up as `add(int,int)`. That is the same vocabulary as the `.lidl` the build derived from your header, and it is the only vocabulary in which this question has one right answer: your module is Qt-free, and a reader in Rust or Nim asking the same module the same question gets the same words back. Note `int` here is LIDL's `int`, which is **64-bit** — each type in the contract maps to exactly one type per language, and integers are 64-bit throughout, so a value that fits your `int64_t` cannot be silently truncated on the way across.
- **Each `Description` is your doc comment**, carried through the module's method introspection. Plain `//` comments (like the type-mapping note in the header) are deliberately ignored, so only intentional docs surface; an undocumented method simply omits it.
- **Line breaks are preserved** — a single-line comment renders inline; a multi-line comment (`factorial`, `libVersion`, `libVersionNotify`) keeps its breaks. The same descriptions appear in `logoscore module-info` and Basecamp's Methods list.
- title: "JSON output"
text: "For scripting and CI, use `--json`:"
run: "./lm/bin/lm methods result/lib/calc_module_plugin.{ext} --json"
code_block: |
# Linux
./lm/bin/lm methods result/lib/calc_module_plugin.so --json
# macOS
./lm/bin/lm methods result/lib/calc_module_plugin.dylib --json
expect_contains:
- '"name": "add"'
- '"description": "Adds two integers and returns the sum."'
post_text: |
```json
[
{
"description": "Adds two integers and returns the sum.",
"isInvokable": true,
"name": "add",
"parameters": [
{ "name": "a", "type": "int" },
{ "name": "b", "type": "int" }
],
"returnType": "int",
"signature": "add(int,int)"
},
...
]
```
The `description` field is the method's doc comment. A multi-line
comment is carried verbatim with embedded `\n` (e.g. `factorial`:
`"Computes the factorial n! of a non-negative integer.\nDefined as
n * (n-1) * ... * 1, with 0! = 1."`). Methods without a doc comment
omit the field.
- title: "List events"
text: |
Events (your `logos_events:` block) are part of the module's API too, and
are introspectable the same way — `lm events` lists each event with its
signature and `///` description:
run: "./lm/bin/lm events result/lib/calc_module_plugin.{ext}"
code_block: |
# Linux
./lm/bin/lm events result/lib/calc_module_plugin.so
# macOS
./lm/bin/lm events result/lib/calc_module_plugin.dylib
expect_contains:
- "void versionReady(tstr version)"
- "Emitted by libVersionNotify() once the library version is known."
- "Carries the version string read from libcalc."
post_text: |
```
Plugin Events:
==============
void versionReady(tstr version)
Signature: versionReady(tstr)
Description:
Emitted by libVersionNotify() once the library version is known.
Carries the version string read from libcalc.
```
Events have no return type (they're fire-and-forget). Running `lm`
with no subcommand prints metadata, methods, **and** events together.
The same event docs appear in `logoscore module-info` and Basecamp's
Interface screen.
# ── Step 6: Test with logoscore ─────────────────────────────────────────────
- title: "Test with `logoscore`"
step: true
steps:
- title: "Build logoscore"
run: "nix build 'github:logos-co/logos-logoscore-cli{release}' --out-link ./logos"
- title: "Set up the modules directory"
text: |
`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:
run: "nix build '.#lgx'"
- run: "nix build 'github:logos-co/logos-package-manager{release}#cli' --out-link ./pm"
- run: "mkdir -p modules"
- run: "./pm/bin/lgpm --modules-dir ./modules install --file result/*.lgx"
post_text: |
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
```
- title: "Start the daemon and load the module"
text: "Start the daemon and load `calc_module`:"
run: "./logos/bin/logoscore -D -m ./modules &"
- run: "sleep 3"
- run: "./logos/bin/logoscore load-module calc_module"
- title: "Inspect methods and events"
text: "`module-info` lists each method **and event** with its signature and the doc-comment description you wrote — the same docs `lm` showed, here straight from the module's introspection:"
run: "./logos/bin/logoscore module-info calc_module"
expect_contains:
- "Adds two integers and returns the sum."
- "Computes the factorial n! of a non-negative integer."
- "Defined as n * (n-1) * ... * 1, with 0! = 1."
- "Read straight from the linked native library, not metadata.json."
- "Emitted by libVersionNotify() once the library version is known."
- "Carries the version string read from libcalc."
post_text: |
```
Name: calc_module
Version: v1.0.0
Status: loaded
PID: 48213
Uptime: 3s
Methods:
add(a: int, b: int) -> int
Adds two integers and returns the sum.
multiply(a: int, b: int) -> int
Multiplies two integers and returns the product.
factorial(n: int) -> int
Computes the factorial n! of a non-negative integer.
Defined as n * (n-1) * ... * 1, with 0! = 1.
fibonacci(n: int) -> int
Returns the nth Fibonacci number (0-indexed).
libVersion() -> tstr
Returns the version string of the wrapped libcalc C library.
Read straight from the linked native library, not metadata.json.
libVersionNotify() -> void
Looks up the library version and emits it as a `versionReady`
event instead of returning it. Used by the QML tutorial (Part 2).
name() -> tstr
The module's name, as declared in its metadata.
version() -> tstr
The module's version, as declared in its metadata.
Events:
versionReady(version: tstr)
Emitted by libVersionNotify() once the library version is known.
Carries the version string read from libcalc.
```
Methods and events both show their doc comments (multi-line ones keep
their line breaks). An undocumented method or event still appears, just
without the indented description.
- title: "Call methods"
text: "Now call them:"
run: "./logos/bin/logoscore call calc_module add 3 5"
expect_contains:
- '"result":8'
- run: "./logos/bin/logoscore call calc_module factorial 5"
expect_contains:
- '"result":120'
- run: "./logos/bin/logoscore call calc_module fibonacci 10"
expect_contains:
- '"result":55'
- run: "./logos/bin/logoscore call calc_module libVersion"
expect_contains:
- '"result":"1.0.0"'
- run: "./logos/bin/logoscore stop"
post_text: |
> For the full daemon/client workflow and other logoscore options, see the [Developer Guide -- Running with logoscore](logos-developer-guide.md#61-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` (the generated wrapper around your impl class)
4. `logos_host` calls `initLogos()` on the generated 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. The generated glue converts the args and invokes `CalcModuleImpl::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 7: Unit-test the Module ────────────────────────────────────────────
- title: "Unit-test the Module"
step: true
text: |
Because your module is a plain C++ class, you can unit-test it **directly** — no Qt, no running host, no IPC. The [Logos Test Framework](https://github.com/logos-co/logos-test-framework) adds two things on top of that: a tiny test runner (`LOGOS_TEST` / `LOGOS_ASSERT_*`) and **link-time mocking of your C library**, so each test can make `calc_add`, `calc_factorial`, … return whatever it wants and assert how your wrapper behaves.
You wire it up by pointing `mkLogosModule` at a `tests/` directory in `flake.nix`, then writing the test files. `nix build .#unit-tests` builds and runs them.
steps:
- title: "Enable tests in `flake.nix`"
text: |
Add a `tests` block to the `mkLogosModule` call. `mockCLibs` lists the external libraries to replace with link-time mocks (so tests don't need the real `libcalc`):
file:
path: flake.nix
language: nix
content: |
{
description = "Calculator module - wraps libcalc C library for Logos";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
tests = {
dir = ./tests;
mockCLibs = [ "calc" ];
};
};
}
- title: "`tests/CMakeLists.txt` — wire up the test binary"
text: |
The test harness configures and builds `tests/` as its own CMake project, so it needs a `tests/CMakeLists.txt`. It includes `LogosTest` (provided by the framework) and calls `logos_test()`, listing your impl source, the test sources, and the C-library mock:
file:
path: tests/CMakeLists.txt
language: cmake
content: |
cmake_minimum_required(VERSION 3.14)
project(CalcModuleTests LANGUAGES CXX)
include(LogosTest)
logos_test(
NAME calc_module_tests
MODULE_SOURCES
../src/calc_module_impl.cpp
mocks/calc_module_events_stub.cpp
TEST_SOURCES
main.cpp
test_calc.cpp
MOCK_C_SOURCES
mocks/mock_libcalc.cpp
)
post_text: |
- **`MODULE_SOURCES`** — your impl `.cpp` (compiled into the test binary, not the real plugin), plus the events stub explained below
- **`TEST_SOURCES`** — the runner entry point plus your `test_*.cpp` files
- **`MOCK_C_SOURCES`** — the link-time replacement for libcalc, so the real library is never linked
`logos_test()` automatically puts the repo root and `../src` on the include path, so `#include "calc_module_impl.h"` and `#include "lib/libcalc.h"` both resolve.
- title: "`tests/mocks/calc_module_events_stub.cpp` — stub the event method"
text: |
In a normal build, `logos-cpp-generator` emits `calc_module_events.cpp` containing the body of every `logos_events:` method (e.g. `versionReady`). The test harness runs the generator in a reduced mode that does **not** emit that file, so `libVersionNotify()` — which calls `versionReady(...)` — would fail to link. Provide a tiny no-op stub for unit tests:
file:
path: tests/mocks/calc_module_events_stub.cpp
language: cpp
content: |
// Stub bodies for the impl's `logos_events:` methods.
// In the real build the codegen generates calc_module_events.cpp with
// bodies that route through LogosModuleContext. The test build skips
// that codegen, so we provide no-op stubs to satisfy the linker.
#include "calc_module_impl.h"
void CalcModuleImpl::versionReady(const std::string&) {}
post_text: |
If you add more events to `logos_events:`, add a matching no-op line here. (A module with no events doesn't need this stub at all.)
- title: "Test runner entry point"
text: |
Create `tests/main.cpp` — one line pulls in the framework's `main()`:
file:
path: tests/main.cpp
language: cpp
content: |
#include <logos_test.h>
LOGOS_TEST_MAIN()
- title: "Mock the C library"
text: |
When building tests, the real `libcalc` is **not** linked. Instead you provide functions with the same signatures backed by the framework's mock store. Each one records that it was called and returns a value the test set up. Create `tests/mocks/mock_libcalc.cpp`:
file:
path: tests/mocks/mock_libcalc.cpp
language: cpp
content: |
// Link-time replacement for libcalc. Each function records the call
// and returns whatever the active test configured via mockCFunction().
#include <logos_clib_mock.h>
extern "C" {
#include "lib/libcalc.h"
}
extern "C" int calc_add(int a, int b) {
LOGOS_CMOCK_RECORD("calc_add");
return LOGOS_CMOCK_RETURN(int, "calc_add");
}
extern "C" int calc_multiply(int a, int b) {
LOGOS_CMOCK_RECORD("calc_multiply");
return LOGOS_CMOCK_RETURN(int, "calc_multiply");
}
extern "C" int calc_factorial(int n) {
LOGOS_CMOCK_RECORD("calc_factorial");
return LOGOS_CMOCK_RETURN(int, "calc_factorial");
}
extern "C" int calc_fibonacci(int n) {
LOGOS_CMOCK_RECORD("calc_fibonacci");
return LOGOS_CMOCK_RETURN(int, "calc_fibonacci");
}
extern "C" const char* calc_version(void) {
LOGOS_CMOCK_RECORD("calc_version");
return LOGOS_CMOCK_RETURN_STRING("calc_version");
}
post_text: |
`LOGOS_CMOCK_RECORD(name)` logs the call; `LOGOS_CMOCK_RETURN(type, name)` / `LOGOS_CMOCK_RETURN_STRING(name)` hand back the value the test set with `mockCFunction(...).returns(...)`.
- title: "Write the tests"
text: |
Create `tests/test_calc.cpp`. Each `LOGOS_TEST` constructs your impl directly, configures the C-function return values, calls a method, and asserts. `LogosTestContext` resets the mock store between tests:
file:
path: tests/test_calc.cpp
language: cpp
content: |
#include <logos_test.h>
#include "calc_module_impl.h"
LOGOS_TEST(add_forwards_to_calc_add) {
auto t = LogosTestContext("calc_module");
t.mockCFunction("calc_add").returns(8);
CalcModuleImpl calc;
LOGOS_ASSERT_EQ(calc.add(3, 5), 8);
LOGOS_ASSERT(t.cFunctionCalled("calc_add"));
}
LOGOS_TEST(multiply_forwards_to_calc_multiply) {
auto t = LogosTestContext("calc_module");
t.mockCFunction("calc_multiply").returns(42);
CalcModuleImpl calc;
LOGOS_ASSERT_EQ(calc.multiply(6, 7), 42);
LOGOS_ASSERT(t.cFunctionCalled("calc_multiply"));
}
LOGOS_TEST(factorial_returns_mocked_value) {
auto t = LogosTestContext("calc_module");
t.mockCFunction("calc_factorial").returns(120);
CalcModuleImpl calc;
LOGOS_ASSERT_EQ(calc.factorial(5), 120);
}
LOGOS_TEST(libVersion_converts_cstring_to_string) {
auto t = LogosTestContext("calc_module");
t.mockCFunction("calc_version").returns("1.0.0");
CalcModuleImpl calc;
LOGOS_ASSERT_EQ(calc.libVersion(), std::string("1.0.0"));
}
post_text: |
A few things worth calling out:
- The tests construct `CalcModuleImpl` like any class — no Qt, no host, no `initLogos`. That's the payoff of the pure-C++ pattern.
- `libVersionNotify()` is safe to call here too: its `versionReady(...)` event resolves to the no-op stub you added, so it won't crash and simply does nothing in the test process.
- `LOGOS_ASSERT_EQ`, `LOGOS_ASSERT`, `LOGOS_ASSERT_TRUE/FALSE`, `LOGOS_ASSERT_NE/GT/GE/LT` are all available from `<logos_test.h>`.
- title: "Run the tests"
text: |
Track the new files (nix only sees git-tracked files), then build and run:
run: "git add tests/ flake.nix"
- run: "nix build '.#unit-tests' -L"
code_block: |
nix build '.#unit-tests' -L
post_text: |
The build compiles your impl (`src/calc_module_impl.cpp`) against the mock library and the test sources, then runs every `LOGOS_TEST`. A passing run ends with a summary line; a failed assertion prints the file/line and fails the build.
> **From the workspace?** You can also run `ws test logos-calc-module` (after `ws sync-graph` picks up the new tests). See the workspace `CLAUDE.md`.
# ── Package for Distribution (prose only) ──────────────────────────────────
- title: "Package for Distribution (Optional)"
text: |
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{release}#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 (prose only) ──────────────────────────────────
- title: "Common Wrapping Patterns"
text: |
All of these are plain C++ — the impl class holds whatever state it needs as private members, and methods use std types. No Qt appears anywhere.
### 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 as a private member of your impl class:
```cpp
class DbModuleImpl : public LogosModuleContext
{
public:
bool open(const std::string& path) {
m_ctx = db_open(path.c_str());
return m_ctx != nullptr;
}
std::string get(const std::string& key) {
if (!m_ctx) return {};
char buf[4096];
int len = db_get(m_ctx, key.c_str(), buf, sizeof(buf));
if (len < 0) return {};
return std::string(buf, len);
}
~DbModuleImpl() { if (m_ctx) db_close(m_ctx); }
private:
db_ctx_t* m_ctx = nullptr; // private — not exposed over IPC
};
```
### Wrapping C callbacks → events
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 function as the callback, passing `this` as `user_data`, and forward into a declared event:
```cpp
class MyImpl : public LogosModuleContext
{
public:
void startListening() {
lib_set_callback(m_ctx, &MyImpl::c_callback, this);
}
logos_events:
void libEvent(int64_t code, const std::string& message);
private:
static void c_callback(int code, const char* msg, void* user_data) {
auto* self = static_cast<MyImpl*>(user_data);
self->libEvent(code, std::string(msg ? msg : ""));
}
void* m_ctx = nullptr;
};
```
Calling the declared event (`libEvent(...)`) routes the typed args to subscribers — you never touch Qt signals or `QVariantList` yourself.
### Wrapping C libraries that allocate strings
If the C library returns allocated strings that must be freed:
```cpp
std::string getData() {
char* c_str = lib_get_data(m_ctx); // Library allocates
std::string result = c_str ? c_str : "";
lib_free_string(c_str); // Library deallocates
return result;
}
```
### Type conversion reference (C ↔ impl class)
In the impl class you work entirely in std/C++ types — the generated glue handles the Qt/wire side. These are the conversions you write between the C library and your method signatures:
| C type | Impl type | C → impl | impl → C |
| ---------------------- | -------------------------- | ------------------------- | -------------------------- |
| `const char*` | `std::string` | `std::string(c_str)` | `s.c_str()` |
| `const char*` (binary) | `std::vector<uint8_t>` | `{data, data + len}` | `v.data()`, `v.size()` |
| `int` | `int64_t` | direct (widen) | `static_cast<int>(n)` |
| `bool` / `int` | `bool` | `result != 0` | direct |
| `void*` | (store as private member) | — | — |
> Use `int64_t` (not `int`) in the public signatures — that's the integer type the generator recognizes. Narrow to the C library's `int` inside the method, as the calc example does.
# ── Advanced: Wrapping a Library from a Flake Input (prose only) ──────────
- title: "Advanced: Wrapping a Library from a Flake Input"
text: |
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{release}";
# 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",
"interface": "universal",
"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 (prose only) ──────────────────────────────────────
- title: "Real-World Example: logos-libp2p-module"
text: |
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/*_impl.cpp**` — Wraps ~40 C functions (`libp2p_new`, `libp2p_start`, `libp2p_connect`, `libp2p_dial`, `libp2p_gossipsub_subscribe`, etc.) as plain public methods
- `**tests/**` — test suite that exercises every wrapped function with the Logos Test Framework
It follows the exact same pattern as this tutorial, just at a larger scale.
# ── Troubleshooting (prose only) ────────────────────────────────────────────
- title: "Troubleshooting"
text: |
### A method doesn't show up in `lm` / can't be called
The generator only exposes `public` methods on the impl class whose parameter and return types it recognizes. If a method is missing:
1. Make sure it's in the `public:` section (not `private:`).
2. Use supported types only — notably `int64_t` (not `int`), `std::string` (not `char*` or `QString`), `std::vector<std::string>`, `bool`, `double`, `LogosMap`/`LogosList`, `StdLogosResult`. See the type table in [Step 3](#step-3-configure-the-logos-module).
3. Keep the signature on as few lines as the parser expects — one declaration per method.
### Build error: unknown type / generator can't parse a method
The `--from-header` parser reads your `*_impl.h` as text. Pulling Qt types or unusual templates into a *public method signature* will confuse it. Keep Qt out of the impl header entirely, and move any helper that needs exotic types into the `private:` section or the `.cpp`.
### 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.
### Events never reach subscribers
If you emit an event (e.g. `versionReady(...)`) but a QML view or another module never receives it:
1. The event must be declared in a `logos_events:` section of the impl header, and your class must inherit `LogosModuleContext`.
2. The event only fires when the module is loaded by a host (logoscore / basecamp). Constructed standalone (unit tests), emission is a safe no-op — that's expected.
3. The subscriber must use the exact event name string, e.g. `logos.onModuleEvent("calc_module", "versionReady")`.
### 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`