Skip to main content

Next Generation Native Node: N-API in C++

Some time ago, way back in the Node JS 0.10.x days, we needed to expose some native libraries previously written in C++ for use by an existing Node JS based service. 

 There were two reasons to reuse this existing C++ code base:

  1. The code was already written and debugged so wrapping it made a lot of sense from a cost perspective. 
  2. The intellectual property was somewhat easier to protect in a binary than it was in a JavaScript module.

Fortunately, support for native modules was already available at the time, and we took advantage of them. Over the lifetime of that project, we ended up with a few breaking changes due to the instability of the Node.js Addon API through it’s early releases. Now, 5 years later, the planned backward compatibility support for the newer Node Native API sounds pretty exciting. 

Below is a brief history of the Native Node APIs and a look at the current new standards for native node development as of Node.js v10.


N-API

Like the original Node.js Addon, the new N-API is delivered as a part of the Node.js distribution. Starting with Node.js v8, it was delivered as an experimental feature and has become stable in the Node.js v10 release. The real benefit of N-API is that it is a true abstraction and is being developed to be forward compatible. For example, modules created with N-API v3 in Node.js v10 are guaranteed to be forward compatible with future versions of Node.js which should result in no rework as projects upgrade Node.js versions. The N-API / Node.js version matrix is shown below. 

N-API Version Table

 


C versus C++

Strictly speaking, N-API is implemented in C. While there is nothing preventing us from using the C implementation, there is a  C++ header only implementation that wraps the C implementation and is much easier to use. Additionally, if N-API adds new features and the C++ wrapper does not immediately wrap those features, but you still want to use them, there is nothing to prevent you from using the C implementation along with the C++ one in the same module. For the remainder of this article, we’ll be using the C++ wrapper.

 


So, what can I do with it?

Let’s look at a couple of simple examples and then move on to something more complex and useful. 

N-API can still use the node-gyp build system but additionally, it can now use CMake which is likely more familiar to C/C++ developers. If you are interested in using CMake, see the cmake-js documentation.

 First, let’s look at the N-API version of the hello, world example. The module itself is not a big departure from the other versions:

// hello.cc
 #include <napi.h>

Napi::String Method(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(Napi::String::New(env, "hello"), Napi::Function::New(env, Method));
  return exports;
}
NODE_API_MODULE(hello, Init)

There is still a function definition (Method) which provides the implementation, and an Init function which defines the module exports. There is really nothing else of interest here.

The binding-gyp is slightly more complex than the original implementation: 

{
  "targets": [{
    "target_name": "hello",
    "sources": [ "hello.cc" ],
    "include_dirs": ["<!@(node -p \"require('node-addon-api').
include\")"],
    "cflags!": [ "-fno-exceptions" ],
    "cflags_cc!": [ "-fno-exceptions" ],
    "defines": [ 'NAPI_DISABLE_CPP_EXCEPTIONS' ]
  }]
}

Like the previous versions, it requires the target name, source, and include_dirs, but it also adds compiler definitions to disable the C++ exception mechanism.

Node Addons, NAN, and N-API all support the features described below, but since this is a post about N-API, I’ll only be describing the N-API ones. 

 


Function Arguments 

Non-trivial functions require data on which to operate. In Node.js, as in other interoperability environments, data must be marshalled from the managed JavaScript environment to the native C++ module for use there. N-API provides helpers to do that. Consider a slight modification to our hello world example in which a name string is passed for the target of the hello.

// hello.cc - With a parameter
#include <sstream>
#include <napi.h>

Napi::Value Method(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  std::string arg0 = info[0].As().Utf8Value();

  std::stringstream str;
  str << "hello, " << arg0;

  return Napi::String::New(env, str.str());
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set(Napi::String::New(env, "hello"), Napi::Function::New(env, Method));
  return exports;
}

NODE_API_MODULE(addon, Init)

In this example, we get the first argument passed to the function and attempt to convert it to a Napi::String. Once that is available, we convert it to a UTF8 string and then use std::stringstream to format the output in order to return “hello, n-api” when the function is called with the string parameter “n-api”.

 


Callbacks

Most functions in node are not simple functions that just return a result, so in this section we’ll implement a callback that will return results through a function call.

// hello.cc - With callback
#include <napi.h>

void RunCallback(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();

  Napi::Function cb = info[0].As();
  cb.Call(env.Global(), {Napi::String::New(env, "hello world")});
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  return Napi::Function::New(env, RunCallback);
}
NODE_API_MODULE(addon, Init)


The RunCallback function in this case retrieves the callback function pointer from the arguments passed to it by the caller in the same way that the string argument was retrieved in the last example. Before the function completes, it executes the callback function passing the “hello world” string as the first parameter.  The client code looks as you might expect:

const { hello } = require('bindings')('addon');

hello((msg) => {
  console.log(msg); // "hello world"
});

Unfortunately, this implementation does not provide actual asynchronous execution. In this case, the function call blocks until after it completes the callback. For true asynchronous execution, we require a somewhat more complex implementation.

 


Actual Asynchrony

Most functions in node are not simple functions that just return a result, so in this section we’ll implement a callback that will return results through a function call.

The AsyncWorker virtual class provides a nice wrapper to do asynchronous work and then hook the completion of that work in order to make a callback to the caller. In this example, we’ll take it one step further and return a Promise to the caller which will later be resolved or rejected depending on the outcome of the asynchronous work.

 


// hello.cc
#include <sstream>
#include <napi.h>

class AsyncWorker : public Napi::AsyncWorker {
public:
  static Napi::Value Create(const Napi::CallbackInfo& info) {
    Napi::Env env = info.Env();

    if (info.Length() != 1 || !info[0].IsString()) {
      Napi::TypeError::New(env, "Expected a single string argument")
          .ThrowAsJavaScriptException();
      return env.Null();
    }

    std::string input = info[0].As().Utf8Value();
    AsyncWorker* worker = new AsyncWorker(info.Env(), input);

    worker->Queue();
    return worker->deferredPromise.Promise();
  }

protected:
  void Execute() override {
    if(input.size() < 1) {
      SetError("EmptyName");
      return;
    }

    std::stringstream str;
    str << "hello, " << input;

    result = str.str();
  }

  virtual void OnOK() override {
      deferredPromise.Resolve(Napi::String::New(Env(), result));
  }

  virtual void OnError(const Napi::Error& e) override {
      deferredPromise.Reject(e.Value());
  }

private:
  AsyncWorker(napi_env env, std::string& name) :
    Napi::AsyncWorker(env),
    input(name),
    result(),
    deferredPromise(Napi::Promise::Deferred::New(env)) { }

  std::string input;
  std::string result;

  Napi::Promise::Deferred deferredPromise;
};

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  return Napi::Function::New(env, AsyncWorker::Create);
}
NODE_API_MODULE(addon, Init)

To learn more, download my article to see how the AsyncWorker is used here.

Steve Brooks, Sr. Software Engineer

Prior to joining Accusoft as a senior software engineer in 2007, Steve Brooks consulted to organizations including NASA and Microsoft developing applications and SDKs. A graduate of North Carolina’s Appalachian State University, Steve has a Bachelor’s of Science in Applied Physics and a background focused primarily in scientific instrumentation development. Since joining Accusoft, he has worked on several products including PICTools, ImagXpress, and PrizmDoc Suite. Steve lives in the mountains of North Carolina where he enjoys spending his free time with his wife and daughters.