How Node.js Runs C++ and Rust Native Addons
Node.js allows developers to execute high-performance, low-level code written in C++ or Rust directly within the JavaScript runtime using native addons. This article explains how Node.js manages these native addons through Node-API (formerly N-API), detailing the role of Application Binary Interface (ABI) stability, how the translation layer works, and how JavaScript loads and executes compiled binary code.
What is Node-API (N-API)?
Historically, writing native addons for Node.js required interacting directly with the V8 JavaScript engine APIs. However, because V8 changes frequently, addons had to be recompiled for every major Node.js release.
To solve this, Node.js introduced Node-API (originally called N-API). Node-API is a fully stable, engine-independent API for building native addons. It provides an abstraction layer over the V8 engine, guaranteeing Application Binary Interface (ABI) stability. This means an addon compiled for one version of Node.js will run on newer versions of Node.js without needing recompilation, even if the underlying V8 engine undergoes major updates.
The Bridge: How JavaScript Communicates with Native Code
Node-API acts as a translator between the managed heap of JavaScript and the unmanaged memory of C++ or Rust.
- Type Conversion: JavaScript types (such as Objects,
Strings, and Numbers) do not exist in C++ or Rust. Node-API provides
C-style functions to safely convert JavaScript values into native types
and vice versa (e.g., converting a JavaScript
Numberto a C++double). - Context and Scope Management: Node-API manages the lifecycle of JavaScript objects passed to native code. It prevents the V8 garbage collector from deleting objects while they are being processed by C++ or Rust.
- Threading: JavaScript runs on a single main thread. Node-API allows native addons to offload CPU-intensive tasks to worker threads using the underlying libuv thread pool, preventing the JavaScript event loop from blocking.
How C++ Addons are Handled
For C++ development, the community uses
node-addon-api, a header-only C++ wrapper
class that simplifies the plain C functions of Node-API.
- Compilation: C++ source code is compiled into a
dynamically linked shared library. The standard build tool used is
node-gyp, which compiles the code into a binary file with a.nodeextension (similar to a.dllon Windows or.soon Linux). - Execution: When the
.nodefile is loaded, it registers its exported functions with the Node.js runtime via Node-API hooks.
How Rust Addons are Handled
Rust has become highly popular for Node.js addons due to its memory
safety and performance. The integration relies on tools like
napi-rs or
Neon, with napi-rs being the
industry standard for Node-API integration.
- Compilation: Developers write idiomatic Rust code
and use procedural macros (attributes like
#[napi]) provided bynapi-rs. These macros automatically generate the C-compatible Node-API boilerplate under the hood. - Tooling: The Rust compiler (
rustc) andcargocompile the code into a shared library. Thenapi-rsCLI tool then packages the compiled library into a.nodebinary.
Loading and Executing the Addon in JavaScript
Once the C++ or Rust code is compiled into a .node
binary, loading it in JavaScript is straightforward.
Node.js treats .node files as native modules. When you
call:
const nativeAddon = require('./build/Release/addon.node');Node.js internally uses the operating system’s dynamic linker (such
as dlopen on macOS/Linux or LoadLibrary on
Windows) to load the binary into the process’s memory space.
Once loaded, the exported native functions are mapped to JavaScript properties. Developers can then invoke these functions just like standard JavaScript functions, enjoying near-native execution speeds for computationally heavy tasks.