Nim C Bindings & CI
Rules
Binding Fundamentals
- Use
importcwithcdeclfor C APIs unless the library explicitly requires a different calling convention (e.g.,stdcall). - Represent opaque C handles as
type Name = ptr objecttypes. UseincompleteStructfor partial/opaque structs to avoid size/layout mismatches. - Use
{.bycopy.}on structs that Nim must pass by value to C. - Declare the C header in the binding when the compiler needs the C definitions for compilation, for example
header: "foo.h". - Use
{.push callconv: cdecl, importc.}blocks when many declarations share the same convention. - Match C storage with Nim's C-compatible types such as
cint,cuint,clong,culong,clonglong,csize_t,cfloat,cdouble,cstring, and pointer types. - Preserve the complete field order for concrete structs passed or returned by value.
- When the requested deliverable is a raw binding, import C symbols directly. Put ergonomic aliases and conversions in a separate wrapper layer.
Linking
- System libraries: link with
-l<name>only. Do not hardcode-Lpaths — the OS toolchain already knows where system libs live. - Local/third-party libraries: add both
-L<dir>and-l<name>(or.lib/.dll.libpaths on Windows MSVC). - Use repository-relative paths for vendored dependencies, for example
third_party/libfoo, to keep builds hermetic. - Distinguish linker-resolved imports from Nim runtime
dynlibimports.headersupplies declarations for generated C; a runtime-onlydynlibproc should not also useheader.
Runtime Library Resolution
- Vendored/local shared libs: colocate the
.so/.dylib/.dllnext to the executable. Do not rely on environment variables for discovery. - System-installed libs: do not copy DLLs/shared libs next to the executable. Rely on the platform's normal system loader configuration. Use environment variables only as temporary overrides.
- On Linux, add rpath
$ORIGINonly when loading colocated shared libs. From the shell, pass--passL:"-Wl,-rpath,\$ORIGIN". In Nim source, use{.passL: "-Wl,-rpath,\\$ORIGIN".}. - Do not use build-tree-only rpaths or absolute paths to non-system shared libraries.
Platform Rules
| Platform | Toolchain | Deps | Link flags | Runtime |
|---|---|---|---|---|
| Linux | System GCC/Clang | apt | -l<name> or -L<dir> -l<name> | Colocate local .so; rpath $ORIGIN if needed |
| macOS | Apple Clang | Homebrew | staticExec("brew --prefix") for -I/-L | Colocate local .dylib |
| Windows | MSVC (--cc:vcc) | vcpkg only | .lib/.dll.lib full paths | Colocate .dll next to .exe |
- On macOS, resolve include/link paths via
staticExec("brew --prefix <formula>"). - On Windows, use MSVC via
--cc:vcc. Use vcpkg only — avoid Chocolatey and MSYS2. Never mix toolchains. - On Windows, do not rely on implicit
-l<name>for MSVC. Use explicit.libor.dll.libpaths. - On Windows CI, export
VCPKG_ROOTto the installed triplet root and prepend itsbintoPATH.
CI & Release
- CI is the authoritative spec for supported platforms, toolchains, and flags. Local workflows must be compatible with CI.
- Keep test builds simple: compile, then run, with minimal environment mutation.
- Align the local toolchain with CI (e.g., MSVC + vcpkg
x64-windows-releasewith--cc:vcc). - Use the reference workflows as starting points —
references/ci.ymlfor validation,references/release.ymlfor tagged releases.
Verification
- Compile against the real header and call the real C implementation.
- Exercise concrete structs passed and returned by value. Compare Nim layout
with C
sizeofandoffsetofprobes when layout is in doubt. - Test bundled shared libraries after copying the executable and library into a clean directory.
- For
dynlib, verify runtime loading without also linking that library.
Workflow
- Identify the C API. Determine calling convention, opaque vs value types, and ownership.
- Write bindings. Use
importc, correct calling convention,incompleteStructfor opaque types,bycopyfor value types. Addheaderpragma. - Set up linking. System libs:
-lonly. Local libs:-L+-lwith repo-relative paths. Windows: explicit.libpaths. - Handle runtime. Colocate local shared libs. For system-installed libs, rely on the normal loader search path. Add rpath
$ORIGINon Linux only for colocated local libs. - Verify the ABI. Run a real C round trip for the types and calls used by the binding.
- Add CI. Copy
references/ci.yml, adapt placeholders (<package>,<src/main.nim>, dependency lists). - Add release. Copy
references/release.yml, adapt placeholders, configure draft releases. - Test locally, then push. Verify the build works locally with the same flags CI uses.
Common Mistakes
| Mistake | Why it's wrong |
|---|---|
Hardcoding -L paths for system libraries | OS toolchain already knows where they are; breaks portability |
| Mixing MSVC + MSYS2/MinGW on Windows | Incompatible ABIs, linker errors, runtime crashes |
Using implicit -l<name> with MSVC | MSVC doesn't resolve libs the same way GCC does; use explicit .lib paths |
| Relying on env vars for vendored shared libs | Fragile across machines; colocate instead |
| Build-tree-only rpaths | Breaks when the binary moves; use $ORIGIN for colocated libs |
| Guessing Windows dependency paths | Non-deterministic; use vcpkg with known install roots |
| Compiling C sources into the executable when the task requires an existing shared library | Bypasses shared-library link and relocation behavior |
| Adding Nim-bodied wrappers to a requested raw binding surface | Hides whether the C API was imported directly and changes the requested layer |
References
references/raw_binding.md— Mapping an opaque handle and by-value C struct.references/ci.yml— Building a C-dependent project on Linux, macOS, and Windows with Nim, Atlas, and vcpkg.references/release.yml— Publishing tagged cross-platform archives in a draft release.


