Wrap a C library as a Logos module
Expose functions from a C shared library through a Logos core module.
This document is accurate for Testnet v0.3.
This procedure walks you through wrapping a C shared library (.so on Linux, .dylib on macOS) as a Logos module. You write one plain C++ class, with no Qt code and no plugin boilerplate, and logos-module-builder generates the Qt plugin around it. By the end, you have a calc_module that builds, loads into logosctl and answers method calls, and unit tests that run it against a mock of the C library.
For a production example, see logos-libp2p-module, which wraps the nim-libp2p library compiled to a C shared library.
-
A supported OS:
- Linux x86_64 or aarch64
- macOS arm64 (Apple Silicon)
-
Git
-
A C compiler (
gccorclang) to build the C library. -
logosctlinstalled.- Install it by running
curl -fsSL https://raw.githubusercontent.com/logos-co/logos-docs/main/resources/scripts/install-logosctl.sh | sudo sh
- Install it by running
-
Nix with flakes enabled.
- Install from nixos.org, then enable flakes:
mkdir -p ~/.config/nixecho 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf -
Basic familiarity with C and C++.
What to expect
- You can wrap a C library in a Logos module by writing one plain C++ class, and let the build generate the Qt plugin around it.
- You can inspect the built module with
lmand call its methods from the command line withlogosctl. - You can unit-test the module against a link-time mock of the C library, without starting a daemon.
Step 1: Scaffold the module project
The with-external-lib template of logos-module-builder provides the flake.nix, metadata.json, CMakeLists.txt and directory layout that a module wrapping a library needs.
-
Create the project directory and initialise it from the template:
mkdir logos-calc-module && cd logos-calc-modulenix flake init -t github:logos-co/logos-module-builder/0.3.2#with-external-lib- The template already follows the pure C++ pattern that this procedure uses: one plain class in
src/external_lib_impl.handsrc/external_lib_impl.cpp, and"interface": "universal"inmetadata.json. - For a module that wraps no external library, use the default template instead:
nix flake init -t github:logos-co/logos-module-builder/0.3.2.
- The template already follows the pure C++ pattern that this procedure uses: one plain class in
-
Remove the template's example class. The following steps replace it with the sources of
calc_module:rm -f src/external_lib_impl.h src/external_lib_impl.cpp
Step 2: Write the C library
Create the C library that the module wraps, in the lib/ directory. To wrap an existing library instead, place its pre-built .so or .dylib and its header file in lib/ and continue at Step 3.
-
Create the
libdirectory:mkdir -p lib -
Create
lib/libcalc.h:#ifndef LIBCALC_H#define LIBCALC_H#ifdef __cplusplusextern "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 prevents C++ name mangling, so the module can find the symbols. -
Create
lib/libcalc.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";} -
Build the shared library:
cd lib# Linuxgcc -shared -fPIC -o libcalc.so libcalc.c# macOS# gcc -shared -fPIC -o libcalc.dylib libcalc.ccd .. -
Confirm that the symbols are exported:
# Linuxnm -D lib/libcalc.so | grep calc# macOS# nm -gU lib/libcalc.dylib | grep calcEach function is listed with
T(the text section). Addresses vary, and macOS prefixes each name with_:0000000000001139 T calc_add0000000000001179 T calc_factorial00000000000011f5 T calc_fibonacci0000000000001159 T calc_multiply0000000000001299 T calc_version
Step 3: Configure the Logos module
With the pure C++ (universal) pattern you write a single C++ class. metadata.json, CMakeLists.txt and flake.nix tell the build system the rest, and logos-cpp-generator generates the Qt plugin around the class. After this step, the project looks like this:
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)
-
Replace
metadata.jsonwith the following. Set"interface": "universal"and declare the library undernix.external_libraries:{"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": []}}}This file is the single source of truth for the module. It is embedded in the plugin, read by
logos-module-builderto configure the Nix build, used by CMake to link the external library, and used to write the LGX package manifest.Field What it does nameModule name. It must be a valid C identifier, because it is used in file names and method calls. mainThe name of the generated plugin, <name>_plugin. You don't write this file: the build producescalc_module_plugin.soorcalc_module_plugin.dylib.interface"universal"selects the pure C++ pattern. The build parsessrc/calc_module_impl.hinto the module's interface contract and generates the Qt plugin from it.nix.external_librariesThe C or C++ libraries the module wraps. nameis the CMake target andvendor_pathis the directory that holds the source or binary. The build links each library into the plugin.nix.cmake.extra_include_dirsDirectories added to the include path, so your C++ code can #include "lib/libcalc.h". -
Replace
CMakeLists.txtwith the following: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_moduleSOURCESsrc/calc_module_impl.hsrc/calc_module_impl.cppEXTERNAL_LIBScalc)- List only your own source files under
SOURCES.LogosModule.cmakecompiles the generated plugin code automatically. NAMEmust matchnameinmetadata.json, andEXTERNAL_LIBSmust matchnix.external_libraries[].name. IfNAMEdiffers, the build can succeed and the install phase still fail, because it looks for the plugin file named aftermetadata.json.logos_module()findslibcalc.soorlibcalc.dylibinlib/, links it to the plugin and sets the RPATH so the plugin finds it at runtime.- The
if/elseif/elseblock is boilerplate. Don't change it.
- List only your own source files under
-
Replace
flake.nixwith the following. It pinslogos-module-builderto the release this procedure was tested with:{description = "Calculator module - wraps libcalc C library for Logos";inputs = {logos-module-builder.url = "github:logos-co/logos-module-builder/0.3.2";};outputs = inputs@{ logos-module-builder, ... }:logos-module-builder.lib.mkLogosModule {src = ./.;configFile = ./metadata.json;flakeInputs = inputs;};}mkLogosModulefetches Qt, the Logos SDK and the code generators, and derives the build frommetadata.json.- To depend on another module, add it as a flake input named after the
namein that module'smetadata.json. For example, a dependency onwaku_moduleis the inputwaku_module.url = "github:logos-co/logos-waku-module". - To fetch and build the library from source instead of placing it in
lib/, see Wrapping a library from a flake input in the Logos tutorials.
-
Create
src/calc_module_impl.h. This is the only interface you write, and it is plain C++:#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`, `logosctl module show`, 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`, `logosctl// module show`, 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);};- Every
publicmethod is callable by other modules and bylogosctl.privatemembers are not exposed. - A doc comment directly above a method or event (
///, or a/** ... */block) becomes its description, whichlm,logosctl module showand Basecamp display. Plain//comments are ignored. - Events are declared in a
logos_events:section. InheritingLogosModuleContextlets the class emit them and call other modules. - Use
int64_tfor integers, notint. The code generator reads the header as text and recognises only the types in this table:
C++ type Contract type A Qt caller sees voidvoidvoidboolboolboolint64_tintqlonglonguint64_tuintqulonglongdoublefloat64doublestd::stringtstrQStringstd::vector<std::string>[tstr]QStringListstd::vector<uint8_t>bstrQByteArrayLogosMap/LogosList(from<logos_json.h>){tstr: any}/[any]QVariantMap/QVariantListStdLogosResultresultLogosResult(from<logos_result.h>)The contract type is what the module publishes about itself, in LIDL, its interface definition language. It is what
lmprints in Step 5, and what a caller in any language binds to. The last column is what a C++ caller built on Qt compiles against. - Every
-
Create
src/calc_module_impl.cpp. Each method calls the C function, converts the result to a C++ type if needed, and returns it:#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()));}You don't write
initLogos,Q_INVOKABLE,name(),version()orlidl(). The code generator produces them from the header andmetadata.json.
Step 4: Build the module
-
Create
.gitignore, so build output stays out of the repository:# Nix build outputresultresult-*# CMake build directorybuild/ -
Initialise the Git repository, stage the files and lock the flake inputs:
git initgit add -Anix flake updategit add flake.lock- Nix only sees files that Git tracks.
-
Build the plugin library:
nix build '.#lib'- The first build takes 5 to 15 minutes while Nix downloads Qt, the Logos SDK and their dependencies. Later builds use the Nix cache.
- Keep the quotes around
'.#lib'. Some shells, notably zsh, interpret#otherwise.
-
Build the full package:
nix build- This build derives the module's LIDL contract from
src/calc_module_impl.hand generates the Qt plugin from it before CMake compiles it.
- This build derives the module's LIDL contract from
-
Inspect the output:
ls -la result/lib/The plugin and the C library sit side by side, so the plugin finds the library at runtime through its RPATH:
# Linuxcalc_module_plugin.so # Your Logos module pluginlibcalc.so # The C library (copied alongside)# macOScalc_module_plugin.dyliblibcalc.dylib
Step 5: Inspect the module
Use the lm tool from logos-module to inspect the compiled plugin.
-
Build
lm:nix build 'github:logos-co/logos-module/0.3.0#lm' --out-link ./lm -
View the module's metadata:
# Linux./lm/bin/lm metadata result/lib/calc_module_plugin.so# macOS./lm/bin/lm metadata result/lib/calc_module_plugin.dylibPlugin Metadata:================Name: calc_moduleDisplay name: (unset — falls back to name)Version: 1.0.0Description: Calculator module wrapping libcalc C libraryAuthor:Type: coreProtocol: 0.9.0Dependencies: (none) -
List the module's methods:
# Linux./lm/bin/lm methods result/lib/calc_module_plugin.so# macOS./lm/bin/lm methods result/lib/calc_module_plugin.dylibPlugin Methods:===============int add(int a, int b)Signature: add(int,int)Invokable: yesDescription: Adds two integers and returns the sum....int factorial(int n)Signature: factorial(int)Invokable: yesDescription:Computes the factorial n! of a non-negative integer.Defined as n * (n-1) * ... * 1, with 0! = 1....tstr lidl()Signature: lidl()Invokable: yesDescription: The module's canonical LIDL interface document.- Signatures use the contract types (
int,tstr), not the C++ types you wrote. In LIDL,intis 64-bit, so a value that fits yourint64_tis never truncated on the way across. - Each
Descriptionis the method's doc comment, with its line breaks kept. name(),version()andlidl()are generated for every module. The last one returns the module's LIDL contract.- Add
--jsonfor output that scripts and CI can parse.
- Signatures use the contract types (
-
List the module's events:
# Linux./lm/bin/lm events result/lib/calc_module_plugin.so# macOS./lm/bin/lm events result/lib/calc_module_plugin.dylibPlugin 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.Running
lmwith no subcommand prints the metadata, methods and events together.
Step 6: Install the module with logosctl
logosctl installs modules from LGX packages, which carry the plugin, its libraries and a manifest.json. Build a portable package, start a daemon in a session of its own and install the package into it.
-
Build the portable LGX package:
nix build '.#lgx-portable' --out-link result-lgx-portable- Build
lgx-portable, notlgx. Thelgxoutput is a development build that keeps its libraries in the Nix store (variantdarwin-arm64-devon Apple Silicon, for example), andlogosctlrefuses to install it. See Troubleshooting.
- Build
-
Point
logosctlat a session for this project. Run this in every terminal you use for the rest of this procedure:export LOGOSCTL_CONFIG_DIR="$PWD/session"- A session holds its own modules, logs and data, so packages from other projects stay out of it. Without
LOGOSCTL_CONFIG_DIR,logosctluses~/.logosctl.
- A session holds its own modules, logs and data, so packages from other projects stay out of it. Without
-
Start the daemon, detached so this terminal stays free:
logosctl daemon start --detach- The command returns once the daemon accepts commands.
-
Install the package:
logosctl package install --file ./result-lgx-portable/*.lgx -y-yapplies the install without asking for confirmation.
The plugin, the C library and the manifest unpack into the session's
modules/directory:session/modules/calc_module/├── calc_module_plugin.dylib # (or .so on Linux)├── libcalc.dylib # (or .so on Linux)├── assets/lidl/ # The module's interface, in LIDL├── manifest.json # Package manifest├── variant # Platform variant identifier└── ... # Runtime libraries bundled by the portable build -
Confirm that the package is installed:
logosctl package lscalc_moduleis listed with the sourceuser, next to the packages embedded inlogosctl.
Step 7: Call the module from logosctl
-
Load the module:
logosctl module load calc_module -
Inspect the module's methods and events:
logosctl module show calc_moduleName: calc_moduleVersion: v1.0.0Status: loaded...Methods:add(a: int, b: int) -> intAdds two integers and returns the sum.multiply(a: int, b: int) -> intMultiplies two integers and returns the product.factorial(n: int) -> intComputes the factorial n! of a non-negative integer.Defined as n * (n-1) * ... * 1, with 0! = 1....Events:versionReady(version: tstr)Emitted by libVersionNotify() once the library version is known.Carries the version string read from libcalc.- The descriptions are the doc comments from
src/calc_module_impl.h, the same oneslmshowed.
- The descriptions are the doc comments from
-
Call its methods:
logosctl call calc_module add 3 5logosctl call calc_module factorial 5logosctl call calc_module fibonacci 10logosctl call calc_module libVersionEach call prints its result:
8120551.0.0- The daemon writes its log, including the output of every module, to
session/logs/daemon.log.
- The daemon writes its log, including the output of every module, to
-
Stop the daemon:
logosctl daemon stop
Step 8: Unit-test the module
Because the module is a plain C++ class, you can unit-test it directly, with no Qt and no daemon. The Logos Test Framework provides a test runner (LOGOS_TEST and LOGOS_ASSERT_*) and link-time mocks of your C library, so each test decides what the C functions return and checks how the wrapper behaves.
-
Enable tests in
flake.nixby adding atestsblock to themkLogosModulecall.mockCLibslists the external libraries to replace with link-time mocks, so the tests don't link the reallibcalc:{description = "Calculator module - wraps libcalc C library for Logos";inputs = {logos-module-builder.url = "github:logos-co/logos-module-builder/0.3.2";};outputs = inputs@{ logos-module-builder, ... }:logos-module-builder.lib.mkLogosModule {src = ./.;configFile = ./metadata.json;flakeInputs = inputs;tests = {dir = ./tests;mockCLibs = [ "calc" ];};};} -
Create
tests/CMakeLists.txt. The test build configurestests/as its own CMake project, which callslogos_test()from the framework:cmake_minimum_required(VERSION 3.14)project(CalcModuleTests LANGUAGES CXX)include(LogosTest)logos_test(NAME calc_module_testsMODULE_SOURCES../src/calc_module_impl.cppmocks/calc_module_events_stub.cppTEST_SOURCESmain.cpptest_calc.cppMOCK_C_SOURCESmocks/mock_libcalc.cpp)MODULE_SOURCES: your implementation, compiled into the test binary, and the events stub from the next action.TEST_SOURCES: the runner entry point and your test files.MOCK_C_SOURCES: the link-time replacement forlibcalc.logos_test()puts the repository root and../srcon the include path, so#include "calc_module_impl.h"and#include "lib/libcalc.h"both resolve.
-
Create
tests/mocks/calc_module_events_stub.cpp. In a normal build, the code generator writes the body of every method inlogos_events:. The test build skips that step, solibVersionNotify()needs a no-op body forversionReadyto link:// 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&) {}- Add a matching no-op line for each event you add to
logos_events:. A module without events doesn't need this file.
- Add a matching no-op line for each event you add to
-
Create
tests/main.cpp, which pulls in the framework'smain():#include <logos_test.h>LOGOS_TEST_MAIN() -
Create
tests/mocks/mock_libcalc.cpp. Each function has the signature of alibcalcfunction, records the call and returns the value the test set:// 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");} -
Create
tests/test_calc.cpp. Each test constructsCalcModuleImpldirectly, sets what the C functions return, calls a method and asserts on the result.LogosTestContextresets the mocks between tests:#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"));}<logos_test.h>also providesLOGOS_ASSERT_TRUE,LOGOS_ASSERT_FALSE,LOGOS_ASSERT_NE,LOGOS_ASSERT_GT,LOGOS_ASSERT_GEandLOGOS_ASSERT_LT.
-
Track the new files, then build and run the tests:
git add tests/ flake.nixnix build '.#unit-tests' -LThe build compiles the implementation against the mocks and runs every test. A passing run ends with a summary:
logos-calc_module-tests> PASS add_forwards_to_calc_add 0mslogos-calc_module-tests> PASS multiply_forwards_to_calc_multiply 0mslogos-calc_module-tests> PASS factorial_returns_mocked_value 0mslogos-calc_module-tests> PASS libVersion_converts_cstring_to_string 0mslogos-calc_module-tests>logos-calc_module-tests> ── Results: 4 passed (0ms) ──────- A failed assertion prints its file and line and fails the build.
Troubleshooting Logos module wrapping
A method doesn't appear in lm or can't be called
The code generator exposes only public methods whose parameter and return types it recognises. Check that the method is in the public: section, that it uses only the types in the table in Step 3 (notably int64_t, not int, and std::string, not char* or QString), and that each method is declared on its own.
The build fails with an unknown type or a method the generator can't parse
The generator reads src/calc_module_impl.h as text to derive the module's contract. Qt types or unusual templates in a public method signature confuse it. Keep Qt out of the header entirely, and move helpers that need other types into the private: section or the .cpp file.
The plugin fails with Cannot load library
Cannot load library calc_module_plugin.so: libcalc.so: cannot open shared object file
The C library must sit in the same directory as the plugin. The build sets the plugin's RPATH to $ORIGIN on Linux and @loader_path on macOS, so it looks for libraries in its own directory.
Events never reach subscribers
Check that the event is declared in a logos_events: section and that the class inherits LogosModuleContext. Events fire only when a host such as logosctl or Basecamp loads the module; constructed directly, as in unit tests, emitting is a no-op. Subscribers must use the exact event name, for example logos.onModuleEvent("calc_module", "versionReady").
logosctl cannot find the module
Check that:
- The module is installed in the current session:
logosctl package lslists it with the sourceuser. If you opened a new terminal, exportLOGOSCTL_CONFIG_DIRagain. - The install did not stop at the variant check.
logosctlinstalls only the portable variant for its own platform. A package from.#lgxfails withPackage does not contain variant for platform: … (package provides: …-dev), so build.#lgx-portable. - You load the module by the
nameinmetadata.json(calc_module), not by the package file name.
nix build .#lib does nothing or fails silently
Some shells, notably zsh, treat # specially. Put the flake reference in quotes: nix build '.#lib'.
The first build is slow
The first nix build downloads Qt 6, the Logos C++ SDK, the code generator and their dependencies. This is a one-time cost; later builds use the Nix cache.
The build fails with undefined symbols from the C library
Check that the .so or .dylib is in lib/ before you build, that the header has extern "C" guards, and that the symbols are exported: nm -D lib/libcalc.so | grep calc on Linux, or nm -gU lib/libcalc.dylib | grep calc on macOS.