Nim C Wrappers
Rules
Architecture
- Use a two-layer design: raw FFI layer (ABI-faithful) + ergonomic Nim layer (safe, idiomatic). Keep the raw layer thin and stable.
- Split modules by library domain. Mirror the C header structure for large libraries.
- Centralize shared types in a
lib_raw_typesmodule to avoid cyclic imports. - Export raw symbols only for advanced users; re-export selected symbols from ergonomic modules.
Raw FFI Layer
- Use
importcwithcdecl(orstdcallif the library requires it). Prefer{.push callconv: cdecl, importc.}blocks for shared conventions. headerdeclares linker-resolved imports;dynlibloads symbols at runtime. Do not combine them on one proc. Guard library names withwhen defined(windows):etc.- Never reorder struct fields. Use
objectin C field order. Addpackedonly if C headers specify packing. - Use
incompleteStructand list only needed fields to reduce ABI risk. - For C enums, use
distinctinteger types +constvalues. Do not use Nimenumin raw bindings. Find actual enum values in C headers—do not guess. - For bitflags, use
distinctinteger types with bitwise helpers. Do not useset[Enum]. - Map C macros: numeric →
const; function-like →template; useinline procwhen arguments should evaluate once. - Keep pointer types as C intends — do not convert them in the raw layer.
Type Mapping
| C Type | Nim Type | Notes |
|---|---|---|
char | cchar | Platform-dependent signedness |
signed char | cschar | Always int8 |
unsigned char | uint8 | Safer default spelling; avoid relying on cuchar status across Nim versions |
short | cshort | Always int16 |
unsigned short | cushort | Always uint16 |
int | cint | Always int32 |
unsigned int | cuint | Always uint32 |
long | clong | ABI-sized (4 on Win64, 8 on LP64) |
unsigned long | culong | ABI-sized |
long long | clonglong | Always int64 |
unsigned long long | culonglong | Always uint64 |
size_t | csize_t | Alias for uint |
intptr_t | int | Pointer-sized signed |
uintptr_t | uint | Pointer-sized unsigned |
float | cfloat | Always float32 |
double | cdouble | Always float64 |
long double | clongdouble | Limited codegen support |
void* | pointer | Untyped |
T* | ptr T | Nullable |
T** | ptr ptr T | |
char* / const char* | cstring | NUL-terminated |
char** | cstringArray | ptr UncheckedArray[cstring] |
Struct types: object in C order. Fixed arrays: array[N, T]. Pointer+length: ptr T + csize_t. Raw buffers: ptr UncheckedArray[T]. Opaque handles: pointer or ptr OpaqueObj (empty object).
Ergonomic Layer
- In
=destroy, explicitly call=destroyon owned nested GC-managed fields (string, seq) after releasing C resources. - Define ownership hooks in the same module as the type. Cross-module hook definitions are a compile error.
- Raise exceptions (IOError, ValueError, etc.) for C errors — do not return result wrappers that only carry ok/kind/message.
- Do not create custom exception types unless callers handle them differently.
- Catch errors only at translation boundaries (C return code → Nim exception, or exception → domain result). Let exceptions propagate otherwise.
Naming
- In raw bindings, match C identifier names directly.
importcwithout an explicit string resolves the C symbol from the Nim name.- Strip redundant C prefixes (LIB_, foo_) in the ergonomic layer.
- Keep raw constant names in C style (e.g.,
CURLE_OK). - When a C identifier collides with a Nim keyword, rename the Nim identifier and use
importc:to map to the original C name.
Callbacks
- Declare callbacks as plain C-callable procs such as
proc onEvent(code: cint; userData: pointer) {.cdecl.}. - Pass closure state through
userdataviarawProc/rawEnv. Seereferences/callback_pattern.md.
Verification
- Add
static: doAssert sizeof(T) == NandoffsetOfchecks in tests to verify struct layouts. - Test ABI with compile + link + smoke test + runtime checks.
Workflow
- Read the C API. Identify structs, enums, functions, callbacks, and ownership semantics.
- Map types. Use the type table. Verify sizes with
static: doAssert sizeof. - Write raw bindings. One module per C header or subsystem. Centralize shared raw types to avoid cycles. Use
importc, correct calling convention,packedonly when C specifies it. Use typed integer aliases for enums. - Write ergonomic wrappers. Add destructors for owned resources (move-only or RC). Raise exceptions for C errors. Re-export only the intended public surface. Strip prefixes.
- Test. Compile check → link check → smoke test → ABI sizeof/offset asserts → runtime ownership tests.
- Iterate. Add missing APIs as needed. Keep raw layer stable.
Common Mistakes
| Mistake | Why it's wrong |
|---|---|
Passing Nim string directly to char* parameter | Type mismatch; use .cstring |
Using seq where C expects stable pointer | add() can reallocate, invalidating the pointer |
| Reordering struct fields | Breaks ABI layout, causes garbage values |
Using Nim enum for C enum values | ABI incompatibility; use typed integer aliases |
| Adding enum or constant values without inspecting headers | Find actual enum values in C headers, do not guess |
| Passing closures as C callbacks | C expects plain function pointers, not GC closures |
Storing cstring beyond the call | cstring from string.cstring dangles after the source is freed |
=dup body using result = src in RC pattern | May trigger implicit =copy instead of =dup; use field-by-field assignment |
| Wrong calling convention | Causes crashes at call sites |
References
references/move_only_resource.md— A move-only wrapper for one owner of a C handle.references/rc_resource.md— A reference-counted wrapper for shared C resource ownership.references/callback_pattern.md— C callbacks with userdata, captured Nim state, and lifetime management.references/enum_and_bitflags.md— Distinct-integer wrappers for non-contiguous enums and bitwise flags.references/module_layout.md— Separating raw FFI from an ergonomic API in two modules.


