C++ Addons In Node JS

Introduction:


As a software developer, I am always looking for ways to improve the performance of my applications. One way to achieve this is by using C++ Addons in Node JS. In this article, we will explore what C++ Addons are, why they are useful in Node JS, and how to create and use them.

Getting Started:


Before we dive into C++ Addons, let’s first make sure we have all the necessary tools installed. First, let’s install Node JS, which can be found on the official Node JS website. Next, we will need to install node-gyp, which is a tool that is used to build Node.js native modules. You can install it using the following command:

npm install -g node-gyp

Now that we have our tools installed, let’s take a look at what C++ Addons are.

What are C++ Addons?


C++ Addons are libraries written in C++ that can be loaded into a Node JS application. They allow you to write performance-critical code in C++ and call it from Node JS. This is useful when you need to perform tasks that are computationally expensive, such as image processing, cryptography, or machine learning.

Why are C++ Addons useful in Node JS?


Node JS is a powerful and flexible platform for building web applications. However, it is built on top of the V8 JavaScript engine, which is designed for single-threaded, event-driven applications. While Node JS is excellent at handling I/O operations, it can struggle with CPU-bound tasks. This is where C++ Addons come in. By writing performance-critical code in C++, we can take advantage of multiple threads and optimize the use of system resources.

How to create a basic C++ Addon:


Now that we understand the basics of C++ Addons, let’s create a basic example. First, create a new directory and navigate into it from your terminal or command prompt. Next, create a file called addon.cc. This is where we will write our C++ code.

#include <node.h>

namespace demo {

  // demo::Method is a struct that holds a callback function
  struct Method {
    v8::Persistent<v8::Function> callback;
    int val;
  };

  // This is our method that we want to expose to Node JS
  void Add(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Isolate* isolate = args.GetIsolate();

    // Check the number of arguments passed
    if (args.Length() < 2) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong number of arguments")));
      return;
    }

    // Check the argument data types
    if (!args[0]->IsNumber() || !args[1]->IsNumber()) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong arguments")));
      return;
    }

    // Perform the addition
    double arg0 = args[0]->NumberValue();
    double arg1 = args[1]->NumberValue();
    double sum = arg0 + arg1;

    // Convert the sum to a V8 result
    v8::Local<v8::Number> result = v8::Number::New(isolate, sum);

    // Return the result to the callback function
    args.GetReturnValue().Set(result);
  }

  // This is our initialization function that will expose our method to Node JS
  void Init(v8::Local<v8::Object> exports) {
    NODE_SET_METHOD(exports, "add", Add);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

This example defines a single method called Add that takes two arguments and adds them together. We also define an initialization function that exposes the Add method to Node JS.

To build and use this addon, we need to create a binding.gyp file in the same directory as addon.cc. This file tells node-gyp how to build our addon.

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [ "addon.cc" ]
    }
  ]
}

Next, run the following command to build the addon:

node-gyp configure build

This will create a build directory with our compiled addon. To use it in a Node JS application, create a new file called app.js and require our addon:

const addon = require('./build/Release/addon');

console.log('Result:', addon.add(1, 2));

Run the app.js file with:

node app.js

You should see the result logged to the console: “Result: 3”.

Advanced Concepts:


Now that we have a basic understanding of how to create a C++ Addon, let’s take a look at some more advanced concepts.

Asynchronous C++ Addons:


When working with Node JS, it is important to keep in mind the event loop. Node JS is designed to be single-threaded, and so blocking the event loop with long-running synchronous operations is not recommended. For performance-intensive tasks that cannot be broken down into smaller chunks, it is best to use an Asynchronous C++ Addon.

An Asynchronous C++ Addon uses a background thread to perform the task and then sends the result back to Node JS. Here is an example of how to create an Asynchronous C++ Addon:

#include <node.h>
#include <uv.h>

namespace demo {

  // demo::Baton is a struct that holds data to be passed between threads
  struct Baton {
    uv_work_t req;
    v8::Persistent<v8::Function> callback;
    double arg1;
    double arg2;
    double result;
  };

  // This is our method that we want to expose to Node JS
  void AddAsync(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Isolate* isolate = args.GetIsolate();

    // Check the number of arguments passed
    if (args.Length() < 3) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong number of arguments")));
      return;
    }

    // Check the argument data types
    if (!args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsFunction()) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong argument types")));
      return;
    }

    // Create a new Baton object
    Baton* baton = new Baton();
    baton->req.data = baton;
    baton->callback.Reset(isolate, v8::Local<v8::Function>::Cast(args[2]));
    baton->arg1 = args[0]->NumberValue();
    baton->arg2 = args[1]->NumberValue();

    // Schedule the work on the background thread
    uv_queue_work(uv_default_loop(), &baton->req, [](uv_work_t* req) {
      Baton* baton = static_cast<Baton*>(req->data);

      // Perform the addition in the background thread
      baton->result = baton->arg1 + baton->arg2;
    }, [](uv_work_t* req, int status) {
      v8::Isolate* isolate = v8::Isolate::GetCurrent();
      v8::HandleScope handleScope(isolate);

      Baton* baton = static_cast<Baton*>(req->data);

      // Pass the result back to the callback function
      v8::Local<v8::Value> argv[] = {
        v8::Number::New(isolate, baton->result)
      };
      v8::Local<v8::Function> callback = v8::Local<v8::Function>::New(isolate, baton->callback);
      callback->Call(isolate->GetCurrentContext()->Global(), 1, argv);

      // Clean up the Baton object
      baton->callback.Reset();
      delete baton;
    });

    args.GetReturnValue().SetUndefined();
  }

  // This is our initialization function that will expose our method to Node JS
  void Init(v8::Local<v8::Object> exports) {
    NODE_SET_METHOD(exports, "addAsync", AddAsync);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

In this example, we define a new method called AddAsync that takes three arguments: two numbers and a callback function. We create a new Baton object that holds the data to be passed between threads. We then use the uv_queue_work function to schedule the work on the background thread.

When the work is complete, we pass the result back to the callback function and clean up the Baton object. To use this addon, we can call the AddAsync method in our Node JS application with the same syntax as before.

Code Example:


Here is an example of a practical application that uses a C++ Addon in Node JS. Suppose we have a web application that needs to perform real-time image processing on user-uploaded images. We can use a C++ Addon to perform the image processing, which is much faster than performing the same operation in JavaScript.

#include <node.h>
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"

namespace demo {

  // demo::Baton is a struct that holds data to be passed between threads
  struct Baton {
    uv_work_t req;
    v8::Persistent<v8::Function> callback;
    std::string imagePath;
    cv::Scalar color;
    int thickness;
  };

  // This is our method that we want to expose to Node JS
  void DrawRectangleAsync(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Isolate* isolate = args.GetIsolate();

    // Check the number of arguments passed
    if (args.Length() < 4) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong number of arguments")));
      return;
    }

    // Check the argument data types
    if (!args[0]->IsString() || !args[1]->IsArray() || !args[2]->IsNumber() || !args[3]->IsFunction()) {
      isolate->ThrowException(v8::Exception::TypeError(
          v8::String::NewFromUtf8(isolate, "Wrong argument types")));
      return;
    }

    // Convert the arguments to the appropriate types
    std::string imagePath = std::string(*v8::String::Utf8Value(args[0]));
    v8::Local<v8::Array> colorArray = v8::Local<v8::Array>::Cast(args[1]);
    cv::Scalar color(colorArray->Get(0)->NumberValue(), colorArray->Get(1)->NumberValue(), colorArray->Get(2)->NumberValue(), colorArray->Get(3)->NumberValue());
    int thickness = args[2]->NumberValue();

    // Create a new Baton object
    Baton* baton = new Baton();
    baton->req.data = baton;
    baton->callback.Reset(isolate, v8::Local<v8::Function>::Cast(args[3]));
    baton->imagePath = imagePath;
    baton->color = color;
    baton->thickness = thickness;

    // Schedule the work on the background thread
    uv_queue_work(uv_default_loop(), &baton->req, [](uv_work_t* req) {
      Baton* baton = static_cast<Baton*>(req->data);

      // Read the image from the file
      cv::Mat image = cv::imread(baton->imagePath, CV_LOAD_IMAGE_COLOR);

      // Draw the rectangle on the image
      cv::rectangle(image, cv::Point(50, 50), cv::Point(100, 100), baton->color, baton->thickness);

      // Write the image back to the file
      cv::imwrite(baton->imagePath, image);
    }, [](uv_work_t* req, int status) {
      v8::Isolate* isolate = v8::Isolate::GetCurrent();
      v8::HandleScope handleScope(isolate);

      Baton* baton = static_cast<Baton*>(req->data);

      // Pass the result back to the callback function
      v8::Local<v8::Value> argv[] = {
        v8::Undefined(isolate)
      };
      v8::Local<v8::Function> callback = v8::Local<v8::Function>::New(isolate, baton->callback);
      callback->Call(isolate->GetCurrentContext()->Global(), 1, argv);

      // Clean up the Baton object
      baton->callback.Reset();
      delete baton;
    });

    args.GetReturnValue().SetUndefined();
  }

  // This is our initialization function that will expose our method to Node JS
  void Init(v8::Local<v8::Object> exports) {
    NODE_SET_METHOD(exports, "drawRectangleAsync", DrawRectangleAsync);
  }

  NODE_MODULE(NODE_GYP_MODULE_NAME, Init)

}  // namespace demo

In this example, we define a new method called DrawRectangleAsync that takes four arguments: an image path, a color array, a thickness value, and a callback function. We create a new Baton object that holds the data to be passed between threads. We then use the uv_queue_work function to schedule the work on the background thread.

When the work is complete, we pass the result back to the callback function and clean up the Baton object. To use this addon, we can call the DrawRectangleAsync method in our Node JS application with the same syntax as before, providing the appropriate arguments.

Conclusion:


C++ addons in Node JS provide a way to extend Node JS with high-performance C++ code. By using addons, we can take advantage of the speed and power of C++ while still using the familiar Node JS syntax. In this article, we covered the basics of creating a C++ addon in Node JS and provided examples of practical applications. With the knowledge gained in this article, readers should be able to further explore the use of C++ addons in Node JS to create high-performance applications.

Events In Node JS
NodeJS

Events In Node JS

Introduction As a Node JS developer, I have often found myself perplexed by events in Node JS. At first, I thought that events were just functions that get triggered when something happens. However, as I delved deeper into this topic, I realized that events are much more powerful and efficient than I had originally thought. […]

Debugger In Node JS
NodeJS

Debugger In Node JS

Debugger In Node JS: Getting a Deeper Understanding One thing we might all have in common as developers in the always changing tech industry is the ongoing need to come up with new and better approaches to debug our code. Since troubleshooting is a crucial step in the development process, we must be well-equipped with […]

Mastering Buffers In Node JS
NodeJS

Mastering Buffers In Node JS

As someone who is just getting started with Node JS, hearing about buffers might be a bit confusing at first. What are they exactly, and why should you care about them? I know I was pretty perplexed by this concept when I first started working with Node JS. However, once I understood what buffers were […]