Overview
Node.js is a powerful platform for building network applications. It is written in C, C++, and JavaScript, providing a versatile environment for developers. While JavaScript is the core language for writing Node.js modules, there are scenarios where the performance and capabilities of C/C++ are desired or necessary. Luckily, Node.js allows developers to write addons in C/C++ that can be invoked from JavaScript code. This tutorial will guide you through the process of writing C/C++ addons for Node.js.
Setting Up The Environment
To begin, you will need to set up the proper development environment, which includes installing Node.js, the Node.js package manager (npm), and node-gyp, a tool used for compiling native addon modules for Node.js.
$ sudo apt-get install nodejs
$ sudo apt-get install npm
$ npm install -g node-gyp
Creating Your First Addon
Let’s start with a simple “Hello, World!” addon. First, you’ll need to initialize a new Node.js module:
$ mkdir hello-addon
$ cd hello-addon
$ npm init -y
$ npm install node-addon-api
Create a `binding.gyp` file next to your `package.json`, with the following contents:
{
"targets": [
{
"target_name": "hello",
"sources": ["hello.cc"]
}
]
}
Now, write your C++ addon in the `hello.cc` file:
#include
Napi::String Hello(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
return Napi::String::New(env, "Hello, World!");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("hello", Napi::Function::New(env, Hello));
return exports;
}
NODE_API_MODULE(hello, Init)
After writing the C++ code, compile your addon using node-gyp:
$ node-gyp configure
$ node-gyp build
Loading and Using Your Addon from JavaScript
To use your newly created addon, you’ll need to load it in a JavaScript file. Create an `index.js` with the following code:
const addon = require('./build/Release/hello');
console.log(addon.hello()); // outputs "Hello, World!"
Run your JavaScript file using Node.js to see the output:
$ node index.js
Advanced Usage
After mastering the basics, you can move on to more complex tasks, like passing arguments to your C/C++ functions, handling callbacks, and working with various data types. The possibilities are almost endless, and your addons can become as sophisticated as needed.
Here’s an example of an addon that accepts arguments and performs calculations:
#include
Napi::Number Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::TypeError::New(env, "Two arguments expected").ThrowAsJavaScriptException();
}
double arg0 = info[0].As().DoubleValue();
double arg1 = info[1].As().DoubleValue();
double sum = arg0 + arg1;
return Napi::Number::New(env, sum);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(addon, Init)
And in your JavaScript file:
const addon = require('./build/Release/addon');
console.log('The sum is', addon.add(3, 5)); // outputs "The sum is 8"
Working with Asynchronous Code
In a Node.js environment, non-blocking operations are crucial. You can write asynchronous C/C++ addons that leverage Node.js’s event loop. For example, using the N-API’s thread-safe function:
#include <napi.h>
#include <thread>
// A data structure to hold the data to be passed to the JavaScript function
struct Data {
int value;
Napi::ThreadSafeFunction tsfn;
};
// A callback function to receive the data from the native thread
void Callback(Napi::Env env, Napi::Function jsCallback, Data* data) {
// Convert the data to a JS value and pass it to the JS callback
jsCallback.Call({ Napi::Number::New(env, data->value) });
// Release the thread-safe function
data->tsfn.Release();
}
// A function to run on the native thread
void ThreadFunction(Data* data) {
// Do some work with the data
data->value *= 2;
// Call the thread-safe function with the data
data->tsfn.NonBlockingCall(data, Callback);
}
// A function exposed to JS to create a thread-safe function
Napi::Value CreateThreadSafeFunction(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// Get the JS callback function as the first argument
Napi::Function jsCallback = info[0].As<Napi::Function>();
// Create a thread-safe function with the JS callback
Napi::ThreadSafeFunction tsfn = Napi::ThreadSafeFunction::New(
env,
jsCallback,
"TestResource",
0, // unlimited queue
1 // initial thread count
);
// Create a data structure with an initial value and the tsfn
Data* data = new Data{ 42, tsfn };
// Acquire the tsfn before passing it to the thread
tsfn.Acquire();
// Start a new thread with the data
std::thread thread(ThreadFunction, data);
// Detach the thread
thread.detach();
// Return the tsfn as a JS value
return tsfn.Value();
}
// Initialize the addon
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("createThreadSafeFunction", Napi::Function::New(env, CreateThreadSafeFunction));
return exports;
}
// Register the addon
NODE_API_MODULE(addon, Init)
A thread-safe function is a way to invoke a JavaScript function from a native thread without blocking the event loop. It allows you to pass data from the native thread to the JavaScript function as an argument. You can create a thread-safe function using the Napi::ThreadSafeFunction::New
method, and then call it from the native thread using the Napi::ThreadSafeFunction::NonBlockingCall
method.
Debugging and Profiling
Debugging native code can be a little trickier than JavaScript. Tools like gdb and Valgrind can be used to debug and profile your C/C++ Node.js addons.
Conclusion
Writing C/C++ addons for Node.js opens up a plethora of possibilities for performance optimization and integration with existing C/C++ libraries. By following this tutorial, you’re now equipped with the knowledge to extend the capabilities of your Node.js applications with powerful native addons.