# Tutorial: Scaffolding Modules with logos-dev-boost
This tutorial shows how to use `logos-dev-boost` to scaffold Logos modules that wrap external C libraries. Instead of manually creating `metadata.json`, `CMakeLists.txt`, `flake.nix`, and C++ wrapper code, the scaffold tool generates everything from a directory containing your library's header and source/binary files.
**What you'll build:**
1. A `calc_wrap` module from the tutorial's `libcalc` library (simple case — C source only)
2. A `sqlcipher_wrap` module from the real [sqlcipher](https://github.com/sqlcipher/sqlcipher) library (realistic case — pre-built `.so` + C wrapper)
3. A `sqlcipher_app` full-app (module + UI) wrapping sqlcipher
**What you'll learn:**
- How `--lib-dir` parses a C header and generates a complete Logos module
- The difference between source-only and pre-built library wrapping
- How to write a thin C facade for libraries with complex APIs (opaque pointers, etc.)
- How to verify the generated module actually works end-to-end
## Prerequisites
- **Nix** with flakes enabled (see [Part 1 prerequisites](tutorial-wrapping-c-library.md#prerequisites))
- **logos-dev-boost** available via Nix — no local clone or npm build required. Verify it works:
```bash
nix run 'github:logos-co/logos-dev-boost' -- --help
```
All scaffold commands in this tutorial use `nix run 'github:logos-co/logos-dev-boost' -- ...`.
---
## Part 1: Wrapping libcalc (source-only library)
This is the simplest case — you have a C header and its `.c` source file, no pre-built binaries.
### 1.1 Prepare the library directory
Create a working directory and write a tiny C calculator library — a header plus its implementation:
```bash
mkdir calc-tutorial && cd calc-tutorial
mkdir -p lib
```
Create `lib/libcalc.h`:
```c
#ifndef LIBCALC_H
#define LIBCALC_H
#ifdef __cplusplus
extern "C" {
#endif
/** Add two integers. */
int calc_add(int a, int b);
/** Multiply two integers. */
int calc_multiply(int a, int b);
/** Compute factorial of n (n must be >= 0). Returns -1 on error. */
int calc_factorial(int n);
/** Compute the nth Fibonacci number (n must be >= 0). Returns -1 on error. */
int calc_fibonacci(int n);
/** Return the library version string. Caller must NOT free. */
const char* calc_version(void);
#ifdef __cplusplus
}
#endif
#endif /* LIBCALC_H */
```
Create `lib/libcalc.c`:
```c
#include "libcalc.h"
int calc_add(int a, int b)
{
return a + b;
}
int calc_multiply(int a, int b)
{
return a * b;
}
int calc_factorial(int n)
{
if (n < 0) return -1;
if (n <= 1) return 1;
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
int calc_fibonacci(int n)
{
if (n < 0) return -1;
if (n == 0) return 0;
if (n == 1) return 1;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int tmp = a + b;
a = b;
b = tmp;
}
return b;
}
const char* calc_version(void)
{
return "1.0.0";
}
```
Your directory should look like:
```
calc-tutorial/
└── lib/
├── libcalc.h # C header with function declarations
└── libcalc.c # C implementation
```
### 1.2 Scaffold the module
```bash
nix run 'github:logos-co/logos-dev-boost' -- init calc_wrap --type module --lib-dir ./lib
```
The scaffold:
1. **Parses `libcalc.h`** — extracts all 5 function declarations
2. **Copies `libcalc.h` and `libcalc.c`** into the project's `lib/` directory
3. **Generates `src/calc_wrap_impl.h`** — a C++ class wrapping each C function
4. **Generates `src/calc_wrap_impl.cpp`** — calls through to the C functions
5. **Generates `CMakeLists.txt`** — compiles the C source alongside the module, enables `LANGUAGES C CXX`
6. **Generates `metadata.json`** — declares the external library with `vendor_path`
7. **Generates `flake.nix`** — standard universal module build with `logos-cpp-generator`
8. **Generates unit tests** — one test per wrapped function
Output:
```
Created 10 files in logos-calc-wrap/
logos-calc-wrap/lib/libcalc.c
logos-calc-wrap/lib/libcalc.h
logos-calc-wrap/metadata.json
logos-calc-wrap/src/calc_wrap_impl.h
logos-calc-wrap/src/calc_wrap_impl.cpp
logos-calc-wrap/CMakeLists.txt
logos-calc-wrap/flake.nix
logos-calc-wrap/tests/main.cpp
logos-calc-wrap/tests/test_calc_wrap.cpp
logos-calc-wrap/tests/CMakeLists.txt
```
### 1.3 Inspect the generated wrapper
The generated `src/calc_wrap_impl.h`:
```cpp
#pragma once
#include <string>
#include <vector>
#include <cstdint>
extern "C" {
#include "lib/libcalc.h"
}
class CalcWrapImpl {
public:
CalcWrapImpl();
~CalcWrapImpl();
int64_t add(int64_t a, int64_t b);
int64_t multiply(int64_t a, int64_t b);
int64_t factorial(int64_t n);
int64_t fibonacci(int64_t n);
std::string version();
private:
// Private members
};
```
Notice the type mapping:
| C type | C++ type | Why |
|--------|----------|-----|
| `int` | `int64_t` | `logos-cpp-generator` only supports `int64_t`, not `int` |
${CMAKE_CURRENT_SOURCE_DIR}/lib # So #include "lib/libcalc.h" works
)
```
When the library is source-only (`.c` + `.h`, no `.so`/`.a`), the scaffold compiles the C source directly as part of the Qt plugin. The C functions are statically linked into the final `calc_wrap_plugin.so`.
---
## Part 2: Wrapping sqlcipher (pre-built library)
Real-world libraries like sqlcipher have complex APIs with opaque pointer types (`sqlite3*`), dozens of functions, and their own build systems. The scaffold can't auto-wrap these directly, but the workflow is straightforward:
1. Build the library to get `.so` + headers
2. Write a thin C facade that hides the complexity
├── sqlite3.h # 14,000-line public API header (362 functions)
└── sqlite3ext.h
```
You could point `--lib-dir` at this directly, but `sqlite3.h` has 362 functions — far too many to wrap as a module. And functions like `sqlite3_open(const char*, sqlite3**)` use opaque pointer types that the scaffold's C parser can't map to C++ automatically.
### 2.2 Write a thin C facade
Create a directory with just what the module needs — a focused header and a small wrapper that manages the `sqlite3*` handle internally:
/** Open an encrypted database. Returns 0 on success. */
int sc_open(const char* filename, const char* key);
/** Close the current database. */
void sc_close(void);
/** Execute a SQL statement (CREATE, INSERT, UPDATE, DELETE). Returns 0 on success. */
int sc_exec(const char* sql);
/** Query a single string value via SELECT. Caller must NOT free. */
const char* sc_query_string(const char* sql);
/** Query a single integer value via SELECT. */
int sc_query_int(const char* sql);
/** Re-key the database. Returns 0 on success. */
int sc_rekey(const char* new_key);
/** Get the last error message. */
const char* sc_errmsg(void);
/** Return the sqlcipher/sqlite version string. */
const char* sc_version(void);
/** Return the number of rows changed by the last statement. */
int sc_changes(void);
#ifdef __cplusplus
}
#endif
#endif /* LIBSQLCIPHER_H */
```
This header exposes 9 functions using only simple C types (`int`, `const char*`, `void`). All the `sqlite3*` pointer management is hidden inside the implementation.
Create `/tmp/sqlcipher-lib/libsqlcipher.c`:
```c
#include "libsqlcipher.h"
#include <string.h>
#include <stdlib.h>
/* Forward-declare sqlite3 types and functions.
These symbols resolve at link time against libsqlcipher.so. */
typedef struct sqlite3 sqlite3;
#define SQLITE_OK 0
extern int sqlite3_open(const char* filename, sqlite3** ppDb);
extern int sqlite3_close(sqlite3*);
extern int sqlite3_exec(sqlite3*, const char* sql,
int (*callback)(void*, int, char**, char**), void* arg, char** errmsg);
extern int sqlite3_key(sqlite3* db, const void* pKey, int nKey);
extern int sqlite3_rekey(sqlite3* db, const void* pKey, int nKey);
extern const char* sqlite3_errmsg(sqlite3*);
extern const char* sqlite3_libversion(void);
extern int sqlite3_changes(sqlite3*);
static sqlite3* g_db = NULL;
static char g_result_buf[4096];
int sc_open(const char* filename, const char* key) {
if (g_db) sc_close();
int rc = sqlite3_open(filename, &g_db);
if (rc != SQLITE_OK) return rc;
if (key && strlen(key) > 0) {
rc = sqlite3_key(g_db, key, (int)strlen(key));
}
return rc;
}
void sc_close(void) {
if (g_db) {
sqlite3_close(g_db);
g_db = NULL;
}
}
int sc_exec(const char* sql) {
if (!g_db) return -1;
return sqlite3_exec(g_db, sql, NULL, NULL, NULL);
}
static int query_string_cb(void* data, int ncols, char** vals, char** names) {
The key technique: forward-declare `sqlite3` types and functions instead of `#include <sqlite3.h>`. This makes the wrapper self-contained — it compiles with just a C compiler, and the `sqlite3_*` symbols resolve at link time against `libsqlcipher.so`.
Your lib directory should look like:
```
/tmp/sqlcipher-lib/
├── libsqlcipher.h # Your simplified C API (9 functions)
std::string val = impl.sc_query_string("SELECT val FROM data");
LOGOS_ASSERT_EQ(val, std::string("sensitive"));
impl.sc_close();
// Re-open with wrong key — query should fail
impl.sc_open("/tmp/test_sc_enc.db", "wrongkey");
int64_t bad_rc = impl.sc_exec("SELECT * FROM data");
LOGOS_ASSERT(bad_rc != 0);
impl.sc_close();
std::remove("/tmp/test_sc_enc.db");
}
```
### 2.6 Run the tests
```bash
nix build .#unit-tests -L
```
```
PASS version_returns_string 0ms
PASS open_close_succeeds 6ms
PASS create_table_and_insert 58ms
PASS query_string_returns_value 59ms
PASS query_int_returns_value 60ms
PASS errmsg_after_bad_sql 0ms
PASS encrypted_db_unreadable_without_key 177ms
── Results: 7 passed (360ms) ──────
```
The encryption test proves sqlcipher is actually working — writing encrypted data, reading it back with the correct key, and correctly rejecting access with a wrong key (`hmac check failed`).
---
## Part 3: Full-app scaffold (module + UI)
The `--type full-app` scaffold creates both a backend module and a QML UI app that calls it.
### 3.1 Scaffold
```bash
nix run 'github:logos-co/logos-dev-boost' -- init sqlcipher_app --type full-app --lib-dir /tmp/sqlcipher-lib
```
This creates two sub-projects:
```
logos-sqlcipher-app/
├── sqlcipher_app-module/ # Universal C++ module wrapping sqlcipher
| `double`, `float` | `double` | `double` | direct |
| `bool` | `bool` | `bool` | direct |
| `void` | — | `void` | direct |
> **Why `int64_t` instead of `int`?** The Logos `logos-cpp-generator` tool only supports `int64_t` for integer types. The scaffold inserts `static_cast<int>()` calls to narrow back to `int` when calling the original C functions.
## Tips for wrapping real-world libraries
1. **Write a C facade.** Most libraries use opaque pointers, complex structs, or callback-heavy APIs. Write a thin `.c`/`.h` that exposes only the operations you need using simple types (`int`, `const char*`, `void`).
2. **Forward-declare, don't include.** If your facade calls into the library's internal API, forward-declare the types and functions instead of `#include`-ing the library's headers. This keeps the facade self-contained and avoids header dependency chains.
3. **Pre-build the library.** Use `nix build nixpkgs#libname` or the library's own build system. Place the `.so`/`.a` in your lib directory alongside the facade.
4. **One header per library.** The scaffold picks one `.h` file to parse. If your library has multiple headers, create a single facade header that declares everything the module should expose.
5. **Test the facade first.** Before scaffolding, compile the facade standalone to verify it links correctly: