c++node.jsnode.js-addon

Node.js add on module in C/C++


I have been exploring writing add-on modules for node.js in C/C++. So far I have a simple add on working where I can call an add on function form JavaScript, it's very simple. The routine is called hello, its passed a string and then returns the same string prefixed with Hello with a space between the two words.

package.json:

{
    "name":         "test",
    "version":      "2.0.0",
    "description":  "test description",
    "main":         "index.js",
    "gypfile":      true,
    "scripts": {
        "build":    "node-gyp rebuild",
        "clean":    "node-gyp clean"
    },
    "author": "Simon Platten",
    "license": "ISC",
    "devDependencies": {
        "node-gyp": "^7.1.2"
      },
    "dependencies": {
        "node-addon-api": "^3.1.0"
    }
}

binding.gyp:

{
    "targets":[{
        "target_name":  "test",
        "cflags!":      ["-fno-exceptions"],
        "cflags_cc!":   ["-fno-exceptions"],
        "sources":      ["cpp/main.cpp"],
        "include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
        "libraries":    [],
        "dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
        "defines":      ["NAPI_DISABLE_CPP_EXCEPTIONS"],
    }]
}

main.cpp

/**
 * File:    main.cpp
 * Notes:
 *  Work in progress for add on module for node.js 'test'
 * History:
 *  2021/03/11 Written by Simon Platten
 */
 #include <napi.h>
 #include <iostream>
 #include <string>
/**
 * Return "Hello " + user name passed back
 */
 Napi::String hello(const Napi::CallbackInfo& info) {
     Napi::Env env = info.Env();
     std::string strResult = "Hello ";
     strResult += info[0].ToString();
     return Napi::String::New(env, strResult);
 }
/**
 * Callback method when module is registered with Node.js
 */
 Napi::Object Init(Napi::Env env, Napi::Object exports) {
     exports.Set(
        Napi::String::New(env, "hello"),
        Napi::Function::New(env, hello)
     );
     return exports;
 }
 NODE_API_MODULE(test, Init);

index.js:

const test = require("./build/Release/test");

console.log(test.hello("Simon"));

This works great, now I want to get a bit more adventurous. I'm not sure of the correct terminology for this, but for example, when a socket is used in node there are all kinds of on callbacks or events that can be managed with a callback function. This is exactly what I want to achieve from my module, the module will be passed a large amount of data to process, I will return immediately whilst the data is processed by the module and when its complete and ready for the client I want to issue a notification to node.js in the form of an on style handler.

How?


Solution

  • In JS:

    Create a class that inherits from EventEmitter. Doing this you'll be able to listen to events from this class.

    If you call your addon's function with a callback from this class and in the callback you emit your event you can achieve what you want.

    Something like this:

    const test = require("./build/Release/test");
    const EventEmitter = require('events');
    
    class Test extends EventEmitter {
        constructor() {
            super();
            this.addon = test;
        }
    
        test() {
            test.hello("Simon", (data) => {
                this.emit("testEvent", data);
            });
        }
    }
    
    const myTest = new Test();
    
    myTest.on("testEvent", (data) => {
        console.log("testEvent says: ", data);
    })
    
    myTest.test();
    

    In C++:

    Based on the example found here, you can add an AsyncWorker that will process your data in the background and call a callback once finished.

    Like this:

    #include <napi.h>
    #include <iostream>
    #include <string>
    #include <chrono>
    #include <thread>
    
    using namespace Napi;
    
    class TestWorker : public AsyncWorker {
    public:
        TestWorker(Function& callback) : AsyncWorker(callback) {}
    
        void Execute() override {
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    
        void OnOK() override {
            HandleScope scope(Env());
            Callback().Call({String::New(Env(), "message from the addon")});
        }
    };
    

    You can provide this callback as an argument to your hello function along with your current argument "Simon".

    Your code extended with the callback argument and the starting of the AsyncWorker:

     Napi::String hello(const Napi::CallbackInfo& info) {
         Napi::Env env = info.Env();
         std::string strResult = "Hello ";
         strResult += info[0].ToString();
         Function cb = info[1].As<Function>();
         TestWorker* wk = new TestWorker(cb);
         wk->Queue();
         return Napi::String::New(env, strResult);
     }