C/C++ Addons
This page goes deeper into practical C/C++ addon patterns that go beyond the basic "wrap a function" example: wrapping a C++ class as a JavaScript object (using Napi::ObjectWrap), exposing synchronous and asynchronous variants of the same operation, handling Buffers and typed arrays for zero-copy data exchange, and the complete build and test workflow. These patterns cover the scenarios you'll actually encounter when wrapping a real C++ library for use in Node.
Wrapping a C++ class with ObjectWrap
// src/counter.cc — expose a C++ Counter class as a JavaScript object
#include <napi.h>
class Counter : public Napi::ObjectWrap<Counter> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "Counter", {
InstanceMethod("increment", &Counter::Increment),
InstanceMethod("decrement", &Counter::Decrement),
InstanceAccessor("value", &Counter::GetValue, nullptr),
});
exports.Set("Counter", func);
return exports;
}
Counter(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<Counter>(info), value_(0) {
if (info.Length() > 0 && info[0].IsNumber()) {
value_ = info[0].As<Napi::Number>().Int32Value();
}
}
private:
int32_t value_;
Napi::Value Increment(const Napi::CallbackInfo& info) {
value_++;
return Napi::Number::New(info.Env(), value_);
}
Napi::Value Decrement(const Napi::CallbackInfo& info) {
value_--;
return Napi::Number::New(info.Env(), value_);
}
Napi::Value GetValue(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), value_);
}
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
return Counter::Init(env, exports);
}
NODE_API_MODULE(counter, Init)// JavaScript usage:
const { Counter } = require('./build/Release/counter')
const c = new Counter(10)
c.increment() // 11
c.increment() // 12
c.decrement() // 11
console.log(c.value) // 11Zero-copy Buffer exchange
// Read a Node.js Buffer directly without copying:
Napi::Value ProcessBuffer(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (!info[0].IsBuffer()) {
Napi::TypeError::New(env, "Buffer expected").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
uint8_t* data = buf.Data(); // pointer to the Buffer's backing memory — NO copy
size_t length = buf.Length()
// Process data in-place or compute a result:
uint64_t checksum = 0;
for (size_t i = 0; i < length; ++i) checksum += data[i];
return Napi::Number::New(env, static_cast<double>(checksum));
}
// Return a new Buffer backed by a malloc'd array (zero-copy from C++ to JS):
Napi::Value GenerateBuffer(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
size_t size = 1024;
uint8_t* data = new uint8_t[size];
// fill data with something...
// Napi::Buffer::New takes ownership and frees data when the Buffer is GC'd:
return Napi::Buffer<uint8_t>::New(env, data, size,
[](Napi::Env, uint8_t* ptr) { delete[] ptr; });
}TypedArray for numerical data exchange
// Accept and return Float64Array for numerical arrays:
Napi::Value ScaleArray(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Float64Array input = info[0].As<Napi::Float64Array>();
double factor = info[1].As<Napi::Number>().DoubleValue();
size_t length = input.ElementLength();
// Create output TypedArray backed by a new ArrayBuffer:
Napi::ArrayBuffer outBuf = Napi::ArrayBuffer::New(env, length * sizeof(double));
Napi::Float64Array output = Napi::Float64Array::New(env, length, outBuf, 0);
for (size_t i = 0; i < length; ++i) {
output[i] = input[i] * factor;
}
return output;
}NAPI_DISABLE_CPP_EXCEPTIONS vs exception handling
// Option 1: NAPI_DISABLE_CPP_EXCEPTIONS (binding.gyp defines this)
// Napi::TypeError::New(...).ThrowAsJavaScriptException(); return env.Null();
// Errors must be thrown manually; C++ exceptions become no-ops.
// Option 2: Allow C++ exceptions (remove NAPI_DISABLE_CPP_EXCEPTIONS)
// Napi errors are thrown as actual C++ exceptions you can catch:
Napi::Value MyFunc(const Napi::CallbackInfo& info) {
try {
if (!info[0].IsString()) throw Napi::TypeError::New(info.Env(), "String expected");
// ...
} catch (const Napi::Error& e) {
e.ThrowAsJavaScriptException();
return info.Env().Null();
}
return info.Env().Undefined();
}Build and test workflow
# Build the addon: npm run build # runs node-gyp rebuild # Output: # build/Release/my_addon.node ← the compiled binary # Run tests (load the addon like any module): npx jest --testPathPattern addon.test # Rebuild when C++ source changes (no --watch support — manual): node-gyp build # faster incremental build (vs rebuild which cleans first) # Debug build (includes debug symbols, no optimization): node-gyp build --debug # Output: build/Debug/my_addon.node
Common pitfalls
Calling V8 from a background thread — only call N-API from the main JS thread; use
AsyncWorkerfor background work.Missing Napi::HandleScope — always create a
HandleScopein callbacks that create Napi values; without it, V8 cannot track handle lifetimes.Using
naninstead of N-API — nan compiles against a specific V8 version; the addon breaks on Node upgrades. Migrate to N-API.Not validating arguments — JavaScript callers can pass any type; always check
info.Length()andinfo[N].Is<Type>()before casting.Leaking handles — persistent handles (
Napi::Persistent) that are neverReset()keep objects alive forever; treat them like manual memory.Shipping without prebuilt binaries — see Native Addons for the prebuildify workflow.