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

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

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 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 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 .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:

[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.

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:

  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 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:

# 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_INVOKABLE method
  • The parameter typesint64_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 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:

{
  "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:

{
  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 librarycargo 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_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:

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-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:

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, or darwin-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_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:

// 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:

  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 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:

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:

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. For wrapping existing C libraries (where you write C++ directly), see Tutorial: Wrapping a C Library.