36 KiB
Tutorial: Building a Logos Module in Rust (or Any Language)
This tutorial walks through creating a Logos module written in pure Rust — with zero hand-written C++ code. Everything the Logos runtime needs (the Qt plugin, the interface, the dispatch layer) is auto-generated at build time from a plain C header.
While this tutorial uses Rust, the same pattern works for any language that can compile to a C-compatible static library: Go, Zig, Nim, C, or anything else with C FFI support. The key insight is that Logos modules are Qt plugins, but you never have to write Qt code yourself — the logos-cpp-generator tool does it for you.
Table of Contents
- Overview
- Prerequisites
- How It Works (The Big Picture)
- Step 1: Create the Project Structure
- Step 2: Write the Rust Library
- Step 3: Write the C Header
- Step 4: Configure the Module (metadata.json)
- Step 5: Configure the Nix Build (flake.nix)
- Step 6: Configure CMake (CMakeLists.txt)
- Step 7: Build the Module
- Step 8: Inspect and Test
- Under the Hood: What Gets Generated
- Adapting This Pattern to Other Languages
- Reference: Type Mappings
- Reference: Naming Conventions
- Troubleshooting
Overview
A traditional Logos module is a C++/Qt plugin. The developer writes:
- An interface header with
Q_INVOKABLEvirtual methods - A plugin class implementing that interface
- CMake + Nix build configuration
This is powerful but requires C++ knowledge, even when the actual logic is written in another language. The --from-c-header feature of logos-cpp-generator eliminates this requirement. You write your logic in any language, export C functions with a naming convention, and the entire Qt plugin layer is auto-generated.
What you write:
| File | Language | Purpose |
|---|---|---|
rust-lib/src/lib.rs |
Rust | Your module logic |
rust-lib/include/rust_calc.h |
C | Declares exported functions |
metadata.json |
JSON | Module name, version, build config |
flake.nix |
Nix | Build orchestration |
CMakeLists.txt |
CMake | Links generated plugin + your library |
What gets auto-generated at build time:
| File | Purpose |
|---|---|
rust_calc_module_plugin.h |
Qt interface + plugin class with Q_INVOKABLE methods |
rust_calc_module_plugin.cpp |
Implementation that calls your C functions |
rust_calc_module.lidl |
LIDL interface definition (for documentation) |
Zero hand-written C++ in your repository.
Prerequisites
- A working logos-workspace checkout with Nix installed
- The
wsCLI on your PATH:export PATH="/path/to/workspace/scripts:$PATH" - For Rust modules:
cargoandrustc(provided automatically by Nix during build) - Basic familiarity with Logos modules (read the Developer Guide first)
How It Works (The Big Picture)
The build pipeline has four stages:
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ Stage 1: Compile your language │
│ ───────────────────────────── │
│ Rust/Go/Zig/etc. source ──→ Static C library (.a) │
│ │
│ Stage 2: Auto-generate Qt plugin │
│ ──────────────────────────────── │
│ C header + metadata.json ──→ logos-cpp-generator --from-c-header │
│ ──→ Qt plugin .h/.cpp + LIDL file │
│ │
│ Stage 3: Compile the plugin │
│ ─────────────────────────── │
│ Generated .h/.cpp + static lib ──→ CMake + Qt MOC ──→ .so plugin │
│ │
│ Stage 4: Package │
│ ──────────────── │
│ Plugin .so ──→ Logos module (loadable by logoscore / Basecamp) │
│ │
└──────────────────────────────────────────────────────────────────────────┘
The critical link is the C header. It serves as a language-neutral contract between your code and the Logos plugin system. The logos-cpp-generator reads this header, detects functions matching a naming convention (prefix_methodname), and generates a complete Qt plugin that calls those functions.
Step 1: Create the Project Structure
Create a new directory for your module and set up the file structure:
mkdir -p logos-rust-calc-module/rust-lib/src
mkdir -p logos-rust-calc-module/rust-lib/include
cd logos-rust-calc-module
Your final directory structure will look like this:
logos-rust-calc-module/
├── metadata.json # Module configuration (~20 lines)
├── flake.nix # Nix build orchestration (~25 lines)
├── CMakeLists.txt # CMake build (~30 lines)
├── .gitignore # Ignore build artifacts
└── rust-lib/ # Your Rust code (this could be go-lib/, zig-lib/, etc.)
├── Cargo.toml # Rust package config
├── Cargo.lock # Dependency lock file
├── src/
│ └── lib.rs # Module logic in pure Rust
└── include/
└── rust_calc.h # C header declaring exported functions
No
src/directory with C++ files. Unlike a traditional Logos module, there are no hand-written.hor.cppfiles. The Qt plugin code is generated automatically during the build.
Create the .gitignore:
result
result-*
build/
.deps/
lib/
generated_code/
rust-lib/target/
Step 2: Write the Rust Library
2.1 Configure Cargo
Create rust-lib/Cargo.toml:
[package]
name = "rust_calc"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["staticlib"]
Key setting: crate-type = ["staticlib"] tells Cargo to produce a .a static archive (e.g., librust_calc.a) instead of a Rust-native .rlib. This static archive can be linked into the C++/Qt plugin by the standard system linker.
No external dependencies. This example has zero crate dependencies, which means
cargo buildworks in the Nix sandbox without network access. If your module needs external crates, you'll need to userustPlatform.buildRustPackagein Nix instead of callingcargodirectly inpreConfigure— see Adapting This Pattern to Other Languages.
2.2 Write the module logic
Create rust-lib/src/lib.rs:
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn rust_calc_add(a: i64, b: i64) -> i64 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_calc_subtract(a: i64, b: i64) -> i64 {
a - b
}
#[no_mangle]
pub extern "C" fn rust_calc_multiply(a: i64, b: i64) -> i64 {
a * b
}
#[no_mangle]
pub extern "C" fn rust_calc_divide(a: i64, b: i64) -> i64 {
if b == 0 {
-1
} else {
a / b
}
}
#[no_mangle]
pub extern "C" fn rust_calc_factorial(n: i64) -> i64 {
if n < 0 {
return -1;
}
if n <= 1 {
return 1;
}
let mut result: i64 = 1;
for i in 2..=n {
result = match result.checked_mul(i) {
Some(v) => v,
None => return -1, // overflow
};
}
result
}
#[no_mangle]
pub extern "C" fn rust_calc_fibonacci(n: i64) -> i64 {
if n < 0 {
return -1;
}
if n == 0 {
return 0;
}
if n == 1 {
return 1;
}
let (mut a, mut b) = (0i64, 1i64);
for _ in 2..=n {
let next = match a.checked_add(b) {
Some(v) => v,
None => return -1, // overflow
};
a = b;
b = next;
}
b
}
#[no_mangle]
pub extern "C" fn rust_calc_version() -> *const c_char {
b"1.0.0\0".as_ptr() as *const c_char
}
Critical rules for exported functions:
-
#[no_mangle]— Prevents Rust from mangling the function name. Without this, the linker would see something like_ZN10rust_calc3add17h8f3e4a5b2c1d0e9fEinstead ofrust_calc_add. -
pub extern "C"— Uses the C calling convention (argument passing, return values, stack layout). This is what makes the function callable from C/C++ code. -
Naming convention:
{prefix}_{method}— All functions share a common prefix (rust_calc_). The generator strips this prefix to derive the Logos method name. Sorust_calc_addbecomes methodadd,rust_calc_factorialbecomesfactorial, etc. -
C-compatible types only — Parameters and return types must be representable in C. Use
i64(notisize),*const c_char(not&str),bool,f64, etc. See Reference: Type Mappings for the full list. -
No panics across FFI — A Rust panic that crosses the FFI boundary is undefined behavior. Use
checked_mul/checked_addinstead of operators that might overflow-panic in debug builds.
2.3 Generate the lock file
Create rust-lib/Cargo.lock. For a crate with no dependencies, this is trivial:
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "rust_calc"
version = "1.0.0"
If you have a Rust toolchain installed locally, you can generate this automatically with cd rust-lib && cargo generate-lockfile.
2.4 (Optional) Add Rust tests
You can add standard Rust tests in the same lib.rs file:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(rust_calc_add(2, 3), 5);
assert_eq!(rust_calc_add(-1, 1), 0);
}
#[test]
fn test_factorial() {
assert_eq!(rust_calc_factorial(0), 1);
assert_eq!(rust_calc_factorial(5), 120);
assert_eq!(rust_calc_factorial(-1), -1);
}
#[test]
fn test_fibonacci() {
assert_eq!(rust_calc_fibonacci(10), 55);
}
#[test]
fn test_version() {
let v = unsafe { std::ffi::CStr::from_ptr(rust_calc_version()) };
assert_eq!(v.to_str().unwrap(), "1.0.0");
}
}
Run locally with cd rust-lib && cargo test (requires a local Rust toolchain).
Step 3: Write the C Header
Create rust-lib/include/rust_calc.h:
#ifndef RUST_CALC_H
#define RUST_CALC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int64_t rust_calc_add(int64_t a, int64_t b);
int64_t rust_calc_subtract(int64_t a, int64_t b);
int64_t rust_calc_multiply(int64_t a, int64_t b);
int64_t rust_calc_divide(int64_t a, int64_t b);
int64_t rust_calc_factorial(int64_t n);
int64_t rust_calc_fibonacci(int64_t n);
const char* rust_calc_version(void);
#ifdef __cplusplus
}
#endif
#endif /* RUST_CALC_H */
This is the most important file in the project. It serves as the bridge between your language and the Logos plugin system. The logos-cpp-generator --from-c-header tool reads this file to determine:
- What methods your module exposes — each function declaration becomes a
Q_INVOKABLEmethod - The parameter types —
int64_tmaps tointin the Qt interface,const char*maps toQString - The return types — same mappings apply
- The method names — derived by stripping the prefix (
rust_calc_) from the function name
Requirements for the C header:
| Requirement | Why |
|---|---|
#ifdef __cplusplus / extern "C" guards |
The header is included by generated C++ code |
One function declaration per line, ending with ; |
The parser processes line-by-line |
Functions use the naming convention {prefix}_{method}(...) |
The prefix is stripped to get the Logos method name |
(void) for functions with no parameters |
Distinguishes from missing parameter list |
| Only C-compatible types | Must be representable in both your language and C++ |
Automation option: For Rust, you can use cbindgen to auto-generate this header from your Rust source. For Go,
cgogenerates headers automatically. For this tutorial, we write it manually since it's only 7 functions.
Step 4: Configure the Module (metadata.json)
Create metadata.json in the project root:
{
"name": "rust_calc_module",
"version": "1.0.0",
"description": "Calculator module implemented in pure Rust",
"author": "Logos Core Team",
"type": "core",
"interface": "universal",
"category": "general",
"main": "rust_calc_module_plugin",
"dependencies": [],
"include": [],
"capabilities": [],
"nix": {
"external_libraries": [],
"packages": {
"build": ["cargo", "rustc"],
"runtime": []
},
"cmake": {
"find_packages": [],
"extra_include_dirs": ["lib"]
}
}
}
Key fields explained:
| Field | What it does |
|---|---|
name |
Module identifier — used as the prefix base. Must be a valid C identifier (letters, digits, underscores). |
main |
The Qt plugin class name. By convention: {name}_plugin. |
type |
Module type: "core" for backend logic, "ui" for Qt widgets. |
nix.packages.build |
Nix packages added to the build environment. ["cargo", "rustc"] ensures the Rust toolchain is available during preConfigure. For Go, you'd use ["go"]. For Zig, ["zig"]. |
nix.cmake.extra_include_dirs |
Directories added to the C++ include path. ["lib"] makes the C header (copied to lib/ during build) visible to the generated plugin code. |
Prefix derivation: The generator auto-derives the function prefix from the module name. For
"name": "rust_calc_module", it strips the_modulesuffix and adds_, giving prefixrust_calc_. You can override this with a"c_prefix"field in thenixsection, or with the--prefixCLI flag.
Step 5: Configure the Nix Build (flake.nix)
Create flake.nix:
{
description = "Calculator module implemented in pure Rust for Logos";
inputs = {
logos-module-builder.url = "github:logos-co/logos-module-builder";
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
preConfigure = ''
echo "=== Building Rust calculator library ==="
export HOME=$TMPDIR
export CARGO_HOME=$TMPDIR/cargo
mkdir -p $CARGO_HOME
pushd rust-lib
cargo build --release --offline 2>&1
popd
mkdir -p lib
cp rust-lib/target/release/librust_calc.a lib/
cp rust-lib/include/rust_calc.h lib/
echo "=== Auto-generating Qt plugin from C header ==="
logos-cpp-generator --from-c-header rust-lib/include/rust_calc.h \
--metadata metadata.json \
--backend qt \
--c-header-include rust_calc.h \
--output-dir ./generated_code
'';
};
}
What preConfigure does — step by step:
-
Build the Rust library —
cargo build --release --offlinecompileslib.rsintorust-lib/target/release/librust_calc.a. The--offlineflag prevents Cargo from trying to access the network (blocked in the Nix sandbox). The--releaseflag enables optimizations. -
Stage build artifacts — Copies the static library and C header to
lib/, where CMake expects to find them. -
Auto-generate the Qt plugin — Runs
logos-cpp-generator --from-c-headerwhich:- Parses the C header to discover exported functions
- Reads
metadata.jsonfor the module name, version, and description - Generates
rust_calc_module_plugin.h(Qt interface + plugin class) - Generates
rust_calc_module_plugin.cpp(implementation calling C functions) - Generates
rust_calc_module.lidl(LIDL interface definition)
Generator CLI flags:
| Flag | Purpose |
|---|---|
--from-c-header <path> |
Path to the C header file to parse |
--metadata <path> |
Path to metadata.json (provides module name, version) |
--backend qt |
Generate Qt/PluginInterface-style plugin code |
--c-header-include <name> |
The #include path used in generated code (just the filename) |
--output-dir <path> |
Where to write generated files |
--prefix <prefix> |
(Optional) Override the auto-derived function prefix |
Timing of
preConfigure: This hook runs after the module builder's own setup (which generateslogos_sdk.cppand other SDK files) but before CMake configuration. By the time CMake runs,generated_code/contains both the SDK files and your auto-generated plugin files.
Step 6: Configure CMake (CMakeLists.txt)
Create CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(RustCalcModulePlugin LANGUAGES CXX)
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
else()
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/metadata.json
${CMAKE_CURRENT_BINARY_DIR}/metadata.json COPYONLY)
logos_module(
NAME rust_calc_module
SOURCES
generated_code/rust_calc_module_plugin.h
generated_code/rust_calc_module_plugin.cpp
INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/lib
${CMAKE_CURRENT_SOURCE_DIR}/generated_code
)
# Link the Rust static library
find_library(LIBRUST_CALC
NAMES librust_calc.a rust_calc
PATHS ${CMAKE_CURRENT_SOURCE_DIR}/lib
NO_DEFAULT_PATH
)
if(LIBRUST_CALC)
target_link_libraries(rust_calc_module_module_plugin PRIVATE ${LIBRUST_CALC})
# Rust static libraries need pthread and dl on Linux
if(NOT APPLE)
target_link_libraries(rust_calc_module_module_plugin PRIVATE pthread dl)
endif()
else()
message(FATAL_ERROR "Rust calculator library (librust_calc.a) not found in lib/")
endif()
Key points:
- SOURCES reference the generated files in
generated_code/, not hand-written source files. - INCLUDE_DIRS adds both
lib/(where the C header lives) andgenerated_code/(where the generated plugin header lives). - find_library locates the Rust static archive. The
logos_module()macro's built-inEXTERNAL_LIBSoption only finds shared libraries (.so/.dylib), so we manually link the static.afile. - pthread and dl are required on Linux because the Rust standard library (statically linked into
librust_calc.a) depends on them for threading and dynamic loading. - Target name convention: The
logos_module()macro creates a target named{NAME}_module_plugin— so forNAME rust_calc_module, the target isrust_calc_module_module_plugin.
Step 7: Build the Module
7.1 Initialize Git
Nix flakes require all source files to be tracked by Git:
git init
git add -A
git commit -m "initial commit"
7.2 Build
From the workspace root:
ws build logos-rust-calc-module --local logos-cpp-sdk
Why
--local logos-cpp-sdk? The--from-c-headerfeature is in the locallogos-cpp-sdksource. Once this feature is merged upstream, the--localflag won't be needed.
The first build takes a few minutes (compiling Qt, the SDK, Rust, etc.). Subsequent builds are fast thanks to Nix caching.
Expected output:
Local overrides:
* logos-cpp-sdk → path:/workspace/repos/logos-cpp-sdk
Building logos-rust-calc-module...
OK logos-rust-calc-module
7.3 Standalone build (without workspace)
If your module is a standalone repo (not part of the workspace), build directly with Nix:
cd logos-rust-calc-module
nix build
Step 8: Inspect and Test
8.1 Inspect with lm
Use the lm tool to verify the module's metadata and methods:
lm result/lib/rust_calc_module_plugin.so
Expected output:
Plugin Metadata:
================
Name: rust_calc_module
Version: 1.0.0
Description: Calculator module implemented in pure Rust
Author: Logos Core Team
Type: core
Dependencies: (none)
Plugin Methods:
===============
void initLogos(LogosAPI* logosAPIInstance)
Signature: initLogos(LogosAPI*)
Invokable: yes
int add(int a, int b)
Signature: add(int,int)
Invokable: yes
int subtract(int a, int b)
Signature: subtract(int,int)
Invokable: yes
int multiply(int a, int b)
Signature: multiply(int,int)
Invokable: yes
int divide(int a, int b)
Signature: divide(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
Every extern "C" function from your C header appears as a Q_INVOKABLE method. Notice that rust_calc_version became libVersion — the generator automatically renames methods that conflict with PluginInterface reserved names (name, version, initLogos).
8.2 Test with logoscore
To test the module at runtime with logoscore, you need to create a module directory with a manifest.json (this is what logoscore uses for module discovery):
mkdir -p modules/rust_calc_module
cp result/lib/rust_calc_module_plugin.so modules/rust_calc_module/
# Create manifest.json for logoscore discovery
cat > modules/rust_calc_module/manifest.json << 'EOF'
{
"name": "rust_calc_module",
"version": "1.0.0",
"type": "core",
"category": "general",
"description": "Calculator module implemented in pure Rust",
"main": { "linux-x86_64-dev": "rust_calc_module_plugin.so" },
"manifestVersion": "0.1.0",
"dependencies": []
}
EOF
Platform key: Adjust the key in
"main"to match your platform:linux-x86_64-dev,linux-arm64-dev,darwin-arm64-dev, ordarwin-x86_64-dev.
Then call methods:
logoscore --modules-dir ./modules \
-l rust_calc_module \
-c "rust_calc_module.add(2,3)" \
--quit-on-finish
Expected output:
Method call successful. Result: 5
Try other methods:
logoscore --modules-dir ./modules -l rust_calc_module \
-c "rust_calc_module.factorial(10)" --quit-on-finish
# → Result: 3628800
logoscore --modules-dir ./modules -l rust_calc_module \
-c "rust_calc_module.fibonacci(10)" --quit-on-finish
# → Result: 55
logoscore --modules-dir ./modules -l rust_calc_module \
-c "rust_calc_module.libVersion()" --quit-on-finish
# → Result: 1.0.0
For more on testing with logoscore, see the Developer Guide -- Running with logoscore.
Under the Hood: What Gets Generated
When logos-cpp-generator --from-c-header runs, it produces three files. Understanding what they contain helps you debug issues and adapt the pattern to other languages.
Generated LIDL file (rust_calc_module.lidl)
This is a human-readable interface definition derived from your C header:
module rust_calc_module {
version "1.0.0"
description "Calculator module implemented in pure Rust"
category "general"
depends []
method add(a: int, b: int) -> int
method subtract(a: int, b: int) -> int
method multiply(a: int, b: int) -> int
method divide(a: int, b: int) -> int
method factorial(n: int) -> int
method fibonacci(n: int) -> int
method libVersion() -> tstr
}
The LIDL file is generated for documentation and is not used during the build. It provides a language-neutral description of your module's API. The type tstr means "text string" (maps to QString in Qt, std::string in C++, const char* in C).
Generated plugin header (rust_calc_module_plugin.h)
This is a complete Qt plugin header with two classes:
// AUTO-GENERATED by logos-cpp-generator --from-c-header -- do not edit
#pragma once
#include <QObject>
#include <QString>
#include <QVariant>
#include <QVariantList>
#include "interface.h"
#include "logos_api.h"
extern "C" {
#include "rust_calc.h" // ← Your C header
}
// Interface class — declares the module's API as pure virtual methods
class RustCalcModuleInterface : public PluginInterface {
public:
virtual ~RustCalcModuleInterface() = default;
Q_INVOKABLE virtual int add(int a, int b) = 0;
Q_INVOKABLE virtual int subtract(int a, int b) = 0;
Q_INVOKABLE virtual int multiply(int a, int b) = 0;
Q_INVOKABLE virtual int divide(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;
};
Q_DECLARE_INTERFACE(RustCalcModuleInterface, "org.logos.RustCalcModuleInterface")
// Plugin class — the actual Qt plugin that logoscore loads
class RustCalcModulePlugin : public QObject, public RustCalcModuleInterface {
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.logos.RustCalcModuleInterface" FILE "metadata.json")
Q_INTERFACES(RustCalcModuleInterface PluginInterface)
public:
explicit RustCalcModulePlugin(QObject* parent = nullptr);
~RustCalcModulePlugin() override;
QString name() const override { return QStringLiteral("rust_calc_module"); }
QString version() const override { return QStringLiteral("1.0.0"); }
Q_INVOKABLE void initLogos(LogosAPI* logosAPIInstance);
Q_INVOKABLE int add(int a, int b) override;
Q_INVOKABLE int subtract(int a, int b) override;
// ... (one method per C function)
signals:
void eventResponse(const QString& eventName, const QVariantList& args);
};
How the Qt plugin system works:
Q_OBJECTenables Qt's meta-object system (signals, slots, runtime reflection)Q_PLUGIN_METADATAembedsmetadata.jsoninto the.sobinary at compile timeQ_INTERFACESdeclares which interfaces the plugin implementsQ_INVOKABLEmakes methods discoverable at runtime — this is howlogoscoreandlmfind your methods- Qt's MOC (Meta-Object Compiler) processes this header during the build to generate the reflection tables
Generated plugin source (rust_calc_module_plugin.cpp)
This is the implementation — each method is a one-liner that calls your C function:
// AUTO-GENERATED by logos-cpp-generator --from-c-header -- do not edit
#include "rust_calc_module_plugin.h"
#include <QDebug>
RustCalcModulePlugin::RustCalcModulePlugin(QObject* parent)
: QObject(parent)
{
qDebug() << "RustCalcModulePlugin: created";
}
// ...
int RustCalcModulePlugin::add(int a, int b)
{
return static_cast<int>(rust_calc_add(static_cast<int64_t>(a), static_cast<int64_t>(b)));
}
int RustCalcModulePlugin::factorial(int n)
{
return static_cast<int>(rust_calc_factorial(static_cast<int64_t>(n)));
}
QString RustCalcModulePlugin::libVersion()
{
return QString::fromUtf8(rust_calc_version());
}
Type conversions at the boundary:
The generated code handles type conversion between Qt types (used by the plugin interface) and C types (used by your functions):
| Direction | Qt type | C type | Conversion |
|---|---|---|---|
| Parameter (Qt → C) | int |
int64_t |
static_cast<int64_t>(param) |
| Parameter (Qt → C) | QString |
const char* |
param.toUtf8().constData() |
| Return (C → Qt) | int64_t |
int |
static_cast<int>(result) |
| Return (C → Qt) | const char* |
QString |
QString::fromUtf8(result) |
| Pass-through | double |
double |
No conversion needed |
| Pass-through | bool |
bool |
No conversion needed |
Adapting This Pattern to Other Languages
The Rust-specific parts of this tutorial are limited to Step 2 (writing the library) and the cargo build line in Step 5 (the preConfigure hook). Everything else — the C header, metadata.json, CMakeLists.txt, and the generator — is language-agnostic.
Go module
go-lib/
├── main.go # Module logic with //export directives
├── go.mod
└── include/
└── my_module.h # Generated by cgo or written manually
In metadata.json, change build packages:
"nix": {
"packages": {
"build": ["go"]
}
}
In flake.nix, change the preConfigure build step:
preConfigure = ''
export HOME=$TMPDIR
export GOPATH=$TMPDIR/go
export GOCACHE=$TMPDIR/go-cache
mkdir -p $GOPATH $GOCACHE
cd go-lib
CGO_ENABLED=1 go build -buildmode=c-archive -o libmy_module.a .
cd ..
mkdir -p lib
cp go-lib/libmy_module.a lib/
cp go-lib/include/my_module.h lib/
logos-cpp-generator --from-c-header go-lib/include/my_module.h \
--metadata metadata.json --backend qt \
--c-header-include my_module.h --output-dir ./generated_code
'';
Zig module
zig-lib/
├── src/
│ └── lib.zig # Module logic with export functions
├── build.zig
└── include/
└── my_module.h # Written manually
In metadata.json:
"nix": {
"packages": {
"build": ["zig"]
}
}
In flake.nix:
preConfigure = ''
cd zig-lib
zig build -Doptimize=ReleaseFast
cd ..
mkdir -p lib
cp zig-lib/zig-out/lib/libmy_module.a lib/
cp zig-lib/include/my_module.h lib/
logos-cpp-generator --from-c-header zig-lib/include/my_module.h \
--metadata metadata.json --backend qt \
--c-header-include my_module.h --output-dir ./generated_code
'';
C module (simplest case)
For a plain C library, you don't even need a separate build step — just compile directly:
preConfigure = ''
mkdir -p lib
gcc -c -O2 -fPIC my_lib.c -o lib/libmy_module.o
ar rcs lib/libmy_module.a lib/libmy_module.o
cp my_lib.h lib/
logos-cpp-generator --from-c-header my_lib.h \
--metadata metadata.json --backend qt \
--c-header-include my_lib.h --output-dir ./generated_code
'';
The universal recipe
Regardless of language, the steps are always:
- Compile your language to a static library (
.a/.lib) - Copy the static library and C header to
lib/ - Run
logos-cpp-generator --from-c-headerto generate the Qt plugin - CMake links the generated plugin code with your static library
Reference: Type Mappings
C header → LIDL → Qt plugin
| C type | LIDL type | Qt type | Notes |
|---|---|---|---|
int64_t |
int |
int |
64-bit in C, 32-bit in Qt interface |
int32_t, int |
int |
int |
|
uint64_t |
uint |
int |
Unsigned in C, signed in Qt |
uint32_t, unsigned int |
uint |
int |
|
double, float |
float64 |
double |
|
bool, _Bool |
bool |
bool |
|
const char*, char* |
tstr |
QString |
Converted via QString::fromUtf8 |
void |
void |
void |
|
| Anything else | any |
QVariant |
Fallback — avoid if possible |
Rust → C type recommendations
| Rust type | C type to use | LIDL type |
|---|---|---|
i64 |
int64_t |
int |
i32 |
int32_t |
int |
u64 |
uint64_t |
uint |
f64 |
double |
float64 |
bool |
bool |
bool |
*const c_char |
const char* |
tstr |
() (unit) |
void |
void |
Reference: Naming Conventions
Function prefix
The generator strips the function prefix to derive method names:
| C function | Prefix | Logos method |
|---|---|---|
rust_calc_add |
rust_calc_ |
add |
rust_calc_factorial |
rust_calc_ |
factorial |
mymod_do_thing |
mymod_ |
do_thing |
Prefix auto-derivation
If you don't specify --prefix, the generator derives it from the module name in metadata.json:
- Take the
namefield (e.g.,"rust_calc_module") - Strip
_modulesuffix if present →"rust_calc" - Append
_→"rust_calc_"
Override with --prefix my_custom_prefix_ or by adding "c_prefix": "my_prefix_" to the nix section of metadata.json.
Reserved names
These method names conflict with PluginInterface built-in methods and are automatically renamed:
| C function | Would-be method | Actual method |
|---|---|---|
prefix_name |
name (reserved) |
libName |
prefix_version |
version (reserved) |
libVersion |
prefix_initLogos |
initLogos (reserved) |
libInitLogos |
The generated implementation correctly maps back to the original C function name when calling it.
Functions that are skipped
Functions in the C header that don't start with the prefix are silently ignored. This means you can have internal helper functions in the same header without them appearing as module methods.
Troubleshooting
cargo build fails with network errors
The Nix sandbox blocks network access. If your Rust project has external crate dependencies, cargo build --offline will fail because it can't download them.
Solution: Use rustPlatform.buildRustPackage in a separate Nix derivation that pre-fetches crates, then pass the built library to the module via preConfigure. See the logos-accounts-module for an example of this pattern with Go.
No methods appear in lm output
The generator didn't find any functions matching your prefix. Check:
- Your C header has function declarations ending with
; - Function names start with the expected prefix (check with
--prefixor the auto-derived prefix) - The header is being read during build (add
catcommands inpreConfigureto verify)
CMake Error: Cannot find source file: generated_code/..._plugin.h
The generator didn't run or failed silently. Add error checking to preConfigure:
preConfigure = ''
logos-cpp-generator --from-c-header ... || { echo "Generator failed!"; exit 1; }
test -f ./generated_code/my_module_plugin.h || { echo "Plugin header not generated!"; exit 1; }
'';
Linker errors: undefined reference to rust_calc_add
The Rust static library wasn't linked. Check:
find_libraryin CMakeLists.txt can findlibrust_calc.ainlib/- The
target_link_librariesline references the correct target ({name}_module_plugin) - The
preConfigureactually copies the.afile tolib/
Linker errors: undefined reference to pthread_create (or dlsym, etc.)
The Rust standard library needs system libraries. Add to CMakeLists.txt:
if(NOT APPLE)
target_link_libraries(my_module_module_plugin PRIVATE pthread dl)
endif()
Method returns wrong type / crashes
Check the type mapping between your language and the C header. Common mistakes:
- Using
size_tinstead ofuint64_t(platform-dependent size) - Returning a stack-allocated string pointer (dangling pointer after function returns)
- Integer overflow in Rust panicking across FFI boundary (use
checked_*operations)
For more on the Logos module system, see the Developer Guide. For wrapping existing C libraries (where you write C++ directly), see Tutorial: Wrapping a C Library.