Files
logos-tutorial/tutorial-rust-module.md
2026-04-03 14:03:14 -04:00

1069 lines
36 KiB
Markdown

# 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](#overview)
- [Prerequisites](#prerequisites)
- [How It Works (The Big Picture)](#how-it-works-the-big-picture)
- [Step 1: Create the Project Structure](#step-1-create-the-project-structure)
- [Step 2: Write the Rust Library](#step-2-write-the-rust-library)
- [Step 3: Write the C Header](#step-3-write-the-c-header)
- [Step 4: Configure the Module (metadata.json)](#step-4-configure-the-module-metadatajson)
- [Step 5: Configure the Nix Build (flake.nix)](#step-5-configure-the-nix-build-flakenix)
- [Step 6: Configure CMake (CMakeLists.txt)](#step-6-configure-cmake-cmakeliststxt)
- [Step 7: Build the Module](#step-7-build-the-module)
- [Step 8: Inspect and Test](#step-8-inspect-and-test)
- [Under the Hood: What Gets Generated](#under-the-hood-what-gets-generated)
- [Adapting This Pattern to Other Languages](#adapting-this-pattern-to-other-languages)
- [Reference: Type Mappings](#reference-type-mappings)
- [Reference: Naming Conventions](#reference-naming-conventions)
- [Troubleshooting](#troubleshooting)
---
## Overview
A traditional Logos module is a C++/Qt plugin. The developer writes:
- An interface header with `Q_INVOKABLE` virtual 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](https://github.com/logos-co/logos-workspace) checkout with Nix installed
- The `ws` CLI on your PATH: `export PATH="/path/to/workspace/scripts:$PATH"`
- For Rust modules: `cargo` and `rustc` (provided automatically by Nix during build)
- Basic familiarity with Logos modules (read the [Developer Guide](logos-developer-guide.md) 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:
```bash
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 `.h` or `.cpp` files. 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`:
```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 build` works in the Nix sandbox without network access. If your module needs external crates, you'll need to use `rustPlatform.buildRustPackage` in Nix instead of calling `cargo` directly in `preConfigure` — see [Adapting This Pattern to Other Languages](#adapting-this-pattern-to-other-languages).
### 2.2 Write the module logic
Create `rust-lib/src/lib.rs`:
```rust
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:**
1. **`#[no_mangle]`** — Prevents Rust from mangling the function name. Without this, the linker would see something like `_ZN10rust_calc3add17h8f3e4a5b2c1d0e9fE` instead of `rust_calc_add`.
2. **`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.
3. **Naming convention: `{prefix}_{method}`** — All functions share a common prefix (`rust_calc_`). The generator strips this prefix to derive the Logos method name. So `rust_calc_add` becomes method `add`, `rust_calc_factorial` becomes `factorial`, etc.
4. **C-compatible types only** — Parameters and return types must be representable in C. Use `i64` (not `isize`), `*const c_char` (not `&str`), `bool`, `f64`, etc. See [Reference: Type Mappings](#reference-type-mappings) for the full list.
5. **No panics across FFI** — A Rust panic that crosses the FFI boundary is undefined behavior. Use `checked_mul` / `checked_add` instead 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:
```toml
# 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:
```rust
#[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`:
```c
#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_INVOKABLE` method
- **The parameter types** — `int64_t` maps to `int` in the Qt interface, `const char*` maps to `QString`
- **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](https://github.com/mozilla/cbindgen) to auto-generate this header from your Rust source. For Go, `cgo` generates 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:
```json
{
"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 `_module` suffix and adds `_`, giving prefix `rust_calc_`. You can override this with a `"c_prefix"` field in the `nix` section, or with the `--prefix` CLI flag.
---
## Step 5: Configure the Nix Build (flake.nix)
Create `flake.nix`:
```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:**
1. **Build the Rust library**`cargo build --release --offline` compiles `lib.rs` into `rust-lib/target/release/librust_calc.a`. The `--offline` flag prevents Cargo from trying to access the network (blocked in the Nix sandbox). The `--release` flag enables optimizations.
2. **Stage build artifacts** — Copies the static library and C header to `lib/`, where CMake expects to find them.
3. **Auto-generate the Qt plugin** — Runs `logos-cpp-generator --from-c-header` which:
- Parses the C header to discover exported functions
- Reads `metadata.json` for 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 generates `logos_sdk.cpp` and 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
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) and `generated_code/` (where the generated plugin header lives).
- **find_library** locates the Rust static archive. The `logos_module()` macro's built-in `EXTERNAL_LIBS` option only finds shared libraries (`.so`/`.dylib`), so we manually link the static `.a` file.
- **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 for `NAME rust_calc_module`, the target is `rust_calc_module_module_plugin`.
---
## Step 7: Build the Module
### 7.1 Initialize Git
Nix flakes require all source files to be tracked by Git:
```bash
git init
git add -A
git commit -m "initial commit"
```
### 7.2 Build
From the workspace root:
```bash
ws build logos-rust-calc-module --local logos-cpp-sdk
```
> **Why `--local logos-cpp-sdk`?** The `--from-c-header` feature is in the local `logos-cpp-sdk` source. Once this feature is merged upstream, the `--local` flag 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:
```bash
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:
```bash
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):
```bash
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`, or `darwin-x86_64-dev`.
Then call methods:
```bash
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:
```bash
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](logos-developer-guide.md#51-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:
```cpp
// 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_OBJECT` enables Qt's meta-object system (signals, slots, runtime reflection)
- `Q_PLUGIN_METADATA` embeds `metadata.json` into the `.so` binary at compile time
- `Q_INTERFACES` declares which interfaces the plugin implements
- `Q_INVOKABLE` makes methods discoverable at runtime — this is how `logoscore` and `lm` find 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:
```cpp
// 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:
```json
"nix": {
"packages": {
"build": ["go"]
}
}
```
In `flake.nix`, change the `preConfigure` build step:
```nix
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`:
```json
"nix": {
"packages": {
"build": ["zig"]
}
}
```
In `flake.nix`:
```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:
```nix
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:
1. **Compile** your language to a static library (`.a` / `.lib`)
2. **Copy** the static library and C header to `lib/`
3. **Run** `logos-cpp-generator --from-c-header` to generate the Qt plugin
4. **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`:
1. Take the `name` field (e.g., `"rust_calc_module"`)
2. Strip `_module` suffix if present → `"rust_calc"`
3. 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](https://github.com/logos-co/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:
1. Your C header has function declarations ending with `;`
2. Function names start with the expected prefix (check with `--prefix` or the auto-derived prefix)
3. The header is being read during build (add `cat` commands in `preConfigure` to verify)
### `CMake Error: Cannot find source file: generated_code/..._plugin.h`
The generator didn't run or failed silently. Add error checking to `preConfigure`:
```nix
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:
1. `find_library` in CMakeLists.txt can find `librust_calc.a` in `lib/`
2. The `target_link_libraries` line references the correct target (`{name}_module_plugin`)
3. The `preConfigure` actually copies the `.a` file to `lib/`
### Linker errors: `undefined reference to pthread_create` (or `dlsym`, etc.)
The Rust standard library needs system libraries. Add to CMakeLists.txt:
```cmake
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_t` instead of `uint64_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](logos-developer-guide.md). For wrapping existing C libraries (where you write C++ directly), see [Tutorial: Wrapping a C Library](tutorial-wrapping-c-library.md).*