# 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.
- How `logoscore` discovers, loads, and calls your module
## Prerequisites
- **Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes globally:
```bash
# Add to ~/.config/nix/nix.conf:
experimental-features = nix-command flakes
```
- **A C compiler** (gcc or clang) for building the C library. Only needed if you're building the `.so`/`.dylib` yourself rather than using a pre-built library.
Before writing any C code, scaffold the Logos module project using the official template. This gives you the correct `flake.nix`, `module.yaml`, directory structure, and build configuration out of the box.
This generates the skeleton files (`flake.nix`, `module.yaml`, `CMakeLists.txt`, etc.) pre-configured for the logos-module-builder. You then customize them for your specific library.
> **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.
> **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/`.
If you used the template in Step 1.1, you already have the skeleton files. Now customize them for your library. A Logos module is a **Qt plugin** that wraps your C library functions as `Q_INVOKABLE` methods. You need five files:
| `name` | Module name — must be a valid C identifier (used in filenames, method calls) |
| `external_libraries[].name` | Library name **without the `lib` prefix** — the builder looks for `lib<name>.so` / `lib<name>.dylib` in the directory specified by `vendor_path`. So `name: calc` matches the file `libcalc.so` / `libcalc.dylib`. This follows the standard Unix library naming convention where `-lcalc` links against `libcalc`. |
| `external_libraries[].vendor_path` | Where to find the pre-built library. `"lib"` means the `lib/` directory in your project root |
| `cmake.extra_include_dirs` | Added to the CMake include path so your C++ code can `#include "lib/libcalc.h"` |
message(FATAL_ERROR "LogosModule.cmake not found")
endif()
# Define the module with its external library dependency
logos_module(
NAME calc_module
SOURCES
src/calc_module_interface.h
src/calc_module_plugin.h
src/calc_module_plugin.cpp
EXTERNAL_LIBS
calc
)
```
**How `EXTERNAL_LIBS calc` works:** The `logos_module()` CMake function searches `lib/` for `libcalc.so` (Linux) or `libcalc.dylib` (macOS), links it to your plugin, and sets up RPATH so the library is found at runtime.
### 2.4 `flake.nix` — Nix Build Config
```nix
{
description = "Calculator module - wraps libcalc C library for Logos";
- `Q_PLUGIN_METADATA(IID ... FILE "metadata.json")` — embeds the metadata into the binary
- `Q_INTERFACES(CalcModuleInterface PluginInterface)` — registers both interfaces with Qt's plugin system
- `initLogos` must be `Q_INVOKABLE` but **not** `override` — the base class `PluginInterface` does not declare it as virtual; the Logos host calls it reflectively via `QMetaObject::invokeMethod`
- `eventResponse` signal is required for event forwarding between modules
- `name()` must return the same string as the `name` field in `module.yaml` and `metadata.json`
- **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.
> **Quoting matters:** Use `'.#lib'` (with quotes) rather than bare `nix build .#lib`. Some shells (especially zsh) may interpret the `#` as a comment character, causing the command to silently build the wrong thing or fail.
`logoscore` expects modules in subdirectories, each with a `manifest.json`. Rather than copying files and writing the manifest manually, use the Nix bundler to create an LGX package and install it with the package manager:
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, use the `#portable` bundler:
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.
## 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";
3. Search for output files matching `output_pattern`
4. Copy the resulting `.so`/`.dylib` and headers to `lib/`
5. Proceed with the normal module build
### For Go libraries
If the external library is written in Go with C bindings (`cgo`):
```yaml
external_libraries:
- name: mygolib
flake_input: "github:example/mygolib"
go_build: true
output_pattern: "libmygolib.*"
```
Setting `go_build: true` enables the Go toolchain and sets `CGO_ENABLED=1`.
---
## Real-World Example: logos-libp2p-module
The [logos-libp2p-module](https://github.com/logos-co/logos-libp2p-module) is a production module that wraps the `nim-libp2p` library (compiled to a C shared library). Key files:
It follows the exact same pattern as this tutorial, just at a larger scale.
---
## Troubleshooting
### `initLogos` marked 'override', but does not override
```
error: 'void MyPlugin::initLogos(LogosAPI*)' marked 'override', but does not override
```
**Fix:** Remove the `override` keyword from `initLogos`. The base `PluginInterface` class does not declare it as virtual. The Logos host calls it reflectively via `QMetaObject::invokeMethod`. Declare it as:
```cpp
Q_INVOKABLE void initLogos(LogosAPI* api); // No override!
```
### Library not found at runtime
```
Cannot load library calc_module_plugin.so: libcalc.so: cannot open shared object file
**Fix:** Ensure `libcalc.so` / `libcalc.dylib` is in the same directory as the plugin. The build system sets RPATH to `$ORIGIN` (Linux) / `@loader_path` (macOS) so the plugin looks for libraries in its own directory.
### `initLogos` stores API pointer in wrong variable
If inter-module calls or API features silently fail, check that `initLogos` assigns to the **global** `logosAPI` variable (defined in the Logos SDK / liblogos), not to a class member like `m_logosAPI`:
```cpp
// CORRECT — uses the global variable from liblogos
void MyPlugin::initLogos(LogosAPI* api)
{
logosAPI = api;
}
// WRONG — stores in a local member, API calls won't work
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: