Object Wrapping
Object wrapping allows you to expose C++ classes to JavaScript as native objects. This enables object-oriented patterns and maintains state between function calls.
Why Object Wrapping?
Stateful Operations
Without wrapping (stateless):
// Every call is independent
const sum1 = addon.add(5, 3); // 8
const sum2 = addon.add(10, 2); // 12
// No state maintained
With wrapping (stateful):
// Object maintains state
const calc = new addon.Calculator(10);
calc.add(5); // 15 (10 + 5)
calc.multiply(2); // 30 (15 * 2)
console.log(calc.value); // 30
Resource Management
C++ resources (files, database connections, GPU contexts):
const db = new addon.Database('connection_string');
db.query('SELECT * FROM users');
db.close(); // Properly cleanup resources
Basic Object Wrapping
Minimal Example
calculator.cc:
#include <node.h>
#include <node_object_wrap.h>
namespace demo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::String;
using v8::Value;
class Calculator : public node::ObjectWrap {
public:
static void Init(Isolate* isolate) {
// Prepare constructor template
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "Calculator").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Prototype methods
NODE_SET_PROTOTYPE_METHOD(tpl, "add", Add);
NODE_SET_PROTOTYPE_METHOD(tpl, "subtract", Subtract);
NODE_SET_PROTOTYPE_METHOD(tpl, "multiply", Multiply);
NODE_SET_PROTOTYPE_METHOD(tpl, "getValue", GetValue);
// Store constructor
constructor.Reset(isolate, tpl->GetFunction(isolate->GetCurrentContext())
.ToLocalChecked());
}
static void NewInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance = cons->NewInstance(context, argc, argv)
.ToLocalChecked();
args.GetReturnValue().Set(instance);
}
private:
explicit Calculator(double value = 0) : value_(value) {}
~Calculator() {}
// Constructor callback
static void New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
// Invoked as constructor: `new Calculator(...)`
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
Calculator* obj = new Calculator(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
} else {
// Invoked as plain function: `Calculator(...)`, turn into construct call
const int argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance = cons->NewInstance(context, argc, argv)
.ToLocalChecked();
args.GetReturnValue().Set(instance);
}
}
// Methods
static void Add(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(args.Holder());
obj->value_ += args[0]->NumberValue(context).FromMaybe(0);
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
static void Subtract(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(args.Holder());
obj->value_ -= args[0]->NumberValue(context).FromMaybe(0);
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
static void Multiply(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(args.Holder());
obj->value_ *= args[0]->NumberValue(context).FromMaybe(0);
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
static void GetValue(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(args.Holder());
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
static v8::Persistent<Function> constructor;
double value_;
};
v8::Persistent<Function> Calculator::constructor;
void Initialize(Local<Object> exports, Local<Object> module) {
Isolate* isolate = Isolate::GetCurrent();
Calculator::Init(isolate);
NODE_SET_METHOD(module, "exports", Calculator::NewInstance);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage:
const Calculator = require('./build/Release/addon');
const calc = new Calculator(10);
console.log(calc.getValue()); // 10
calc.add(5);
console.log(calc.getValue()); // 15
calc.multiply(2);
console.log(calc.getValue()); // 30
calc.subtract(10);
console.log(calc.getValue()); // 20
Key concepts:
1. Inherit from ObjectWrap:
class Calculator : public node::ObjectWrap {
Provides automatic memory management.
2. Wrap C++ object:
Calculator* obj = new Calculator(value);
obj->Wrap(args.This()); // Bind to JavaScript object
3. Unwrap in methods:
Calculator* obj = ObjectWrap::Unwrap<Calculator>(args.Holder());
obj->value_ += x; // Access C++ member
4. Destructor called automatically:
~Calculator() {
// Cleanup resources
// Called when JavaScript object is garbage collected
}
Properties (Getters/Setters)
calculator-properties.cc:
class Calculator : public node::ObjectWrap {
public:
static void Init(Isolate* isolate) {
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "Calculator")
.ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
// Methods
NODE_SET_PROTOTYPE_METHOD(tpl, "add", Add);
// Properties (getter/setter)
Local<ObjectTemplate> itpl = tpl->InstanceTemplate();
itpl->SetAccessor(
String::NewFromUtf8(isolate, "value").ToLocalChecked(),
GetValue,
SetValue
);
constructor.Reset(isolate, tpl->GetFunction(
isolate->GetCurrentContext()).ToLocalChecked());
}
private:
// Property getter
static void GetValue(Local<String> property,
const PropertyCallbackInfo<Value>& info) {
Isolate* isolate = info.GetIsolate();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(info.Holder());
info.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
// Property setter
static void SetValue(Local<String> property,
Local<Value> value,
const PropertyCallbackInfo<void>& info) {
Isolate* isolate = info.GetIsolate();
Calculator* obj = ObjectWrap::Unwrap<Calculator>(info.Holder());
Local<Context> context = isolate->GetCurrentContext();
obj->value_ = value->NumberValue(context).FromMaybe(0);
}
// ... rest of implementation
};
JavaScript usage:
const calc = new Calculator(10);
// Getter
console.log(calc.value); // 10
// Setter
calc.value = 50;
console.log(calc.value); // 50
calc.add(10);
console.log(calc.value); // 60
Factory Pattern
Create objects from factory functions:
factory.cc:
class MyObject : public node::ObjectWrap {
public:
static void Init(Isolate* isolate) {
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "MyObject")
.ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(tpl, "getValue", GetValue);
constructor.Reset(isolate, tpl->GetFunction(
isolate->GetCurrentContext()).ToLocalChecked());
}
static void CreateInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance = cons->NewInstance(context, argc, argv)
.ToLocalChecked();
args.GetReturnValue().Set(instance);
}
private:
explicit MyObject(double value = 0) : value_(value) {}
~MyObject() {}
static void New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
if (args.IsConstructCall()) {
double value = args[0]->IsUndefined() ?
0 : args[0]->NumberValue(context).FromMaybe(0);
MyObject* obj = new MyObject(value);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
}
}
static void GetValue(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
MyObject* obj = ObjectWrap::Unwrap<MyObject>(args.Holder());
args.GetReturnValue().Set(Number::New(isolate, obj->value_));
}
static v8::Persistent<Function> constructor;
double value_;
};
v8::Persistent<Function> MyObject::constructor;
void Initialize(Local<Object> exports) {
MyObject::Init(Isolate::GetCurrent());
NODE_SET_METHOD(exports, "createObject", MyObject::CreateInstance);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
JavaScript usage:
const addon = require('./build/Release/addon');
// Factory function (not constructor)
const obj = addon.createObject(42);
console.log(obj.getValue()); // 42
Passing Wrapped Objects
Pass C++ objects between functions:
pass-objects.cc:
class MyObject : public node::ObjectWrap {
// ... Init and New ...
public:
double GetValue() const { return value_; }
void SetValue(double value) { value_ = value; }
private:
double value_;
};
// Function that accepts wrapped object
void ProcessObject(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// Unwrap the object
MyObject* obj = node::ObjectWrap::Unwrap<MyObject>(args[0].As<Object>());
// Use it
double value = obj->GetValue();
obj->SetValue(value * 2);
args.GetReturnValue().Set(Number::New(isolate, obj->GetValue()));
}
void Initialize(Local<Object> exports) {
MyObject::Init(Isolate::GetCurrent());
NODE_SET_METHOD(exports, "createObject", MyObject::CreateInstance);
NODE_SET_METHOD(exports, "processObject", ProcessObject);
}
JavaScript usage:
const obj = addon.createObject(10);
console.log(obj.getValue()); // 10
const result = addon.processObject(obj);
console.log(result); // 20
console.log(obj.getValue()); // 20 (modified)
Resource Management
Proper cleanup of system resources:
file-wrapper.cc:
#include <node.h>
#include <node_object_wrap.h>
#include <fstream>
namespace demo {
class FileWrapper : public node::ObjectWrap {
public:
static void Init(Isolate* isolate) {
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, New);
tpl->SetClassName(String::NewFromUtf8(isolate, "File").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
NODE_SET_PROTOTYPE_METHOD(tpl, "write", Write);
NODE_SET_PROTOTYPE_METHOD(tpl, "close", Close);
constructor.Reset(isolate, tpl->GetFunction(
isolate->GetCurrentContext()).ToLocalChecked());
}
static void CreateInstance(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
const unsigned argc = 1;
Local<Value> argv[argc] = { args[0] };
Local<Function> cons = Local<Function>::New(isolate, constructor);
Local<Object> instance = cons->NewInstance(context, argc, argv)
.ToLocalChecked();
args.GetReturnValue().Set(instance);
}
private:
explicit FileWrapper(const std::string& filename) {
file_.open(filename, std::ios::out);
}
~FileWrapper() {
// Automatic cleanup when object is garbage collected
if (file_.is_open()) {
file_.close();
}
}
static void New(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
if (args.IsConstructCall()) {
String::Utf8Value filename(isolate, args[0]);
FileWrapper* obj = new FileWrapper(*filename);
obj->Wrap(args.This());
args.GetReturnValue().Set(args.This());
}
}
static void Write(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
FileWrapper* obj = ObjectWrap::Unwrap<FileWrapper>(args.Holder());
if (!obj->file_.is_open()) {
isolate->ThrowException(v8::Exception::Error(
String::NewFromUtf8(isolate, "File is closed").ToLocalChecked()));
return;
}
String::Utf8Value data(isolate, args[0]);
obj->file_ << *data;
obj->file_.flush();
}
static void Close(const FunctionCallbackInfo<Value>& args) {
FileWrapper* obj = ObjectWrap::Unwrap<FileWrapper>(args.Holder());
if (obj->file_.is_open()) {
obj->file_.close();
}
}
static v8::Persistent<Function> constructor;
std::ofstream file_;
};
v8::Persistent<Function> FileWrapper::constructor;
void Initialize(Local<Object> exports) {
FileWrapper::Init(Isolate::GetCurrent());
NODE_SET_METHOD(exports, "openFile", FileWrapper::CreateInstance);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
JavaScript usage:
const file = addon.openFile('output.txt');
file.write('Hello, World!\n');
file.write('Second line\n');
file.close();
// Or let garbage collector close it automatically
Summary
Object wrapping enables object-oriented C++ in JavaScript:
Key Patterns:
- Inherit from
node::ObjectWrap - Use
Wrap()to bind C++ object to JavaScript object - Use
Unwrap()to access C++ object in methods - Destructor called automatically on garbage collection
- Factory functions for creating instances
Best Practices:
- ✓ Always set internal field count:
SetInternalFieldCount(1) - ✓ Use
Persistent<Function>for constructor - ✓ Cleanup resources in destructor
- ✓ Check object state in methods (file open, connection alive)
- ✓ Use properties (getters/setters) for natural JavaScript API
When to Use Object Wrapping:
- Stateful operations (calculators, games)
- Resource management (files, databases, sockets)
- Complex data structures (trees, graphs)
- Hardware interfaces (cameras, sensors)
Next: Memory management and optimization