Files
logos-tutorial/tutorial-dev-boost.md

800 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-05-18 15:43:59 -04:00
# 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` |
| `const char*` (return) | `std::string` | Safe C++ string ownership |
| `const char*` (param) | `const std::string&` | Automatic `.c_str()` conversion |
The generated `src/calc_wrap_impl.cpp` calls through to the C functions with the necessary casts:
```cpp
int64_t CalcWrapImpl::add(int64_t a, int64_t b) {
return static_cast<int64_t>(::calc_add(static_cast<int>(a), static_cast<int>(b)));
}
std::string CalcWrapImpl::version() {
return std::string(::calc_version());
}
```
The `::` prefix ensures the C function is called (not a same-named method on the class).
### 1.4 Build and test
```bash
cd logos-calc-wrap
git init && git add -A
nix build
```
Install the `lm` module inspector (one-time):
```bash
nix build 'github:logos-co/logos-module#lm' --out-link ./lm-tool
```
Verify the plugin:
```bash
./lm-tool/bin/lm ./result/lib/calc_wrap_plugin.so
```
Expected output:
```
Plugin Metadata:
================
Name: calc_wrap
Version: 1.0.0
Type: core
Plugin Methods:
===============
int add(int a, int b)
int multiply(int a, int b)
int factorial(int n)
int fibonacci(int n)
QString version()
```
Run the unit tests:
```bash
nix build .#unit-tests -L
```
```
PASS add_works 0ms
PASS multiply_works 0ms
PASS factorial_works 0ms
PASS fibonacci_works 0ms
PASS version_works 0ms
── Results: 5 passed (0ms) ──────
```
### 1.5 How the generated CMakeLists works
```cmake
cmake_minimum_required(VERSION 3.14)
project(LogosCalcWrapPlugin LANGUAGES C CXX) # C enabled for libcalc.c
logos_module(
NAME calc_wrap
SOURCES
src/calc_wrap_impl.h
src/calc_wrap_impl.cpp
generated_code/calc_wrap_qt_glue.h
generated_code/calc_wrap_dispatch.cpp
lib/libcalc.c # C source compiled directly
INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/generated_code
${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
3. Point `--lib-dir` at the facade
### 2.1 Build sqlcipher
Use nix to build sqlcipher from nixpkgs:
```bash
nix build nixpkgs#sqlcipher -o /tmp/sqlcipher-built
```
This produces:
```
/tmp/sqlcipher-built/
├── lib/
│ ├── libsqlcipher.so # The shared library
│ ├── libsqlcipher.a # Static archive
│ └── ...
└── include/sqlcipher/
├── 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:
```bash
mkdir -p /tmp/sqlcipher-lib
cp /tmp/sqlcipher-built/lib/libsqlcipher.so /tmp/sqlcipher-lib/
cp /tmp/sqlcipher-built/lib/libsqlcipher.a /tmp/sqlcipher-lib/
```
Create `/tmp/sqlcipher-lib/libsqlcipher.h`:
```c
#ifndef LIBSQLCIPHER_H
#define LIBSQLCIPHER_H
#ifdef __cplusplus
extern "C" {
#endif
/* Thin wrapper over sqlcipher's sqlite3 API.
Manages a single database 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) {
(void)data; (void)names;
if (ncols > 0 && vals[0]) {
strncpy(g_result_buf, vals[0], sizeof(g_result_buf) - 1);
g_result_buf[sizeof(g_result_buf) - 1] = '\0';
}
return 0;
}
const char* sc_query_string(const char* sql) {
if (!g_db) return "";
g_result_buf[0] = '\0';
sqlite3_exec(g_db, sql, query_string_cb, NULL, NULL);
return g_result_buf;
}
static int query_int_val = 0;
static int query_int_cb(void* data, int ncols, char** vals, char** names) {
(void)data; (void)names;
if (ncols > 0 && vals[0]) {
query_int_val = atoi(vals[0]);
}
return 0;
}
int sc_query_int(const char* sql) {
if (!g_db) return -1;
query_int_val = 0;
sqlite3_exec(g_db, sql, query_int_cb, NULL, NULL);
return query_int_val;
}
int sc_rekey(const char* new_key) {
if (!g_db || !new_key) return -1;
return sqlite3_rekey(g_db, new_key, (int)strlen(new_key));
}
const char* sc_errmsg(void) {
if (!g_db) return "no database open";
return sqlite3_errmsg(g_db);
}
const char* sc_version(void) {
return sqlite3_libversion();
}
int sc_changes(void) {
if (!g_db) return 0;
return sqlite3_changes(g_db);
}
```
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)
├── libsqlcipher.c # Wrapper implementation (forward-declares sqlite3)
├── libsqlcipher.so # Pre-built sqlcipher (from nix build)
└── libsqlcipher.a # Pre-built sqlcipher static archive
```
### 2.3 Scaffold the module
```bash
nix run 'github:logos-co/logos-dev-boost' -- init sqlcipher_wrap --type module --lib-dir /tmp/sqlcipher-lib
```
Output:
```
Created 12 files in logos-sqlcipher-wrap/
logos-sqlcipher-wrap/lib/libsqlcipher.a
logos-sqlcipher-wrap/lib/libsqlcipher.c
logos-sqlcipher-wrap/lib/libsqlcipher.h
logos-sqlcipher-wrap/lib/libsqlcipher.so
logos-sqlcipher-wrap/metadata.json
logos-sqlcipher-wrap/src/sqlcipher_wrap_impl.h
logos-sqlcipher-wrap/src/sqlcipher_wrap_impl.cpp
logos-sqlcipher-wrap/CMakeLists.txt
logos-sqlcipher-wrap/flake.nix
logos-sqlcipher-wrap/tests/main.cpp
logos-sqlcipher-wrap/tests/test_sqlcipher_wrap.cpp
logos-sqlcipher-wrap/tests/CMakeLists.txt
```
When the scaffold detects both `.c` source and `.so`/`.a` binaries, the generated CMakeLists does both:
```cmake
logos_module(
NAME sqlcipher_wrap
SOURCES
...
lib/libsqlcipher.c # Compile the wrapper
INCLUDE_DIRS
...
${CMAKE_CURRENT_SOURCE_DIR}/lib
EXTERNAL_LIBS
sqlcipher # Link against libsqlcipher.so
)
```
The wrapper `.c` is compiled as part of the plugin, and `libsqlcipher.so` is linked in to provide the `sqlite3_*` symbols.
### 2.4 Build
```bash
cd logos-sqlcipher-wrap
git init && git add -A
nix build
```
Verify:
```bash
./lm-tool/bin/lm ./result/lib/sqlcipher_wrap_plugin.so
```
```
Plugin Metadata:
================
Name: sqlcipher_wrap
Version: 1.0.0
Type: core
Plugin Methods:
===============
int sc_open(QString filename, QString key)
void sc_close()
int sc_exec(QString sql)
QString sc_query_string(QString sql)
int sc_query_int(QString sql)
int sc_rekey(QString new_key)
QString sc_errmsg()
QString sc_version()
int sc_changes()
```
All 9 methods exposed with correct Qt types.
### 2.5 Write real integration tests
The auto-generated tests are smoke tests (call each method once). Replace `tests/test_sqlcipher_wrap.cpp` with real database operations:
```cpp
#include <logos_test.h>
#include "../src/sqlcipher_wrap_impl.h"
#include <cstdio>
LOGOS_TEST(version_returns_string) {
SqlcipherWrapImpl impl;
std::string ver = impl.sc_version();
LOGOS_ASSERT(ver.size() > 0);
}
LOGOS_TEST(open_close_succeeds) {
SqlcipherWrapImpl impl;
int64_t rc = impl.sc_open("/tmp/test_sc_open.db", "secret");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
impl.sc_close();
std::remove("/tmp/test_sc_open.db");
}
LOGOS_TEST(create_table_and_insert) {
SqlcipherWrapImpl impl;
int64_t rc = impl.sc_open("/tmp/test_sc_crud.db", "mykey");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
rc = impl.sc_exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
rc = impl.sc_exec("INSERT INTO users (name, age) VALUES ('Alice', 30)");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
LOGOS_ASSERT_EQ(impl.sc_changes(), (int64_t)1);
rc = impl.sc_exec("INSERT INTO users (name, age) VALUES ('Bob', 25)");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
impl.sc_close();
std::remove("/tmp/test_sc_crud.db");
}
LOGOS_TEST(query_string_returns_value) {
SqlcipherWrapImpl impl;
impl.sc_open("/tmp/test_sc_qstr.db", "pass");
impl.sc_exec("CREATE TABLE kv (key TEXT, val TEXT)");
impl.sc_exec("INSERT INTO kv VALUES ('greeting', 'hello world')");
std::string val = impl.sc_query_string("SELECT val FROM kv WHERE key='greeting'");
LOGOS_ASSERT_EQ(val, std::string("hello world"));
impl.sc_close();
std::remove("/tmp/test_sc_qstr.db");
}
LOGOS_TEST(query_int_returns_value) {
SqlcipherWrapImpl impl;
impl.sc_open("/tmp/test_sc_qint.db", "pass");
impl.sc_exec("CREATE TABLE nums (n INTEGER)");
impl.sc_exec("INSERT INTO nums VALUES (42)");
impl.sc_exec("INSERT INTO nums VALUES (8)");
int64_t count = impl.sc_query_int("SELECT COUNT(*) FROM nums");
LOGOS_ASSERT_EQ(count, (int64_t)2);
int64_t sum = impl.sc_query_int("SELECT SUM(n) FROM nums");
LOGOS_ASSERT_EQ(sum, (int64_t)50);
impl.sc_close();
std::remove("/tmp/test_sc_qint.db");
}
LOGOS_TEST(errmsg_after_bad_sql) {
SqlcipherWrapImpl impl;
impl.sc_open("/tmp/test_sc_err.db", "pass");
impl.sc_exec("THIS IS NOT VALID SQL");
std::string err = impl.sc_errmsg();
LOGOS_ASSERT(err.size() > 0);
LOGOS_ASSERT(err.find("error") != std::string::npos || err.find("near") != std::string::npos);
impl.sc_close();
std::remove("/tmp/test_sc_err.db");
}
LOGOS_TEST(encrypted_db_unreadable_without_key) {
SqlcipherWrapImpl impl;
impl.sc_open("/tmp/test_sc_enc.db", "secretkey");
impl.sc_exec("CREATE TABLE data (val TEXT)");
impl.sc_exec("INSERT INTO data VALUES ('sensitive')");
impl.sc_close();
// Re-open with correct key — should work
int64_t rc = impl.sc_open("/tmp/test_sc_enc.db", "secretkey");
LOGOS_ASSERT_EQ(rc, (int64_t)0);
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
│ ├── lib/ # libsqlcipher.h, .c, .so, .a
│ ├── src/ # sqlcipher_app_impl.h/.cpp
│ ├── metadata.json
│ ├── CMakeLists.txt
│ ├── flake.nix
│ └── tests/
├── sqlcipher_app-ui/ # QML + C++ backend UI
│ ├── src/
│ │ ├── sqlcipher_app_ui.rep # Qt Remote Objects interface
│ │ ├── sqlcipher_app_ui_plugin.h/.cpp
│ │ └── qml/Main.qml
│ ├── metadata.json # dependencies: ["sqlcipher_app"]
│ ├── CMakeLists.txt
│ └── flake.nix # includes module as path: input
├── project.json
└── .gitignore
```
The scaffold automatically:
- Generates the `.rep` file with slots matching the library's functions
- Generates the UI plugin calling the backend module via `LogosAPIClient`
- Generates a QML view with input fields and a button for the first method
- Wires the UI's `flake.nix` to depend on the module
### 3.2 Build
```bash
cd logos-sqlcipher-app
git init && git add -A
# Build module first
cd sqlcipher_app-module
nix build
# Build UI (uses module via path: input from parent git repo)
cd ../sqlcipher_app-ui
nix build
```
Both produce plugin `.so` files ready for loading in `logoscore` or basecamp.
---
## How the scaffold decides what to generate
| Input directory contains | Scaffold behavior |
|--------------------------|-------------------|
| `.h` + `.c` only | Compiles `.c` as CMake source (`LANGUAGES C CXX`) |
| `.h` + `.so`/`.a` only | Links pre-built library (`EXTERNAL_LIBS`) |
| `.h` + `.c` + `.so`/`.a` | Both — compiles `.c` wrapper AND links `.so` |
| Multiple `.h` files | Prefers `lib*.h`, then directory-name match, skips internal headers |
| Multiple `.c` files | Only copies the `.c` matching the chosen header basename |
## C type mapping reference
The scaffold maps C types to the types supported by `logos-cpp-generator`:
| C type | C++ param type | C++ return type | Call wrapper |
|--------|---------------|-----------------|--------------|
| `int`, `int32_t` | `int64_t` | `int64_t` | `static_cast<int>(param)` |
| `long`, `int64_t` | `int64_t` | `int64_t` | direct |
| `unsigned int`, `uint32_t` | `uint64_t` | `uint64_t` | `static_cast<unsigned int>(param)` |
| `const char*` | `const std::string&` | `std::string` | `param.c_str()` / `std::string(result)` |
| `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:
```bash
gcc -shared -fPIC -o test.so libwrapper.c -L. -lthelib
```