什么是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-toolsnpm 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_namesources两个属性。target_name定义了该Native包的名称,sources定义了需要编译的文件。

对于gyp文件的更多配置,可参考nodejs/node-gypGYP 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++对象模型以及异常处理。

参考资料

  1. N-API - Node.js v12 Documentation
  2. node-addon-examples - GitHub