Beginners guide to writing NodeJS Addons using C++ and N-API (node-addon-api)
Node.js Addons are dynamically-linked shared objects written in C++ that can be loaded via require() and used as ordinary Node.js modules. They primarily interface between JavaScript and C/C++ libraries.
Common reasons to write addons:
- Accessing native APIs difficult to implement in JavaScript alone
- Integrating third-party C/C++ libraries
- Rewriting performance-critical modules in C++
What is N-API?
N-API (Native API) is an ABI-stable API for building native addons independent of the underlying JavaScript runtime (like V8). It ensures addons compiled for one Node.js version work on later versions without recompilation. N-API became stable in Node v10.
The node-addon-api package provides C++ wrapper classes for N-API, offering C++ object models and exception handling with minimal overhead.
Setting Up: Boilerplate Configuration
Project Initialization
mkdir test-addon
cd test-addon
git init
npm init
Installing Dependencies
npm install node-gyp --save-dev
npm install node-addon-api
node-gyp compiles the addons. node-addon-api simplifies C++ addon development.
Core Configuration Files
.gitignore
node_modules
*.log
build
binding.gyp
{
"targets": [{
"target_name": "testaddon",
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
"sources": [
"cppsrc/main.cpp"
],
'include_dirs': [
"<!@(node -p \"require('node-addon-api').include\")"
],
'libraries': [],
'dependencies': [
"<!(node -p \"require('node-addon-api').gyp\")"
],
'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
}]
}
package.json
{
"name": "test-addon",
"version": "1.0.0",
"main": "index.js",
"gypfile": true,
"scripts": {
"build": "node-gyp rebuild",
"clean": "node-gyp clean"
},
"devDependencies": {
"node-gyp": "^3.7.0"
},
"dependencies": {
"node-addon-api": "^1.3.0"
}
}
cppsrc/main.cpp
#include <napi.h>
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return exports;
}
NODE_API_MODULE(testaddon, InitAll)
index.js
const testAddon = require('./build/Release/testaddon.node')
module.exports = testAddon
Build with npm run build.
Understanding the Boilerplate
#include <napi.h>includes the N-API headerNODE_API_MODULEregisters the module entry point - parameters are module name and init functionInitAllreceivesenv(JavaScript runtime context) andexports(object for exporting functions/classes)
Exporting C++ Functions
Simple Function Export
Create cppsrc/Samples/functionexample.h:
#include <napi.h>
namespace functionexample {
std::string hello();
Napi::String HelloWrapped(const Napi::CallbackInfo& info);
Napi::Object Init(Napi::Env env, Napi::Object exports);
}
Create cppsrc/Samples/functionexample.cpp:
#include "functionexample.h"
std::string functionexample::hello(){
return "Hello World";
}
Napi::String functionexample::HelloWrapped(const Napi::CallbackInfo& info)
{
Napi::Env env = info.Env();
Napi::String returnValue = Napi::String::New(env, functionexample::hello());
return returnValue;
}
Napi::Object functionexample::Init(Napi::Env env, Napi::Object exports)
{
exports.Set(
"hello", Napi::Function::New(env, functionexample::HelloWrapped)
);
return exports;
}
Key concepts:
- Every exported function needs a wrapped version (e.g.,
HelloWrapped) - Wrapped functions take
CallbackInfocontaining context and parameters - The
Initfunction registers the wrapper with the exports object
Update main.cpp:
#include <napi.h>
#include "Samples/functionexample.h"
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return functionexample::Init(env, exports);
}
NODE_API_MODULE(testaddon, InitAll)
Update index.js:
const testAddon = require('./build/Release/testaddon.node');
console.log('addon', testAddon);
console.log(testAddon.hello());
module.exports = testAddon;
After rebuilding: node index.js outputs:
addon { hello: [Function] }
Hello World
Functions with Parameters
Add an add function to functionexample.cpp:
int functionexample::add(int a, int b){
return a + b;
}
Napi::Number functionexample::AddWrapped(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
}
Napi::Number first = info[0].As<Napi::Number>();
Napi::Number second = info[1].As<Napi::Number>();
int returnValue = functionexample::add(first.Int32Value(), second.Int32Value());
return Napi::Number::New(env, returnValue);
}
Test with:
console.log('add ', testAddon.add(5, 10));
// Output: add 15
Exporting C++ Classes
Basic Class Structure
Create cppsrc/Samples/actualclass.h:
class ActualClass {
public:
ActualClass(double value);
double getValue();
double add(double toAdd);
private:
double value_;
};
Create cppsrc/Samples/actualclass.cpp:
#include "actualclass.h"
ActualClass::ActualClass(double value){
this->value_ = value;
}
double ActualClass::getValue()
{
return this->value_;
}
double ActualClass::add(double toAdd)
{
this->value_ += toAdd;
return this->value_;
}
Creating the Wrapper Class
Create cppsrc/Samples/classexample.h:
#include <napi.h>
#include "actualclass.h"
class ClassExample : public Napi::ObjectWrap<ClassExample> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
ClassExample(const Napi::CallbackInfo& info);
private:
static Napi::FunctionReference constructor;
Napi::Value GetValue(const Napi::CallbackInfo& info);
Napi::Value Add(const Napi::CallbackInfo& info);
ActualClass *actualClass_;
};
Create cppsrc/Samples/classexample.cpp:
#include "classexample.h"
Napi::FunctionReference ClassExample::constructor;
Napi::Object ClassExample::Init(Napi::Env env, Napi::Object exports) {
Napi::HandleScope scope(env);
Napi::Function func = DefineClass(env, "ClassExample", {
InstanceMethod("add", &ClassExample::Add),
InstanceMethod("getValue", &ClassExample::GetValue),
});
constructor = Napi::Persistent(func);
constructor.SuppressDestruct();
exports.Set("ClassExample", func);
return exports;
}
ClassExample::ClassExample(const Napi::CallbackInfo& info) : Napi::ObjectWrap<ClassExample>(info) {
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
int length = info.Length();
if (length != 1 || !info[0].IsNumber()) {
Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
}
Napi::Number value = info[0].As<Napi::Number>();
this->actualClass_ = new ActualClass(value.DoubleValue());
}
Napi::Value ClassExample::GetValue(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
double num = this->actualClass_->getValue();
return Napi::Number::New(env, num);
}
Napi::Value ClassExample::Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
if (info.Length() != 1 || !info[0].IsNumber()) {
Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
}
Napi::Number toAdd = info[0].As<Napi::Number>();
double answer = this->actualClass_->add(toAdd.DoubleValue());
return Napi::Number::New(info.Env(), answer);
}
Key concepts:
- Wrapper classes extend
Napi::ObjectWrap<ClassName> DefineClasscreates the class exported to JavaScriptInstanceMethodregisters member functionsNapi::FunctionReferencestores the class constructor
Test:
const classInstance = new testAddon.ClassExample(4.3);
console.log('Testing class initial value:', classInstance.getValue());
console.log('After adding 3.3:', classInstance.add(3.3));
// Testing class initial value: 4.3
// After adding 3.3: 7.6
Passing Complex Objects Between Worlds
To pass an existing class instance as a constructor argument, use Napi::ObjectWrap::Unwrap():
ClassExample::ClassExample(const Napi::CallbackInfo& info) : Napi::ObjectWrap<ClassExample>(info) {
Napi::Env env = info.Env();
Napi::HandleScope scope(env);
int length = info.Length();
if (length != 1) {
Napi::TypeError::New(env, "Only one argument expected").ThrowAsJavaScriptException();
}
if(!info[0].IsNumber()){
Napi::Object object_parent = info[0].As<Napi::Object>();
ClassExample* example_parent = Napi::ObjectWrap<ClassExample>::Unwrap(object_parent);
ActualClass* parent_actual_class_instance = example_parent->GetInternalInstance();
this->actualClass_ = new ActualClass(parent_actual_class_instance->getValue());
return;
}
Napi::Number value = info[0].As<Napi::Number>();
this->actualClass_ = new ActualClass(value.DoubleValue());
}
ActualClass* ClassExample::GetInternalInstance() {
return this->actualClass_;
}
Test:
const prevInstance = new testAddon.ClassExample(4.3);
console.log('Initial value:', prevInstance.getValue());
console.log('After adding 3.3:', prevInstance.add(3.3));
const newFromExisting = new testAddon.ClassExample(prevInstance);
console.log('Value of derived instance:', newFromExisting.getValue());
// Initial value: 4.3
// After adding 3.3: 7.6
// Value of derived instance: 7.6
Summary
N-API provides a stable interface for building Node.js addons in C++. The workflow:
- Write C++ implementation
- Create N-API wrapper functions/classes
- Register exports via
Init - Update
binding.gypwith source files - Rebuild with
npm run build