写一个N-API没那么难?

本文基于Nodejs v13.1.0html

阅读本篇文章以前,请阅读前置文章:node

阅读完本篇文章以后,但愿你能够掌握如下知识点:linux

  • C++ Addon引入的原理
  • NAN写法与NAPI写法的区别
  • C++ Addon对算法效率的影响

本文demo地址:传送门git

一、NAN和NAPI的历史简介

诚如从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁一文所提到的NAN和NAPI的历史,NAN为了搞定”封建时代“混乱的C++原生模块,再也不让一个模块只能被若干个nodejs版本使用,而提出使用宏定义来解决这个问题,因此说NAN是一大堆宏定义,兼容各类nodejs版本的宏定义。作到了一次编写,处处编译github

而这种设计模式仍是依然有缺点,那就是屡次编译,也就是说你写的插件若是到了更高的Nodejs版本,仍是须要再次编译,让宏定义再次匹配新的版本去编译出新的插件包,因而在node v8版本以后,nodejs提出了新的一套机制,也就是咱们此次的主角-NAPI。面试

不一样版本的 Node.js 使用一样的接口,这些接口是稳定地 ABI 化的,即应用二进制接口(Application Binary Interface)。这使得在不一样 Node.js 下,只要 ABI 的版本号一致,编译好的 C++ 扩展就能够直接使用,而不须要从新编译。
复制代码

那么咱们怎么查看当前Node版本的ABI版本呢?经过process.versions.modules能够打印出当前的ABI版本,nodejs提供了一份完整的ABI版本列表:算法

abi_version_registry.jsonnpm

process.versions提供了Nodejs一些版本相关的提示,包括v8使用的版本,各个依赖包的版本,好比在版本v13.2.0下打印:(注:napi这个字段是NAPI模块版本,它有一个本身的版本矩阵:N-API Version Matrix)json

{
  node: '13.2.0',
  v8: '7.9.317.23-node.20',
  uv: '1.33.1',
  zlib: '1.2.11',
  brotli: '1.0.7',
  ares: '1.15.0',
  modules: '79',
  nghttp2: '1.40.0',
  napi: '5',
  llhttp: '1.1.4',
  openssl: '1.1.1d',
  cldr: '35.1',
  icu: '64.2',
  tz: '2019c',
  unicode: '12.1'
}
复制代码

N-API 定义了以下特性:windows

  • 提供头文件node_api.h
  • 任何 N-API 调用都返回一个napi_status枚举,来表示此次调用成功与否;
  • N-API 的返回值因为被napi_status占坑了,因此真实返回值经过形参来传递;
  • 全部JavaScript数据类型都被黑盒类型napi_value封装,再也不是相似于 v8::Objectv8::Number等类型;
  • 若是函数调用不成功,能够经过napi_get_last_error_info函数来获取最后一次出错的信息。

1.一、既然说N-API是基于ABI的,为啥它使用的不是modules字段,而是自定义了napi这个字段?

这个问题是留给你们思考的。有疑问欢迎留言讨论。

二、Nodejs是如何引入执行一个C++插件的?

老规矩,以下图所示:

图中咱们还发现有下面这么一个结论:为啥咱们直接require JSON文件的时候,能够自动转化为一个对象?

由于模块解析的时候,若是是json后缀的时候,会调用JSON.parse这个方法

Tips:上图还能够做为面试题《请说说在Nodejs中require一个文件后一个简单流程》的一个简单答案

咱们重点关注到v8里面的DLOpen方法。

该方法是为了解析node后缀的模块而写,node模块本质是一个动态连接库(windows下后缀是dll,linux下后缀是so),因此你看v8源码下,若是是支持__POSIX__标准的话,是使用系统APIdlopen()打开便可,若是是非__POSIX__的话,就只能借助libuv的uv_dlopen方法去打开。

其次文件打开以后,执行如下几个判断:

  • 判断模块的初始化函数是否符合标准
  • 判断是不是普通的C++插件,若是是的话,看看是否当前的nodejs版本ABI版本是否能够支持加载该模块

最后执行模块的初始化函数

这里能够看到N-API的模块是不须要判断的,从这里也印证了这句话:

A given version n of N-API will be available in the major version of Node.js in which it was published, and in all subsequent versions of Node.js, including subsequent major versions.
复制代码

2.一、使用不一样版本编译和执行NAN模块

利用nvm切换nodejs版本,咱们先使用node v11.15.0编译,再使用node v13.2.0执行,nodejs会报以下错误:

:16
internal/modules/cjs/loader.js:1190
  return process.dlopen(module, path.toNamespacedPath(filename));
                 ^

Error: The module '/Users/linxiaowu/Github/nodejs-NAPI-demo/packages/md5-NAN/build/Release/md5-nan.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 67. This version of Node.js requires
NODE_MODULE_VERSION 79. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
    at Object.Module._extensions..node (internal/modules/cjs/loader.js:1190:18)
    at Module.load (internal/modules/cjs/loader.js:976:32)
    at Function.Module._load (internal/modules/cjs/loader.js:884:14)
    at Module.require (internal/modules/cjs/loader.js:1016:19)
    at require (internal/modules/cjs/helpers.js:69:18)
    at Object.<anonymous> (/Users/linxiaowu/Github/nodejs-NAPI-demo/packages/md5-NAN/index.js:1:15)
    at Module._compile (internal/modules/cjs/loader.js:1121:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1160:10)
    at Module.load (internal/modules/cjs/loader.js:976:32)
    at Function.Module._load (internal/modules/cjs/loader.js:884:14)
复制代码

2.二、使用不一样版本编译和执行N-API模块

一样使用上述的步骤,是能够正常执行NAPI模块的。其中v11.15.0的process.versions

{ node: '11.15.0',
  v8: '7.0.276.38-node.19',
  uv: '1.27.0',
  zlib: '1.2.11',
  brotli: '1.0.7',
  ares: '1.15.0',
  modules: '67',
  nghttp2: '1.37.0',
  napi: '4',
  llhttp: '1.1.1',
  http_parser: '2.8.0',
  openssl: '1.1.1b',
  cldr: '34.0',
  icu: '63.1',
  tz: '2018e',
  unicode: '11.0' }
复制代码

而v13.2.0的process.versions是:

{
  node: '13.2.0',
  v8: '7.9.317.23-node.20',
  uv: '1.33.1',
  zlib: '1.2.11',
  brotli: '1.0.7',
  ares: '1.15.0',
  modules: '79',
  nghttp2: '1.40.0',
  napi: '5',
  llhttp: '1.1.4',
  openssl: '1.1.1d',
  cldr: '35.1',
  icu: '64.2',
  tz: '2019c',
  unicode: '12.1'
}
复制代码

由此验证了N-API在兼容性和编译这块作的确实够好。

三、NAN和NAPI写法对比

以demo中的为例子,分别用NAN和N-API实现了一个md5,下图是两者的对比:

咱们逐条逐条分析。

Tips:NAN使用第三方包nan,N-API使用第三方包node-addon-api

3.一、头部①

从头部能够看出N-API的头文件更加干净清爽,没有那些v8的语句。由于不须要再像NAN那样使用:

using v8::Local;
using v8::Object;
using v8::String
复制代码

3.二、实现部分②

实现部分,NAN使用宏定义将实现的函数头部包裹起来,而N-API通过node-addon-api包裹以后,更像一个正常的函数,有函数名、形参、返回值。函数体实现两者差距不是很大,除了返回值:

NAN:

info.GetReturnValue().Set(New(md5str).ToLocalChecked());

很是的v8!

而N-API:

return String::New(env, md5str,32);

很是的正常!

3.三、初始化函数③

两者的区别一看就看出差距:

NAN:

Nan::HandleScope scope;
Nan::SetMethod(exports, "md5", md5);
复制代码

N-API:

exports.Set("md5", Function::New(env, GetMD5));
return exports;
复制代码

3.四、模块定义④

NAN模块的初始化是交给 Node.js 提供的宏来实现的:

NODE_MODULE(addon, init)

而N-API使用本身的宏定义(NAPI_MODULE),由于咱们使用node-addon-api,因此它也对这个宏定义包裹成下面这个了:

NODE_API_MODULE(addon, Init)

四、NAPI能够提升算法效率吗?

咱们来看看使用N-API对排序算法的一个效率提高,示例中使用了两种排序算法:冒泡排序和快速排序:

代码参考:sort.cc

好比以快速排序为例子,快速排序的算法时间复杂度是NlogN,C语言版本的快排:

void quickSort(unsigned int *array, unsigned int length)
{
  unsigned int partition;
  unsigned int i, j;
  unsigned int rightLength, leftLength;
  unsigned int *rightArray, *leftArray;

  if (length < 2)
  {
    return;
  }
  partition = *(array);
  i = 1;

  for (j = 1; j <= length; j++)
  {
    if (*(array + j) < partition)
    {
      swap(array + i, array + j);
      i++;
    }
  }
  swap(array, array + i - 1);

  leftLength = i - 1;
  leftArray = array;
  rightArray = array + i;
  rightLength = length - i;

  quickSort(rightArray, rightLength);
  quickSort(leftArray, leftLength);
}
复制代码

当咱们改变demo中test/index.js中数组的长度,获得的js版本排序时间和N-API的排序时间(单位ms)以下图:

从图中能够看到,在快速排序算法中,随着数组长度的增长,js版本的排序时间甚至还优于N-API的排序时间,能够说两者不相上下,而在冒泡排序中,N-API的排序时间一直是优于js版本的排序时间,得出两个结论:

  • 选择算法的重要性!
  • v8对代码优化已经作得很好了!

参考

  1. 从暴力到 NAN 再到 NAPI——Node.js 原生模块开发方式变迁
  2. ABI Stability
  3. Shared library handling
  4. process.versions.modules is not for N-API ?
相关文章
相关标签/搜索