Lesson 32-Node.js Custom Extensions

Node.js Native Extension Development Basics

Overview of Extension Development Architecture

Node.js native extensions enable developers to write high-performance modules in C/C++ that interact with JavaScript. The core architecture consists of three key components:

  1. V8 Engine Integration: Handles conversion of JavaScript objects and functions
  2. libuv Event Loop: Manages asynchronous I/O operations
  3. Node.js API Layer: Provides high-level abstraction interfaces

Typical Extension Module Structure:

my-addon/
├── binding.gyp          # Build configuration file
├── src/
   ├── addon.cc         # Main extension code
   └── utils.h          # Utility functions
├── package.json
└── test/
    └── test.js          # Test script

Build System Configuration

binding.gyp Example:

{
  "targets": [
    {
      "target_name": "my-addon",
      "sources": ["src/addon.cc"],
      "include_dirs": ["<!(node -e \"require('node-addon-api').include\")"],
      "dependencies": ["<!(node -e \"require('node-addon-api').gyp\")"],
      "cflags_cc": ["-std=c++17"],
      "conditions": [
        ["OS=='win'", {"defines": ["OS_WIN"]}],
        ["OS=='mac'", {"defines": ["OS_MAC"]}]
      ]
    }
  ]
}

Build Commands:

# Install node-gyp
npm install -g node-gyp

# Configure build environment
node-gyp configure

# Build extension
node-gyp build

# Rebuild (for development)
node-gyp rebuild

Core Functionality Implementation

JavaScript and C++ Interaction

Basic Type Conversion Example:

#include <napi.h>

// JavaScript number -> C++ int
Napi::Number Add(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  // Parameter validation
  if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
    Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
    return env.Null();
  }

  // Type conversion
  double arg0 = info[0].As<Napi::Number>().DoubleValue();
  double arg1 = info[1].As<Napi::Number>().DoubleValue();

  // Compute result
  double result = arg0 + arg1;

  // Return JavaScript number
  return Napi::Number::New(env, result);
}

// Register module
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "add"), 
             Napi::Function::New(env, Add));
  return exports;
}

NODE_API_MODULE(myaddon, Init)

Complex Object Interaction:

#include <napi.h>
#include <vector>

// JavaScript array -> C++ vector
Napi::Array ProcessArray(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  if (info.Length() < 1 || !info[0].IsArray()) {
    Napi::TypeError::New(env, "Array expected").ThrowAsJavaScriptException();
    return env.Null();
  }

  Napi::Array jsArray = info[0].As<Napi::Array>();
  uint32_t length = jsArray.Length();
  std::vector<int> cppVector(length);

  // Read data from JS array
  for (uint32_t i = 0; i < length; i++) {
    Napi::Value item = jsArray[i];
    if (!item.IsNumber()) {
      Napi::TypeError::New(env, "Array elements must be numbers")
        .ThrowAsJavaScriptException();
      return env.Null();
    }
    cppVector[i] = item.As<Napi::Number>().Int32Value();
  }

  // Process data (example: multiply each element by 2)
  for (auto& item : cppVector) {
    item *= 2;
  }

  // Create result JS array
  Napi::Array result = Napi::Array::New(env, length);
  for (uint32_t i = 0; i < length; i++) {
    result[i] = Napi::Number::New(env, cppVector[i]);
  }

  return result;
}

Asynchronous Operation Implementation

Asynchronous Work with libuv Example:

#include <napi.h>
#include <uv.h>

struct AsyncData {
  Napi::FunctionReference callback;
  int input;
  int result;
};

// Async work thread execution function
void AsyncWork(uv_work_t* req) {
  AsyncData* data = static_cast<AsyncData*>(req->data);

  // Simulate time-consuming computation
  data->result = data->input * 2;

  // Perform real I/O or blocking tasks here
}

// Callback after async work completes
void AsyncAfter(uv_work_t* req, int status) {
  Napi::Env env = Env();
  AsyncData* data = static_cast<AsyncData*>(req->data);

  // Prepare to call JavaScript callback
  Napi::HandleScope scope(env);
  Napi::Value argv[1] = { Napi::Number::New(env, data->result) };

  // Call callback function
  data->callback.Call(env.Global(), 1, argv);

  // Clean up resources
  delete data;
  delete req;
}

// Async method exposed to JavaScript
Napi::Value AsyncAdd(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsFunction()) {
    Napi::TypeError::New(env, "Number and callback expected")
      .ThrowAsJavaScriptException();
    return env.Null();
  }

  int input = info[0].As<Napi::Number>().Int32Value();
  Napi::Function callback = info[1].As<Napi::Function>();

  // Create async data
  AsyncData* data = new AsyncData;
  data->input = input;
  data->callback = Napi::Persistent(callback);

  // Create uv_work_t request
  uv_work_t* req = new uv_work_t;
  req->data = data;

  // Queue work in libuv
  uv_queue_work(uv_default_loop(), req, AsyncWork, AsyncAfter);

  // Return undefined (async operation)
  return env.Undefined();
}

Promise-Based Asynchronous Implementation:

#include <napi.h>

struct PromiseData {
  Napi::Promise::Deferred deferred;
  int input;
  int result;
};

// Async work thread function
void PromiseWork(uv_work_t* req) {
  PromiseData* data = static_cast<PromiseData*>(req->data);
  data->result = data->input * 3; // Simulate computation
}

// Callback after async completion
void PromiseAfter(uv_work_t* req, int status) {
  Napi::Env env = Env();
  PromiseData* data = static_cast<PromiseData*>(req->data);

  if (status == 0) {
    // Resolve Promise successfully
    data->deferred.Resolve(Napi::Number::New(env, data->result));
  } else {
    // Reject Promise
    data->deferred.Reject(Napi::Error::New(env, "Async operation failed").Value());
  }

  delete data;
  delete req;
}

// Promise method exposed to JavaScript
Napi::Value PromiseAdd(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  if (info.Length() < 1 || !info[0].IsNumber()) {
    Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException();
    return env.Null();
  }

  int input = info[0].As<Napi::Number>().Int32Value();

  // Create Promise
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);

  // Prepare async data
  PromiseData* data = new PromiseData;
  data->deferred = deferred;
  data->input = input;

  // Create work request
  uv_work_t* req = new uv_work_t;
  req->data = data;

  // Queue work
  uv_queue_work(uv_default_loop(), req, PromiseWork, PromiseAfter);

  // Return Promise object
  return deferred.Promise();
}

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here

Share your love