保存成功
订阅成功
保存失败,请重试
提交成功

从暴力到 NAN 再到 N-API——Node.js 原生模块开发方式变迁

蚂蚁金服高级工程师,曾负责大搜车无线架构组中间件团队。开源爱好者,Toshihiko 作者、阿里云 ONS SDK 作者,Node.js 核心贡献者之一。即将出版《Node.js:来一打 C++ 扩展》一书。
查看本场Chat

前言

在 Node.js 开发领域中,原生 C++ 模块的开发一直是一个被人冷落的角落。但是实际上在必要的时候,用 C++ 进行 Node.js 的原生模块开发能有意想不到的好处。

  • 性能提升。很多情况下,使用 C++ 进行 Node.js 原生模块开发的性能会比纯 Node.js 开发要高,少数情况除外。

  • 开发成本节约。在一些即有的 C++ 代码上做封装,开发成本远远低于从零开始写 Node.js 代码。

  • Node.js 无法完成的工作。个别情况,开发者只能得到一个库的静态连接库或者动态链接库以及一堆 C++ 头文件,其余都是黑盒的,这种情况就不得不使用 C++ 进行模块开发了。

本文将从早期的 Node.js 开始,逐渐披露 Node.js 原生 C++ 模块开发方式的变迁。一直到最后,会比较详细地对 Node.js v8.x 新出的原生模块开发接口 N-API 做一次初步的尝试和解析,使得大家对 Node.js 原生 C++ 模块开发的固有印象(认为特别麻烦)有一个比较好的改观,让大家都来尝试一下 Node.js 原生 C++ 模块的开发。

不变应万变

虽然 Node.js 原生 C++ 模块开发方式有了很大的改变,但是有一些内容是不变的,至少到现在来说都是基本上没什么 Breaking 的变化。

原生模块本质

这就要从 Node.js 最本质的 C++ 模块开发讲起了。举个例子,我们在 Linux 下有一个合法的原生模块 ons.node,它其实是一个二进制文件,使用文本编辑器无法正常地看出什么鬼,直到我们遇到了二进制文件查看器。

ons.node 二进制内容

眼尖的同学会看到它的 Magic Number[^1] 是 0x7F454C46,其按位的 ASCII 码代表的字符串是 ELF。于是答案呼之欲出,这就是一个 Linux 下的动态链接库文件。

事实上,不只是在 Linux 中。当一个 Node.js 的 C++ 模块在 OSX 下编译会得到一个后缀是 *.node 本质上是 *.dylib 的动态链接库;而在 Windows 下则会得到一个后缀是 *.node 本质上是 *.dll 的动态链接库。

这么一个模块在 Node.js 中被 require 的时候,是通过 process.dlopen() 对其进行引入的。我们来看一下 Node.js v6.9.4 的 DLOpen[^2] 函数吧:

void DLOpen(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);
  uv_lib_t lib;

  ...

  Local<Object> module = args[0]->ToObject(env->isolate());
  node::Utf8Value filename(env->isolate(), args[1]);

  // 使用 uv_dlopen 加载链接库
  const bool is_dlopen_error = uv_dlopen(*filename, &lib);
  node_module* const mp = modpending;
  modpending = nullptr;

  ...

  // 将加载的链接库句柄转移到 mp 上
  mp->nm_dso_handle = lib.handle;
  mp->nm_link = modlist_addon;
  modlist_addon = mp;

  Local<String> exports_string = env->exports_string();

  // exports_string 其实就是 `"exports"`
  // 这句的意思是 `exports = module.exports`
  Local<Object> exports = module->Get(exports_string)->ToObject(env->isolate());

  if (mp->nm_context_register_func != nullptr) {
    mp->nm_context_register_func(exports, module, env->context(), mp->nm_priv);
  } else if (mp->nm_register_func != nullptr) {
    mp->nm_register_func(exports, module, mp->nm_priv);
  } else {
    uv_dlclose(&lib);
    env->ThrowError("Module has no declared entry point.");
    return;
  }
}

按照逻辑来讲,这个加载过程其实就是下面这样的。

互动评论
评论
Adele3 年前
这样的编程方式一般用于什么样的业务场景?
评论
田永科3 年前
👍清晰
评论
查看更多
微信扫描登录