什么是N-API
N-API为开发者提供了一套C/C++ API用于开发Node.js的Native扩展模块。从Node.js 8.0.0开始,N-API以实验性特性作为Node.js本身的一部分被引入,并且从Node.js 10.0.0开始正式全面支持N-API。
Hello N-API
本文将使用一个简单的模块作为示例介绍N-API。我们将编写一个hello模块,其中包括一个返回Hello N-API!字符串的方法greeting。其实现的功能相当于下列Javascript代码:
const greeting = () => {
  return 'Hello N-API!';
}
module.exports = {
  greeting,
};
greeting方法定义
首先,我们需要定义greeting方法,并返回值为Hello N-API!的字符串。为了使用N-API提供的接口及类型定义,我们需要引入node_api.h头文件。使用N-API定义的方法需要满足napi_callback类型,其定义为:
typedef napi_value (*napi_callback)(napi_env env, napi_callback_info info);
napi_callback是使用N-API开发的Native函数的函数指针类型,其接受类型分别为napi_env以及napi_callback_info的两个参数,并返回类型为napi_value的值。greeting方法中涉及到的几个类型定义及其用途如下:
- napi_value类型是一个用于表示Javascript值的指针
- napi_env类型用于存储Javascript虚拟机的上下文
- napi_callback_info类型用于调用回调函数时,传递调用时的上下文信息
我们定义的greeting方法如下:
napi_value greeting(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value word;
  char *str = "Hello N-API!";
  status = napi_create_string_utf8(env, str, strlen(str), &word);
  assert(status == napi_ok);
  return word;
}
在greeting方法中,我们通过napi_create_string_utf8函数创建了值为"Hello N-API!"的Javascript字符串对象,并将其作为该方法的返回值返回。napi_create_string_utf8用于创建一个UTF-8类型的字符串对象,其值来自于参数传递的UTF-8编码字符串,函数原型如下:
napi_status napi_create_string_utf8(napi_env env,
  const char *str,
  size_t length,
  napi_value* result
);
- env:传递当前VM的上下文信息
- str:UTF-8编码的字符序列
- length:字符序列- str的长度
- result:用于表示创建的Javascript字符串对象的指针
napi_create_string_utf8返回一个napi_status类型的值,当其值为napi_ok时代表完成字符串对象的创建。如示例中代码所示,我们在调用napi_create_string_utf8后,便使用assert判断其返回值是否为napi_ok。
napi_status是一个用于指示N-API中状态的枚举类型,其值可参考napi_status。
模块注册
在完成了greeting方法后,我们还需要注册我们的hello模块。N-API通过NAPI_MODULE(modname, regfunc)宏进行模块的注册。其接受两个参数,分别为模块名及模块初始化函数。模块初始化函数需要满足下列函数签名:
napi_value (*)(napi_env env, napi_value exports);
在模块的初始化中,我们可以定义模块需要暴露的方法及属性。我们的模块初始化函数如下所示:
napi_value init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor descriptor = {
    "greeting",
    0,
    greeting,
    0,
    0,
    0,
    napi_default,
    0,
  };
  status = napi_define_properties(env, exports, 1, &descriptor);
  assert(status == napi_ok);
  return exports;
}
NAPI_MODULE(hello, init);
在我们的的初始化函数中,需要在模块的exports对象中定义greeting属性。在定义属性之前,我们需要创建一个napi_property_descriptor类型的属性描述符,该类型的定义如下:
typedef struct {
  const char* utf8name;
  napi_value name;
  napi_callback method;
  napi_callback getter;
  napi_callback setter;
  napi_value value;
  napi_property_attributes attributes;
  void* data;
} napi_property_descriptor;
对于本文示例中需要使用的属性值描述如下所示,关于napi_property_descriptor的更多描述可参考napi_property_descriptor。
- utf8name:UTF-8编码的字符序列
- name:由Javascript对象表示的字符串或者Symbol
utf8name以及name二者中必须且只能有一个被提供,其代表属性的名称。
- method:将该属性设置为表示一个Javascript方法(function)
- attributes:属性的行为控制标志,示例中使用了默认的- napi_default值,更多描述可参考napi_property_attributes
我们需要定义的greeting属性是一个方法,所以我们所创建的属性描述符主要传递了utf8name以及method属性。
在创建属性描述符后,便需要将其在模块的exports对象中定义,使Javascript代码能够访问。对象属性的定义使用了napi_define_properties函数,它可以快速的为一个对象定义指定数量的属性。该函数定义为:
napi_status napi_define_properties(napi_env env,
  napi_value object,
  size_t property_count,
  const napi_property_descriptor *properties
);
- object:需要定义属性的Javascript对象
- property_count:属性数量
- properties:属性描述符数组
同样,napi_define_properties也返回了一个napi_status类型的值表示函数调用是否成功。
最后,我们只需要在模块初始化函数中返回exports对象,并通过NAPI_MODULE(hello, init)注册hello模块。到此为止,我们的hello模块便编写完成了。
模块编译
Native模块的构建可选择node-gyp或者cmake.js,二者的使用需要安装C/C++工具链,本文选择了node-gyp作为示例的构建工具。node-gyp是基于Google的gyp工具开发,它除了必要的C/C++编译器以外,还需要安装Python以及make工具。对于Windows用户,使用node-gyp需要安装Python并通过npm安装windows-build-tools(npm install --global --production windows-build-tools)。
接下来,需要定义binding,gyp文件。binding,gyp是node-gyp的JSON类型配置文件,文中示例程序使用的binding.gyp内容如下所示:
{
  "targets": [
    {
      "target_name": "hello",
      "sources": [
        "hello.c"
      ]
    }
  ]
}
如示例所示,binding,gyp文件中定义了targets,它定义了一组gyp能生成的目标。targets中定义了一个对象,其包括了target_name和sources两个属性。target_name定义了该Native包的名称,sources定义了需要编译的文件。
对于gyp文件的更多配置,可参考nodejs/node-gyp、GYP User Documentation以及GYP Input Format Reference。
接下来便可以使用node-gyp构建示例中编写的Native模块。
$ node-gyp configure build
在完成构建后,将会在当前目录下产生一个build文件,其中包括了生成的各个中间文件以及.node文件。.node文件本质上即一个动态的链接库,Node.js会调用dlopen函数用于加载.node文件。
测试
在构建Native模块后,就将在js代码中引入生成的.node文件,并调用上文模块中定义greeting方法。
const hello = require('./build/Release/hello.node');
console.log(hello.greeting());
运行该程序,将得到下面的输出结果:
$ node index.js
Hello N-API!
若安装了bindings依赖,便可将const hello = require('./build/Release/hello.node');修改为const hello = require('bindings')('hello');。
const hello = require('bindings')('hello');
console.log(hello.greeting());
结束语
对于Node.js Native扩展模块的开发,除了使用N-API提供的API以外,还可选择nodejs/nan或者nodejs/node-addon-api。
N-API提供的接口为纯C的风格,对于C++开发者可选用node-addon-api,其在N-API的基础上提供了C++对象模型以及异常处理。