软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

NodeJS C++插件开发体验(N-API)

NodeJS C++插件直接扩展NodeJS V8引擎,性能比JS要高很多。同时,通过N-API跨越JS与C++的边界,不需要做序列化/反序列化,性能代价也很小。根据二八法则,使用C++插件来解决NodeJS业务系统的性能问题/密集型计算,而不是完整业务系统的主要功能。

在开发NodeJS插件时,可以选择N-API、nan、或者内部V8接口实现。但是,N-API有良好的ABI兼容性,可确保插件一次编译能够在多个NodeJS版本中运行,更新NodeJS版本之后不需要重新编译插件。学习NodeJS插件开发,可参考官方的node-addon-examples

在本示例中,我们使用C++11 std与boost实现4个导出函数:

generateNumbers(n: number, callback: (v: number) => void): void;
joinStrings(arr: string[], sep: string): string;
isPrime(n: number) => boolean;
isPrimeAsync(n: number): Promise<boolean>;

本示例源码:napi-addon-1-hello-primes

1. binding.gyp

我们使用node-gyp构建插件项目,需全局安装node-gyp: npm install -g node-gyp

示例binding.gyp参考node-addon-examples/1_hello_world/napi/即可。复杂的项目,可参考node-sqlite3

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [
        "src/hello.cc",
        "src/generate_numbers.cc",
        "src/is_primes.cc",
        "src/join_strings.cc",
        "src/util.cc"
      ]
    }
  ]
}

2. C++源码

2.1. 插件导出函数

N-API注册导出函数地址到exports成员属性,因此不需要函数定义为extern "C".

// src/hello.hpp
#ifndef __HELLO_HPP_INCLUDED__
#define __HELLO_HPP_INCLUDED__

#include <node_api.h>
#include <string>

#ifdef __cplusplus
// extern "C" {
#endif

std::string value_to_string(napi_env env, napi_value value);

// generateNumbers(n: number, callback: (v: number) => void): void
napi_value GenerateNumbers(napi_env env, napi_callback_info info);

// short time job: joinStrings(arr: string[], sep: string): string
napi_value JoinStrings(napi_env env, napi_callback_info info);

// long time job: isPrime(n: number) => boolean
napi_value IsPrime(napi_env env, napi_callback_info info);

// long time job: isPrimeAsync(n: number).then((boolean) => void);
napi_value IsPrimeAsync(napi_env env, napi_callback_info info);


#ifdef __cplusplus
// }
#endif

#endif // !__HELLO_HPP_INCLUDED__
// src/hello.cpp

#include <assert.h>
#include <node_api.h>

#include "hello.hpp"


#define DECLARE_NAPI_METHOD(name, func) \
  { name, 0, func, 0, 0, 0, napi_default, 0 }

napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor descArr[] = {
    DECLARE_NAPI_METHOD("generateNumbers", GenerateNumbers),
    DECLARE_NAPI_METHOD("joinStrings", JoinStrings),
    DECLARE_NAPI_METHOD("isPrime", IsPrime),
    DECLARE_NAPI_METHOD("isPrimeAsync", IsPrimeAsync),
  };
  status = napi_define_properties(env, exports, sizeof(descArr)/sizeof(descArr[0]), descArr);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

2.2. 参数读取示例: JoinStrings

读取JS传入的字符串数组,并转换到std::vector<std::string>,再使用boost::join()完成字符串的拼接。

// src/join_strings.cc


#include <assert.h>
#include <node_api.h>

#include <utility>
#include <vector>
#include <string>

#include <boost/algorithm/string/join.hpp>

#include "hello.hpp"

// short time job: join(arr: string[], sep: string)
napi_value JoinStrings(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result;

  // Input args
  size_t argc = 2;
  napi_value argv[2];

  status = napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
  assert(status == napi_ok);

  std::vector<std::string> vec;
  std::string sep{"|"};

  if (argc) {
    uint32_t length = 0;
    status = napi_get_array_length(env, argv[0], &length);
    assert(status == napi_ok);
    vec.reserve(length);

    for (uint32_t i = 0; i < length; i++) {
      napi_value e;
      status = napi_get_element(env, argv[0], i, &e);
      assert(status == napi_ok);

      vec.push_back(value_to_string(env, e));
    }
  }

  if (argc > 1) {
    sep = value_to_string(env, argv[1]);
  }

  // Join the strings
  auto joined = boost::join(vec, sep);
  status = napi_create_string_utf8(env, joined.c_str(), joined.size(), &result);
  assert(status == napi_ok);
  return result;
}

// src/util.cc

#include <assert.h>
#include <node_api.h>

#include <utility>
#include <string>

#include "hello.hpp"

std::string value_to_string(napi_env env, napi_value value) {
  napi_status status;
  size_t strLen = 0;
  status = napi_get_value_string_utf8(env, value, nullptr, 0, &strLen);
  assert(status == napi_ok);

  std::string str;
  // buffer includes '\0'
  str.resize(strLen + 1);

  status = napi_get_value_string_utf8(env, value,
    const_cast<std::string::value_type *>(str.data()), str.size(), &strLen);
  assert(status == napi_ok);
  str.resize(strLen);
  return std::move(str);
}

2.3. 回调函数示例: GenerateNumbers

generateNumbers(n, callback)的第二个参数是回调函数。使用napi_call_function()调用回调函数。GenerateUniformDistNum<>()函数支持C++ lambda函数回调。

// src/generate_numbers.cc

#include <assert.h>
#include <node_api.h>

#include <random>

#include "hello.hpp"

template <typename Callback>
class UniformDistNum {
  Callback cb_;

public:
  UniformDistNum(Callback cb) : cb_(cb) {
  }

  template <typename IntType>
  void Generate(IntType n) const {
    std::random_device r;
    // Choose a random mean between 1 and n
    std::default_random_engine e(r());
    std::uniform_int_distribution<IntType> uniform_dist(1, n);

    for (IntType i = 0; i < n; i++) {
      auto mean = uniform_dist(e);
      cb_(mean);
    }
  }
};

template <typename IntType, typename Callback>
void GenerateUniformDistNum(IntType n, Callback cb) {
  UniformDistNum<Callback> distNum(cb);
  distNum.Generate(n);
}

// generateNumbers(n: number, callback: (v: number) => void)
napi_value GenerateNumbers(napi_env env, napi_callback_info info) {
  napi_status status;

  size_t argc = 2;
  napi_value args[2];
  status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  assert(status == napi_ok);
  assert(argc >= 2);

  uint32_t n = 0;
  status = napi_get_value_uint32(env, args[0], &n);
  assert(status == napi_ok);

  napi_value global;
  status = napi_get_global(env, &global);
  assert(status == napi_ok);

  napi_value cb = args[1];

  GenerateUniformDistNum(n, [env, global, cb](uint32_t mean) {
    napi_value argv[1];

    napi_status status = napi_create_uint32(env, mean, argv);
    assert(status == napi_ok);

    status = napi_call_function(env, global, cb, 1, argv, nullptr);
    assert(status == napi_ok);
  });

  return nullptr;
}

2.4. Promise异步任务: IsPrimeAsync

添加PrimeIter随机访问迭代器类,完成boost::math::prime(n)与std::vector<IntType> primesExt的访问。

GeneratePrimesExt()函数生成比boost::math::prime(n)更多的质数,默认新增1000个。

// src/is_prime.cc

#include <assert.h>
#include <node_api.h>

#include <cstdio>
#include <utility>
#include <vector>
#include <string>
#include <memory>

#include <algorithm>
#include <boost/math/special_functions/prime.hpp>
#include <boost/iterator/iterator_facade.hpp>

#include "hello.hpp"

typedef struct {
  napi_async_work work;
  napi_deferred deferred;
  int64_t num;
  bool result;
} IsPrimeWorkData;

template <typename IntType>
class PrimeIter
  : public boost::iterator_facade<
        PrimeIter<IntType>
      , IntType
      , boost::random_access_traversal_tag
      , IntType
    >
{
public:
  PrimeIter(): n(0), primesExt(nullptr) {}
  PrimeIter(IntType n_, const std::vector<IntType> *pExt = nullptr): n(n_), primesExt(pExt) {}
  PrimeIter(const PrimeIter &other) = default;

private:
  friend class boost::iterator_core_access;
  template <class> friend class PrimeIter;

  template <class OtherValue>
  bool equal(PrimeIter<OtherValue> const& other) const {
    return this->n == other.n;
  }

  void increment() { n++; }

  void decrement() { n--; }

  void advance(int n_) {
    n += n_;
  }

  template <class OtherValue>
  int distance_to(PrimeIter<OtherValue> const& other) const {
    return n - other.n;
  }

  IntType dereference() const {
    IntType bpMax = boost::math::max_prime;

    if (n <= bpMax) {
      return boost::math::prime(n);
    }

    if (primesExt && n - bpMax <= primesExt->size()) {
      return (*primesExt)[n - bpMax - 1];
    }

    return 0;
  }

  IntType n;
  const std::vector<IntType> *primesExt;
};

std::vector<uint32_t> GeneratePrimesExt(uint32_t extCount = 1000) {
  std::vector<uint32_t> primesExt;
  primesExt.reserve(extCount);

  uint32_t maxN = boost::math::max_prime + extCount;

  for (uint32_t i = boost::math::prime(boost::math::max_prime) + 1, n = boost::math::max_prime + 1;
    n <= maxN;
    i++) {
    bool isPrimeNum = true;

    for (uint32_t inner = 0; inner < n; inner++) {
      uint32_t innerPrime = *PrimeIter<uint32_t>(inner, &primesExt);
      if (i % innerPrime == 0) {
        isPrimeNum = false;
        break;
      }
    }

    if (isPrimeNum) {
      primesExt.push_back(i);
      n++;
      // std::printf("%d\n", i);
    }
  }

  return std::move(primesExt);
}

bool isPrimeInner(int64_t num) {
  if (num < boost::math::prime(0)) {
    return false;
  } else if (num <= boost::math::prime(boost::math::max_prime)) {
    return std::binary_search(PrimeIter<uint32_t>(0), PrimeIter<uint32_t>(boost::math::max_prime + 1), num);
  }

  // Generate Primes ext
  static auto primesExt = GeneratePrimesExt();
  uint32_t maxPrimes = primesExt[primesExt.size() - 1];

  if (num > maxPrimes) {
    std::fprintf(stderr, "Input number (%d) is large than max prime supported (%d)\n", num, maxPrimes);
  } else {
    return std::binary_search(primesExt.begin(), primesExt.end(), num);
  }

  return false;
}

// long time job: isPrime(n: number) => boolean
napi_value IsPrime(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value result;

  // Input args
  size_t argc = 1;
  napi_value argv[1];

  status = napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
  assert(status == napi_ok);

  int64_t num = 0;
  status = napi_get_value_int64(env, argv[0], &num);
  assert(status == napi_ok);

  bool isTrue = isPrimeInner(num);

  status = napi_get_boolean(env, isTrue, &result);
  assert(status == napi_ok);
  return result;
}

#define CHECK(expr) \
  { \
    if ((expr) == 0) { \
      fprintf(stderr, "%s:%d: failed assertion `%s'\n", __FILE__, __LINE__, #expr); \
      fflush(stderr); \
      abort(); \
    } \
  }


void IsPrimeExecuteWork(napi_env env, void* data) {
  auto workData = (IsPrimeWorkData *) data;
  workData->result = isPrimeInner(workData->num);
}

void IsPrimeWorkComplete(napi_env env, napi_status status, void* data) {
  std::unique_ptr<IsPrimeWorkData> workData((IsPrimeWorkData *) data);

  if (status != napi_ok) {
    return;
  }

  napi_value result;

  status = napi_get_boolean(env, workData->result, &result);
  assert(status == napi_ok);

  CHECK(napi_resolve_deferred(env, workData->deferred, result) == napi_ok);

  // Clean up the work item associated with this run.
  CHECK(napi_delete_async_work(env, workData->work) == napi_ok);

  // Set both values to NULL so JavaScript can order a new run of the thread.
  workData->work = NULL;
  workData->deferred = NULL;
}

// long time job: isPrimeAsync(n: number).then((boolean) => void);
napi_value IsPrimeAsync(napi_env env, napi_callback_info info) {
  napi_value work_name;
  napi_value promise;
  napi_status status;

  // Input args
  size_t argc = 1;
  napi_value argv[1];

  status = napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
  assert(status == napi_ok);

  int64_t num = 0;
  status = napi_get_value_int64(env, argv[0], &num);
  assert(status == napi_ok);

  std::unique_ptr<IsPrimeWorkData> workData(new IsPrimeWorkData());
  workData->num = num;
  workData->result = false;

  // Create a string to describe this asynchronous operation.
  CHECK(napi_create_string_utf8(env,
                                 "N-API Deferred Promise from IsPrimeAsync",
                                 NAPI_AUTO_LENGTH,
                                 &work_name) == napi_ok);

  // Create a deferred promise which we will resolve at the completion of the work.
  CHECK(napi_create_promise(env,
                             &(workData->deferred),
                             &promise) == napi_ok);

  // Create an async work item, passing in the addon data, which will give the
  // worker thread access to the above-created deferred promise.
  CHECK(napi_create_async_work(env,
                                NULL,
                                work_name,
                                IsPrimeExecuteWork,
                                IsPrimeWorkComplete,
                                workData.get(),
                                &(workData->work)) == napi_ok);

  // Queue the work item for execution.
  CHECK(napi_queue_async_work(env, workData->work) == napi_ok);

  workData.release();

  // This causes created `promise` to be returned to JavaScript.
  return promise;
}

3. 构建插件

正常构建,执行: node-gyp build

添加新文件之后,我们需要重新构建:node-gyp rebuild --debug

4. 使用插件

在hello.js中调用插件导出的函数:

// hello.js
const addon = require('bindings')('hello');

console.log(addon);

function run() {
  console.log('----- JoinStrings -----');
  console.log(addon.joinStrings([ 'a', 'b', 'c' ], '1'));

  console.log('----- GenerateNumbers -----');
  let nums = '';

  addon.generateNumbers(100, (n) => {
    if (nums.length) {
      nums += ',';
    }

    nums += n;
  });

  console.log(nums);

  console.log('----- IsPrime -----');

  const checkedPrimes = [ 1, 2, 39194 + 0xffff, 114713, 116447, 39194 + 0xffff + 6644668 ];

  checkedPrimes.forEach((n) => {
    console.log(n, 'is prime:', addon.isPrime(n));
  });

  console.log('----- IsPrimeAsync -----');

  checkedPrimes.forEach((n) => {
    addon.isPrimeAsync(n).then((result) => {
      console.log(n, 'is prime:', result);
    });
  });
}

run();

5. 调试NodeJS插件

给VSCode安装好C++调试插件,使用调试 node . 命令执行的程序。.vscode/launch.json内容如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Node Native",
            "type": "cppvsdbg",
            "request": "launch",
            "program": "node",
            "args": ["${workspaceFolder}"],
            "stopAtEntry": false,
            "cwd": "${workspaceFolder}",
            "environment": [],
            "externalConsole": false
        }
    ]
}

6. 预编译代替bindings

安装包含C++插件的npm包时,如果每次都需要编译,耗时比较久,不是很方便。为了提升安装速度,我们一般使用预编译二进制包,在安装npm时下载即可使用。使用package.json的scripts.install脚本启动下载。

可参考 node-sqlite3 的配置使用node-pre-gyp

7. N-API与标准C++边界划分

我们仅在exports的函数中使用与N-API相关的函数,在其它函数中尽量使用标准C++相关的类型与函数。