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++相关的类型与函数。