Skip to main content

Architecture Overview

Node.js is built on top of V8 (JavaScript engine) and libuv (event loop), with a C++ core that bridges JavaScript and native functionality.

V8 Engine

JavaScript execution and memory management

libuv

Event loop and async I/O

C++ Core

Bindings and internal APIs

Core Components

Isolate

The v8::Isolate represents a single JavaScript engine instance with its own heap:
// Accessing the current isolate
Isolate* isolate = Isolate::GetCurrent();

// Given a binding function
void MyFunction(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
}

// Given an Environment
Isolate* isolate = env->isolate();
V8 APIs are not thread-safe unless explicitly specified. In a typical application, the main thread and each Worker thread have one Isolate.

Environment

The Environment class represents a Node.js instance:
// Each Environment is associated with:
// - One event loop (uv_loop_t)
// - One Isolate (v8::Isolate)
// - One principal Realm

// Accessing current Environment
Environment* env = Environment::GetCurrent(context);
Environment* env = Environment::GetCurrent(isolate);
Environment* env = Environment::GetCurrent(args);

Realm

The Realm class is a container for JavaScript objects and functions:
// Each Realm has:
// - A global object
// - A set of intrinsic objects  
// - An associated v8::Context

Realm* realm = Realm::GetCurrent(args);
Realm* realm = Realm::GetCurrent(context);

File Structure

Node.js C++ files follow a consistent structure:
Declaration files
  • Contain class and function declarations
  • Should only include other .h files
  • Keep interface clean and readable

Key Directories

src/
├── node.h                 # Main Node.js embedder API
├── node.cc                # Core Node.js implementation
├── api/                   # Public C++ APIs
├── async_wrap.{h,cc}      # Async tracking
├── base_object.{h,cc}     # Base class for JS-backed objects
├── env.{h,cc}             # Environment implementation
├── node_binding.{h,cc}    # Binding system
├── node_buffer.{h,cc}     # Buffer implementation
├── node_file.{h,cc}       # File system operations
├── node_http_parser.cc    # HTTP parser binding
└── ...

JavaScript Value Handles

V8 uses handles to access JavaScript objects safely:

Local Handles

Temporary pointers valid only in current function scope:
void GetFoo(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  EscapableHandleScope handle_scope(isolate);
  
  Local<Context> context = isolate->GetCurrentContext();
  Local<Object> obj = args[0].As<Object>();
  
  Local<String> foo_string = 
      String::NewFromUtf8(isolate, "foo").ToLocalChecked();
  
  Local<Value> return_value;
  if (obj->Get(context, foo_string).ToLocal(&return_value)) {
    args.GetReturnValue().Set(handle_scope.Escape(return_value));
  }
}
Local handles (v8::Local<T>) should never be allocated on the heap. Use v8::LocalVector<T> for heap-allocated collections.

Global Handles

Persistent references that survive across function calls:
v8::Global<v8::Object> reference;

void StoreReference(Isolate* isolate, Local<Object> obj) {
  // Create a strong reference to obj
  reference.Reset(isolate, obj);
}

Local<Object> LoadReference(Isolate* isolate) {
  // Must be called with a HandleScope
  return reference.Get(isolate);
}

Binding Functions

C++ functions exposed to JavaScript follow a specific signature:
void MyBinding(const FunctionCallbackInfo<Value>& args) {
  // Access arguments
  CHECK(args[0]->IsString());
  Local<String> arg0 = args[0].As<String>();
  
  // Access this value
  Local<Object> self = args.This();
  
  // Set return value
  args.GetReturnValue().Set(42);
}

Registering Bindings

void Initialize(Local<Object> target,
                Local<Value> unused,
                Local<Context> context,
                void* priv) {
  Environment* env = Environment::GetCurrent(context);
  Isolate* isolate = env->isolate();
  
  // Add methods to exports
  SetMethod(context, target, "myFunction", MyBinding);
  
  // Create a class
  Local<FunctionTemplate> tmpl = 
      NewFunctionTemplate(isolate, MyClass::New);
  tmpl->InstanceTemplate()->SetInternalFieldCount(1);
  
  // Add prototype methods
  SetProtoMethod(isolate, tmpl, "method", MyClass::Method);
  
  SetConstructorFunction(context, target, "MyClass", tmpl);
}

NODE_BINDING_CONTEXT_AWARE_INTERNAL(my_module, Initialize)

BaseObject Pattern

Most C++ objects associated with JavaScript objects inherit from BaseObject:
class MyObject : public BaseObject {
 public:
  MyObject(Environment* env, Local<Object> object)
      : BaseObject(env, object) {
    MakeWeak();
  }
  
  static void New(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    new MyObject(env, args.This());
  }
  
  static void Method(const FunctionCallbackInfo<Value>& args) {
    MyObject* obj;
    ASSIGN_OR_RETURN_UNWRAP(&obj, args.This());
    // Use obj...
  }
  
 private:
  // Object state
  int counter_ = 0;
};
BaseObject provides automatic lifetime management, memory tracking, and integration with Node.js cleanup hooks.

AsyncWrap for Async Tracking

AsyncWrap enables async_hooks tracking:
class MyAsyncResource : public AsyncWrap {
 public:
  MyAsyncResource(Environment* env, Local<Object> object)
      : AsyncWrap(env, object, AsyncWrap::PROVIDER_MYTYPE) {
    MakeWeak();
  }
  
  void DoAsyncWork() {
    // Make callback into JavaScript
    HandleScope handle_scope(env()->isolate());
    Context::Scope context_scope(env()->context());
    
    Local<Value> argv[] = { /* ... */ };
    MakeCallback(env()->oncomplete_string(), 
                 arraysize(argv), argv);
  }
};

HandleWrap and ReqWrap

Wrappers for libuv handles and requests:

HandleWrap

class TCPWrap : public HandleWrap {
 public:
  static void New(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    new TCPWrap(env, args.This());
  }
  
  TCPWrap(Environment* env, Local<Object> object)
      : HandleWrap(env,
                   object,
                   reinterpret_cast<uv_handle_t*>(&handle_),
                   AsyncWrap::PROVIDER_TCPWRAP) {
    int r = uv_tcp_init(env->event_loop(), &handle_);
    CHECK_EQ(r, 0);
  }
  
 private:
  uv_tcp_t handle_;
};

ReqWrap

class WriteWrap : public ReqWrap<uv_write_t> {
 public:
  WriteWrap(Environment* env, Local<Object> object)
      : ReqWrap(env, object, AsyncWrap::PROVIDER_WRITEWRAP) {}
  
  static void New(const FunctionCallbackInfo<Value>& args) {
    Environment* env = Environment::GetCurrent(args);
    new WriteWrap(env, args.This());
  }
};

Internal Fields

V8 objects can store pointers in internal fields:
// Set up class with internal fields
Local<FunctionTemplate> tmpl = 
    NewFunctionTemplate(isolate, Constructor);
tmpl->InstanceTemplate()->SetInternalFieldCount(1);

// Store pointer
obj->SetAlignedPointerInInternalField(0, ptr);

// Retrieve pointer
void* ptr = obj->GetAlignedPointerFromInternalField(0);

Exception Handling

V8 uses Maybe and MaybeLocal types for error handling:
Maybe<double> SumArray(Local<Context> context, 
                       Local<Array> array) {
  double sum = 0;
  
  for (uint32_t i = 0; i < array->Length(); i++) {
    Local<Value> entry;
    if (!array->Get(context, i).ToLocal(&entry)) {
      // Exception occurred, return empty Maybe
      return Nothing<double>();
    }
    
    if (entry->IsNumber()) {
      sum += entry.As<Number>()->Value();
    }
  }
  
  return Just(sum);
}
Never use .ToChecked() or .FromJust() unless you’re absolutely certain the operation cannot fail. Incorrect usage can crash Node.js.

Memory Management

Cleanup Hooks

void CleanupHook(void* arg) {
  MyData* data = static_cast<MyData*>(arg);
  delete data;
}

void Initialize(Local<Object> exports) {
  Environment* env = Environment::GetCurrent(exports);
  MyData* data = new MyData();
  env->AddCleanupHook(CleanupHook, data);
}

MemoryRetainer

class MyClass : public BaseObject,
                public MemoryRetainer {
 public:
  void MemoryInfo(MemoryTracker* tracker) const override {
    tracker->TrackField("buffer", buffer_);
    tracker->TrackField("data", data_);
  }
  
  SET_MEMORY_INFO_NAME(MyClass)
  SET_SELF_SIZE(MyClass)
  
private:
  std::string buffer_;
  std::vector<int> data_;
};

Performance Considerations

Avoid Checked Conversions

Use .To() instead of .ToChecked() to handle potential exceptions

Reuse Handle Scopes

Don’t create unnecessary HandleScope instances

Fast API Calls

Use V8 fast API calls for performance-critical paths

Memory Tracking

Implement MemoryRetainer for proper heap snapshot support

C++ Addons

Build native addons using these internals

Embedding

Embed Node.js in C++ applications

V8 Integration

Deep dive into V8 engine

Source Code

Explore Node.js C++ source