Denim - Native NodeJS/BunJS addons powered by Nim
π Written in Nim language
nimble install denim
API reference
- CLI build via Nim + internal CMake driver (faster, no npm deps) or
node-gyp - Low-level & High-level API
- Nim (latest / via
choosenim) - Node (latest) and
node-gyp, or justcmakefor--cmakebuilds (no npm packages needed)
Denim is a hybrid package, you can use it as a CLI for compiling Nim code to .node addon
via Nim + internal CMake driver or node-gyp, and as a library for importing NAPI bindings.
denim build -h shows the build flags:
denim 0.2.1 - Nim code to Node.js/Bun.js in seconds via NAPI
MIT license | Made by Humans from OpenPeeps
https://github.com/openpeeps/denim
usage: denim <command> [options]
commands:
build <nim:file> build a native .node addon from Nim
build options:
-y remove an existing denim_build directory without asking
--cmake use the internal CMake driver (faster, no npm deps)
--generator:<name> CMake generator (default: Ninja if available, else Unix Makefiles)
--nodedir:<dir> Node headers directory (or NODE_HEADERS env)
--includeDirs:<list> extra -I header search dirs
--includeFiles:<list> extra .c files to compile
--defines:<list> extra -D defines
--cflags:<flags> extra C compiler flags
--linkFlags:<flags> extra linker flags
-r release build (-d:release --opt:speed)
--verbose print the Nim and CMake/node-gyp output
-h, --help show this help
options:
-h, --help show this help
--version show version
Denim reads your Nim code first: {.compile.} sources are compiled automatically,
and {.passC.} / {.passL.} pragmas reach the C compiler and linker without any
flags. The CLI flags below are only overrides for things Nim does not track.
Every flag accepts either a comma-separated string or a stringified JSON array:
--includeDirs '["/opt/foo/include","/opt/bar/include"]'
--defines 'FOO=1,BAR="baz"'
--cmakeβ build with the internal CMake driver instead of node-gyp (faster, recommended). Only needs acmakebinary and local Node headers (node_api.h); nocmake-jsnpm package required.--generatorβ CMake generator override, e.g.--generator "Unix Makefiles". Default:Ninjawhen available, elseUnix Makefiles.--nodedirβ Node headers override (also acceptsNODE_HEADERSenv). Default: auto-detected from the runningnodebinary and well-known include paths.-yβ remove an existingdenim_builddirectory without asking.-rβ release build (-d:release --opt:speed).--verboseβ print the Nim and CMake/node-gyp output.--includeDirsβ extra-Iheader search dirs. Seldom needed,{.passC: "-I...".}is forwarded automatically.--includeFilesβ extra.cfiles to compile. Seldom needed,{.compile: "foo.c".}files are picked up automatically.--definesβ extra-Ddefines, e.g.--defines 'FOO=1,BAR="baz"'. Prefer{.passC: "-DFOO=1".}in Nim.--cflagsβ extra C compiler flags, e.g.--cflags '-O2 -std=c99'. Prefer{.passC.}in Nim.--linkFlagsβ extra linker flags, e.g.--linkFlags '-L/opt/lib -lssl'. Prefer{.passL.}in Nim.
No cmake-js npm package is needed, only a cmake binary (3.15+) and a C++
compiler. Denim drives CMake itself:
- Runs
nim c --compileOnlyand reads the nimcache JSON, forwarding{.compile.}sources,{.passC.}include dirs/defines/flags, and{.passL.}link flags into a generatedCMakeLists.txt. - Locates local Node headers (
node_api.h) β override with--nodedir(orNODE_HEADERSenv). Otherwise it checks the runningnodeinstall prefix,which node,/usr/local,/usr,/opt/homebrew,/opt/local, and the~/.node-gypcaches. Nothing is downloaded. - Configures with
Ninjawhen available, elseUnix Makefiles(override with--generator), then buildsdenim_build/build/Release/<name>.nodeand moves it tobin/.
Omit --cmake to build with node-gyp instead (unchanged behavior, requires
node-gyp on PATH). On Windows the internal driver is not supported yet β
omit --cmake there as well.
denim build myaddon.nim --cmake -y
node -e "console.log(require('./bin/myaddon.node').add(2, 3))"
# 5
Use init to define module initialization.
when defined napibuild:
# optionally, you can use `napibuild` flag to wrap your code
# this flag is set when compiling via `denim build src/myprogram.nim`
import denim # import NAPI bindings
init proc(module: Module) =
# registering properties and functions here
# this is similar with javascript `module.exports`
elif isMainModule:
echo "just a normal nim program"Use low-level API to convert Nim values to napi_value (NapiValueType).
Use assert to check if a low-level function returns a success or failure. Currently, the following status codes are supported
import denim
init proc(module: Module) =
module.registerFn(0, "awesome"):
var str2napi: napi_value
var str = "Nim is awesome!"
assert Env.napi_create_string_utf8(str, str.len.csize_t, str2napi.addr)
return str2napiAlternatively, use %* to auto-convert Nim values to NapiValueType.
let
a: napi_value = %* "Hey"
b: napi_value = %* true
assert a.kind == napi_string
assert b.kind == napi_booleanSince v0.1.5, you can use {.export_napi.} pragma to export functions and object properties.
import denim
init proc(module: Module): # the name `module` is required
proc hello(name: string) {.export_napi} =
## A simple function from Nim
return %*("Hello, " & args.get("name").getStr)
var awesome {.export_napi.} = "Nim is Awesome!"Calling a function/property from Node/Bun
const app = require('myaddon.node')
console.log(app.hello("World!")) // Hello, World!
console.log(app.awesome) // Nim is Awesome!app.hello()/*
* A simple function from Nim
* @param {string} name
* @return {string}
*/
Type mismatch parameter: `name`. Got `undefined`, expected `string`
The classic Nim idiom of shipping C sources with {.compile.} just works.
This example wraps a tiny C Magic 8-Ball and asks it questions from Node:
vendor/magic8ball.h
#ifndef MAGIC8BALL_H
#define MAGIC8BALL_H
const char* magic8ball_ask(unsigned int seed);
#endifvendor/magic8ball.c
#include "magic8ball.h"
#ifndef NUM_ANSWERS
#error "NUM_ANSWERS not defined - pass it via {.passC.}"
#endif
#if NUM_ANSWERS != 8
#error "NUM_ANSWERS out of sync with the answers table"
#endif
static const char* answers[] = {
"It is certain", "Without a doubt", "Ask again later",
"Cannot predict now", "Don't count on it", "My sources say no",
"Signs point to yes", "Very doubtful"
};
const char* magic8ball_ask(unsigned int seed) {
return answers[seed % NUM_ANSWERS];
}vendor/magic.nim
import std/os
const vendorDir = currentSourcePath().parentDir()
{.passC: "-I\"" & vendorDir & "\"".}
{.passC: "-DNUM_ANSWERS=8".}
{.compile: "magic8ball.c".}
{.push importc, cdecl, header: "magic8ball.h".}
proc magic8ball_ask(seed: cuint): cstring
{.pop.}
proc ask*(seed: int): string = $magic8ball_ask(seed.cuint)myaddon.nim
import vendor/magic
import denim
init proc(module: Module) =
proc ask8ball(seed: int): string {.export_napi.} =
return %* ask(args.get("seed").getInt)Build it and shake the ball:
denim build myaddon.nim --cmake -y
node -e "const m = require('./bin/myaddon.node'); console.log(m.ask8ball(42))"
# Ask again later
Denim forwards the vendored magic8ball.c and the -I/-DNUM_ANSWERS=8
flags into the CMake build. Without them the build would fail on
#include "magic8ball.h" or on the #error guards, try removing the
{.passC.} lines to see it break.
System libraries need no vendoring, just {.passL.}. Denim picks it up from
the Nim build and links it, no --linkFlags needed. This addon roundtrips a
string through zlib's compress/uncompress:
myaddon.nim
{.passL: "-lz".}
{.push importc, header: "<zlib.h>".}
proc compressBound(sourceLen: culong): culong
proc compress(dest: cstring, destLen: ptr culong,
source: cstring, sourceLen: culong): cint
proc uncompress(dest: cstring, destLen: ptr culong,
source: cstring, sourceLen: culong): cint
{.pop.}
proc roundtrip*(s: string): string =
var cbuf = newString(compressBound(s.len.culong))
var cwritten = cbuf.len.culong
if compress(cbuf.cstring, addr cwritten, s.cstring, s.len.culong) != 0:
raise newException(ValueError, "compress failed")
var ubuf = newString(s.len + 1)
var uwritten = ubuf.len.culong
if uncompress(ubuf.cstring, addr uwritten, cbuf.cstring, cwritten) != 0:
raise newException(ValueError, "uncompress failed")
ubuf.setLen(uwritten)
return ubuf
import denim
init proc(module: Module) =
proc zroundtrip(input: string): string {.export_napi.} =
return %* roundtrip(args.get("input").getStr)denim build myaddon.nim --cmake -y
node -e "console.log(require('./bin/myaddon.node').zroundtrip('Hello from zlib!'))"
# Hello from zlib!
The generated build links -lz automatically. Only reach for
--linkFlags '-L/opt/lib -lfoo' when the library is invisible to Nim,
e.g. a path {.passL.} cannot express.
The smallest possible roundtrip: one C function, one Nim wrapper, one exported addon function.
vendor/mini.h
#ifndef MINI_H
#define MINI_H
int mini_add(int a, int b);
#endifvendor/mini.c
#include "mini.h"
int mini_add(int a, int b) { return a + b; }vendor/mini.nim
import std/os
const vendorDir = currentSourcePath().parentDir()
{.passC: "-I\"" & vendorDir & "\"".}
{.compile: "mini.c".}
{.push importc, cdecl, header: "mini.h".}
proc mini_add(a, b: cint): cint
{.pop.}
proc add*(a, b: int): int = mini_add(a.cint, b.cint).intmyaddon.nim
import vendor/mini
import denim
init proc(module: Module) =
proc add(a: int, b: int): string {.export_napi.} =
return %* $(add(args.get("a").getInt, args.get("b").getInt))Copy this skeleton whenever you need a single C helper inside an addon, then grow toward the 8-ball pattern once you need headers and defines.
- Tim Engine β A template engine. GitHub
- Bowdy β A fast stylesheet language, alternative to SassC, DartSass. GitHub
- HappyX β Macro-oriented asynchronous web-framework written in Nim. GitHub
- OpenParser β A collection of tiny parsers and dumpers. CSV, TOML, YAML, RSS, BSON, FBE, DotEnv, Regex, SQL, Gettext (po/mo) and more! GitHub
- DROP YOUR PROJECT HERE! π File an Issue
- Option to link external C Headers/libraries
- Extend High-level API with compile-time functionality.
- π Found a bug? Create a new Issue
- π Wanna help? Fork it!
Denim | MIT license. Made by Humans from OpenPeeps
Thanks to Andrew Breidenbach and Andrei Rosca for their work.
Copyright Β© 2026 OpenPeeps & Contributors β All rights reserved.