NodeJSC/C++ Addons

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

CPP
// 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)

TS
// 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)   // 11
ObjectWrap binds a C++ class instance to a JavaScript object — the C++ object lives as long as the JavaScript object; GC of the JS object destroys the C++ object
`Napi::ObjectWrap<T>` manages the lifetime relationship between the C++ object and its JavaScript wrapper. When the JavaScript object is garbage collected, the C++ destructor is called. This means the native object persists exactly as long as the JavaScript object is reachable. If you hold a raw pointer to the C++ object outside of the ObjectWrap, ensure the JavaScript object is also kept alive (e.g. stored in a module-level variable) — otherwise the C++ object can be destroyed while you still hold a pointer to it, causing a use-after-free.
Zero-copy Buffer exchange

CPP
// 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; });
}
The pointer passed to Napi::Buffer::New must remain valid until the finalizer runs — never stack-allocate or delete it before the GC collects the Buffer
When you create a `Napi::Buffer::New` backed by a raw pointer, V8 does NOT copy the memory — it uses your pointer directly as the buffer's backing store. The memory must remain valid until the GC collects the JavaScript `Buffer` object and runs the finalizer you provide. If you `delete[]` the array before that, the JavaScript code ends up with a dangling pointer and reading the buffer causes undefined behavior (corrupt data or a crash). The finalizer lambda is where you `delete[]` or `free()` the memory. For `malloc`-allocated memory use `free`; for `new[]` use `delete[]`.
TypedArray for numerical data exchange

CPP
// 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

CPP
// 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

Bash
# 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 AsyncWorker for background work.

  • Missing Napi::HandleScope — always create a HandleScope in callbacks that create Napi values; without it, V8 cannot track handle lifetimes.

  • Using nan instead 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() and info[N].Is<Type>() before casting.

  • Leaking handles — persistent handles (Napi::Persistent) that are never Reset() keep objects alive forever; treat them like manual memory.

  • Shipping without prebuilt binaries — see Native Addons for the prebuildify workflow.

Next
An alternative to native addons that avoids the N-API layer entirely: [WebAssembly in Node.js](/nodejs/wasm).