Denim - Native NodeJS/BunJS addons powered by Nim
π Written in Nim language
nimble install denim
API reference
- CLI build via Nim +
node-gypor CMake.js (faster) - CLI publish to NPM
- Low-level & High-level API
- Open Source |
MITLicense
- Nim (latest / via
choosenim) - Node (latest) and
node-gypor CMake.js
Denim is a hybrid package, you can use it as a CLI for compiling Nim code to .node addon via Nim + NodeGYP and as a library for importing NAPI bindings.
Simply run denim -h
DENIM - Nim code to Bun.js/Node.js in seconds via NAPI
(c) George Lemon | MIT License
Build Version: 0.2.0
build <nim:file> Build a native `node` addon from Nim
-y:bool
--cmake:bool Whether to use CMake.js instead of node-gyp (faster)
--includeDirs:string Extra `-I` header search dirs
--includeFiles:string Extra `.c` files to compile
--defines:string Extra `-D` defines
--cflags:string Extra C compiler flags
--linkFlags:string Extra linker flags
-r:bool Compile in release mode (default: false)
--verbose:bool Verbose output
publish <addon:file> Publish your addon (requires npm cli)
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 CMake.js instead of node-gyp (faster, recommended).-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.
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))denim build myaddon.nim --cmake -y
node -e "console.log(require('./bin/myaddon.node').add(2, 3))"
# 5
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.
Use Denim as a Nimble task:
task napi, "Build a .node addon":
exec "denim build src/myprogram.nim"Want to pass custom flags to Nim Compiler? Create a .nims file:
when defined napibuild:
# add some flagsNote
Combining native code with JavaScript is a powerful way to optimize performance-critical parts. Note that converting data between Nim and N-API types adds overhead, so it's best to minimize the number of calls across the boundary and batch data when possible.
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`
- 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!
- 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.