C Interop & the Foreign Function Interface
--app:lib turns a Nim project into something other languages can link and call.
Calling C from Nim
The backend integration manual describes the direction of the call as an architectural decision: "Usually the direction of which calls which depends on your software architecture (is Nim your main program or is Nim providing a component?)". This chapter is the first direction — Nim is the main program and the C library is a component.
importc, header and cdecl
A foreign procedure is declared like a Nim procedure with an empty body and three pragmas: importc names the C symbol, header tells the compiler which C header to include, and the calling convention (usually cdecl) must match the C prototype. Nothing is generated for the body because the body exists in C.
# Minimal binding to three functions of the C standard library. The body is
# empty on purpose: 'importc' means "the implementation is already out there".
proc cSqrt(x: cdouble): cdouble {.importc: "sqrt", header: "<math.h>".}
proc cStrcmp(a, b: cstring): cint {.importc: "strcmp", header: "<string.h>".}
proc cPutchar(ch: cint): cint {.importc: "putchar", header: "<stdio.h>".}
# A variadic C function: the 'varargs' pragma says the C prototype ends in '...'.
# Nim cannot check the extra arguments, so the format string is your contract.
proc cPrintf(fmt: cstring): cint {.importc: "printf", header: "<stdio.h>", varargs.}
# Type mapping is explicit: Nim 'float' is not C 'double' by default, 'cstring'
# is a pointer to characters, and 'cint' matches C 'int' on every platform.
let y = cSqrt(2.0) # call C's sqrt with a C double
echo y > 1.41 and y < 1.42 # true — 1.41421... printed as float
echo cStrcmp("abc", "abd") # -1: C compares bytes, not characters
discard cPutchar('N'.cint) # prints N, returns the character written
discard cPrintf("hello from C: %d\n".cstring, 42.cint)
Declare the header in angle brackets exactly as C would, escape the angle brackets in HTML (above) so the page renders them, and keep one binding module per C library so the pragmas live in a single place that can be reviewed.
Opaque Handles and C Structs
C libraries usually hand back pointers to structures whose layout is private. Nim models those as an incomplete object and only ever passes the pointer, which keeps the binding valid even when the struct changes size.
type
CFile {.importc: "FILE", header: "<stdio.h>", incompleteStruct.} = object
## 'incompleteStruct' means: do not guess the layout, only a pointer is legal.
proc cFopen(name, mode: cstring): ptr CFile {.importc: "fopen", header: "<stdio.h>".}
proc cFclose(f: ptr CFile): cint {.importc: "fclose", header: "<stdio.h>".}
proc cFputs(s: cstring, f: ptr CFile): cint {.importc: "fputs", header: "<stdio.h>".}
let handle = cFopen("/dev/null", "w") # may return nil on failure
if handle.isNil: # always test a foreign pointer
echo "could not open the file"
else:
discard cFputs("data\n".cstring, handle)
discard cFclose(handle) # every borrow has a matching free call
Compiling and Linking Foreign Code
Declaring a prototype is only half of a binding: the compiler still has to see the foreign source file, and the linker still has to find the library. Both requests can be made from inside the source, which keeps a binding module self-contained instead of relying on somebody remembering a command line.
Sources, Include Paths and Linker Flags
# cbits/scale.c is compiled by Nim's C backend and linked into the program.
# Paths in pragmas are resolved relative to the file containing them.
{.compile: "cbits/scale.c".}
{.passC: "-I cbits".} # forwarded verbatim to the C compiler
{.passL: "-lm".} # forwarded verbatim to the linker
proc scale(value, factor: cint): cint {.importc, header: "scale.h", cdecl.}
# 'importc' with no name pragma: the Nim name must equal the C name.
echo scale(21, 2) # 42 — implemented in cbits/scale.c
The same switches exist on the command line, which is how you add a flag for one build without editing sources: nim c --passC:"-I include" --passL:"-lSDL2" app.nim. Write {.compile.} once per file, or list them ({.compile: ["a.c", "b.c"].}); the objects are cached, so repeated builds only recompile what changed.
Dynamic Libraries
When the library must be found at run time — a plugin, an optional dependency, or a system library whose version varies — the dynlib pragma delays symbol resolution until the program starts. Its argument is a pattern, so one declaration can match the naming conventions of several platforms.
# The pattern 'libtcl(|8.5|8.6).so' matches libtcl.so, libtcl8.5.so and
# libtcl8.6.so, taking whichever the loader finds first.
proc tclInit(interp: pointer): cint {.cdecl, importc: "Tcl_Init",
dynlib: "libtcl(|8.5|8.6).so".}
# A missing library becomes a runtime error at the first call, not a link error
# at build time — which is exactly what an optional integration wants.
if tclInit(nil) == 0:
echo "Tcl refused the interpreter"
For fully dynamic loading — deciding on a library while the program runs — std/dynlib exposes the loader directly: loadLib, symAddr and unloadLib.
import std/dynlib
let handle = loadLib("libz.so.1") # nil when the library is absent
if handle.isNil:
echo "zlib is not installed" # degrade gracefully instead of crashing
else:
# Cast the raw symbol address to a typed Nim proc; the signature is the
# contract you must get right, because the loader cannot check it for you.
let version = cast[proc (): cstring {.cdecl.}](symAddr(handle, "zlibVersion"))()
echo "zlib ", version
unloadLib(handle) # release the handle before exiting
Exporting Nim to C and C++
The opposite direction is what makes Nim usable as a library: your Nim procedure is compiled as an ordinary C function the host program calls. The backend manual frames this as the architectural choice — Nim can be the component rather than the main program — and the mechanism is a pair of pragmas plus a library build mode.
exportc and the C ABI
# No 'importc' here: the body is Nim, and 'exportc' tells the C backend to give
# it a stable, unmangled symbol name the host program can declare.
proc fib(a: cint): cint {.exportc, cdecl.} =
## 'cdecl' fixes the calling convention to the platform's C convention, so a
## C header declaring 'int fib(int);' matches this definition exactly.
if a <= 2: 1 else: fib(a - 1) + fib(a - 2)
# Exporting an overloaded proc needs an explicit name, because C has no
# concept of overloading:
proc toString(n: cint): cstring {.exportc: "intToString", cdecl.} =
result = cstring($n) # WARNING: see the ownership note further down
Types crossing the boundary must exist in both languages: use cint, csize_t, cstring and pointers, not Nim int, string or seq. Nim's int is 64-bit on most platforms while C's int is 32-bit, so a Nim int in an exported signature quietly changes the ABI.
Building the Library and NimMain
An exported proc is only reachable if the artefact is a library, so the build switches change: --app:lib produces a shared library, --app:staticLib a static one, and --header writes out the matching C header.
nim c --app:lib --header:fib.h -o:libfib.so fib.nim # shared library + header
nim c --app:staticLib -o:libfib.a fib.nim # static archive
# The backend manual shows the static variant being linked into a C program:
# gcc -o m -Inimcache -Ipath/to/nim/lib maths.c libfib.nim.a
Ownership at the Boundary
A pointer that crosses into C is invisible to Nim's collector: if the host holds the only reference to a Nim-owned object, the collector may free it while the host still uses it. The usual pattern is one allocation the host owns, released through an exported destructor.
type Handle = ref object
items: seq[string]
proc newHandle(): Handle {.exportc: "handleNew", cdecl.} =
## The host receives a raw pointer. Ownership now belongs to the host, so the
## handle must survive until the matching 'handleFree' call.
GC_ref(result) # keep the object alive regardless of Nim refs
result = Handle(items: @[])
proc addItem(handle: Handle, text: cstring) {.exportc: "handleAdd", cdecl.} =
handle.items.add($text) # 'cstring' is converted at the boundary
proc size(handle: Handle): csize_t {.exportc: "handleSize", cdecl.} =
csize_t(handle.items.len) # csize_t, not Nim's int: the C ABI decides
proc freeHandle(handle: Handle) {.exportc: "handleFree", cdecl.} =
GC_unref(handle) # hand ownership back before the last reference goes
Three details keep this correct. Freeing the Nim side of an object the host still holds is a use-after-free, so the free call must be the host's responsibility and documented as such. Returning a cstring built from a Nim string is a dangling pointer as soon as the temporary is collected — copy into host-provided memory instead. And the artefact must be built as a library: with --app:lib or --app:staticLib, the C code calls NimMain() once at startup to initialise the Nim runtime before touching any exported proc.
Generating C++
The C++ backend (nim cpp) emits C++ instead of C, which lets the generated code use C++ templates and std::string, and lets you link against C++ libraries directly. The interop pragmas are the same; {.importcpp.} and {.exportcpp.} replace their C counterparts, and a header is attached with the same header pragma.
Building with the C++ Backend
The switch is the compilation command itself: nim cpp in place of nim c. Everything else — build modes, optimisation, cross-compilation — behaves as it does for C, because the backend is simply a different receiver of the same switches.
nim cpp -d:release app.nim # emit C++ and compile it
nim cpp -r app.nim # compile and run in one step
nim cpp --compileOnly app.nim # stop after the generated C++ is written
--compileOnly is the inspection tool: it leaves the generated C++ on disk in the nimcache directory, which is the reliable way to answer "what did this construct actually compile to?". It is also how you spot the helper types the backend inserts around closures and exceptions.
importcpp Patterns
# 'importcpp' takes a pattern; '#' is replaced by each argument in order.
proc cppSqrt(x: cdouble): cdouble {.importcpp: "std::sqrt(@)", header: "<cmath>".}
echo cppSqrt(9.0) # 3.0
# Constructors are just procedures returning a value:
proc newVector(): cint {.importcpp: "std::vector<int>().size()",
header: "<vector>".}
Roughly speaking, three pragma families cover the boundary: importc/exportc for C, importcpp/exportcpp for C++, and importjs/exportjs for JavaScript. Each has a pattern form, which is how operator names, templates and overloads — things C does not have — are expressed. Compile with nim cpp to generate C++; the same source often compiles under both backends, which is useful for comparing the emitted code.
Before writing an importc by hand, check whether the binding already exists: nimble search covers most popular C libraries, and the manual's foreign function interface chapter documents every pragma referenced above.