Back to News & Insights
JavaScript October 5, 2026 路 13 min read

Relaxnative: Import C, C++, Rust, Zig and Go Straight into Node.js

Relaxnative lets you call C, C++, Rust, Zig and Go functions from Node.js with one loadNative() call: compile-on-import, a content-hash cache, ahead-of-time builds, three isolation modes and a signed

Relaxnative: Import C, C++, Rust, Zig and Go Straight into Node.js

TL;DR: Relaxnative is a Node.js library that lets you write a function in C, C++, Rust, Zig or Go, point loadNative() at the file, and call it from JavaScript like any other function. No node-gyp, no binding.gyp, no hand-written N-API glue. It compiles on first import, caches by content hash, can ship prebuilt so production needs no compiler, and lets you choose how much a native crash is allowed to hurt. 馃敆 GitHub 路 馃摝 npm 路 馃悰 Issues

Table of Contents Why I built this Hello, native Five languages, one call What loadNative() actually does Reading signatures: tree-sitter and friends The types contract A cache keyed by everything Ahead-of-time builds: no compiler in production Three isolation modes Annotations: @sync, @async, @cost Benchmarks: where native wins (and where it loses) RelaxRegistry and supply-chain trust Developer experience What I learned What's next

JavaScript is fast, often surprisingly fast. But every Node developer eventually hits a wall where it isn't fast enough: 64-bit integer math. JS numbers are doubles, so real u64 work means BigInt, and BigInt is slow. Raw byte crunching, like image pixels, checksums and histograms, where you want tight loops over memory with no bounds checks. Code that already exists in C or Rust that you'd rather reuse than rewrite.

The traditional answer is a native addon: node-gyp, a binding.gyp, N-API boilerplate, a C++ wrapper per function, and a build that breaks on every other Windows machine. It works, but it's a lot of ceremony for "I want to call add(a, b) written in C".

I wanted something closer to Python's ctypes or Bun's bun:ffi, but for plain Node, for several languages, and with real answers for safety. What happens when native code segfaults? What happens when you install someone else's native code? That's Relaxnative.

That's the whole integration. The first loadNative() finds a compiler, builds a shared library, reads the function signatures out of your source and binds them. Every later run hits the cache.

Each language compiles to a C-ABI shared library (.dll / .so / .dylib), so from JavaScript they all look the same:

| Language | Extension | How you export | Compiler | | --- | --- | --- | --- | | C | .c | plain top-level functions | clang / gcc / MSVC cl / zig cc | | C++ | .cpp .cc .cxx | extern "C" functions | clang++ / g++ / cl / zig c++ | | Rust | .rs | #[nomangle] pub extern "C" fn | rustc --crate-type cdylib | | Zig | .zig | export fn | zig build-lib -dynamic | | Go | .go | //export Name + import "C" | go build -buildmode=c-shared |

One Zig install is a cheat code here: zig cc and zig c++ are clang-based, so a single download covers C, C++ and Zig.

Go on Windows was the fun one. To build a real Windows DLL, Relaxnative cross-compiles inside WSL with GOOS=windows CGOENABLED=1 CC=x8664-w64-mingw32-gcc go build -buildmode=c-shared, so you only need go and mingw-w64 inside your distro.

Multi-file builds work too. Extra C/C++ sources get linked into the same library, Rust files are mod-declared, Zig files are @imported, and Go files form one main package:

The whole library is built around one pipeline in src/loader.ts: Prebuilt? If .relaxnative/prebuilt/ has an artifact for this platform and this exact source content, use it and skip compiling entirely. Detect the toolchain for the file's language (clang, gcc, cl, zig, rustc, go). Hash the compile inputs. On a cache hit, reuse the library. Compile to a shared library on a miss. C/C++ get -O2 by default, because building at -O0 makes native code lose badly to V8's JIT and that's an embarrassing benchmark. Parse the signatures from your source to learn names, parameter types and return types. Bind them with koffi, a fast FFI library for Node. Wrap each function according to the isolation mode you picked.

To bind a function, Relaxnative has to know its signature without you writing it twice. So it reads your source: C, C++ and Rust are parsed with tree-sitter grammars, giving a real syntax tree instead of regex guesses. Zig and Go use small lexical scanners that look for export fn and //export and map types like i32, []u8 or C.int to the shared type model.

Real C is messier than it looks, and the parser has the scars to prove it. My favourite: for a pointer-returning function like char greet(int), tree-sitter nests the function declarator inside a pointerdeclarator and hangs the there, not on the return type. The first version read the name as greet(int), found no parameters, decided the return type was unknown, and silently dropped the function. The fix unwraps every pointerdeclarator layer and folds the stars back into the return type.

Other details it handles: foo(void) means "no parameters", so the lone void is dropped. Variadic functions (printf-style) are detected and rejected instead of being called with a garbage stack. Pointer stars can live in the declarator (uint8t buf), so type and declarator text are combined with a space. Otherwise unsigned long long x becomes unsigned long longx.

And there's a rule: if a type isn't recognized, fail fast. A wrong FFI signature doesn't throw a nice error. It corrupts memory.

| Concept | C / C++ | Rust | Zig | Go (cgo) | | --- | --- | --- | --- | --- | | 32-bit int | int / int32t | i32 | i32 | C.int | | 64-bit int | int64t | i64 | i64 | int64 | | float / double | float / double | f32 / f64 | f32 / f64 | float32 / float64 | | bool | bool | bool | bool | bool | | byte buffer | uint8t | mut u8 | []u8 | byte | | C string | const char | const i8 | [c]const u8 | C.char |

On the JavaScript side: Scalars become number. 64-bit integers can come back as bigint and accept either. Pointers accept TypedArrays: pass a Uint8Array for uint8t or a Uint32Array for uint32t. Native code writes straight into your array. const char parameters and returns are plain JS strings. For manual memory there's native.alloc(size), which returns a NativeBuffer with an .address.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation