Node 原生模块杂谈

发布于 2022-07-25 12:04:47 字数 5438 浏览 257 评论 0

网上谈 Node C++ 扩展的文章种类比较单一,基本上都是在说怎么去写扩展,而对模块本身的解读相当少,笔者恰巧拜读了相关代码,在此做个记录。注: 文中的 原生模块 均是指代 C++ 模块

Node 如何加载原生模块

但是随着 Node 项目的演进,已经发生了一些微妙的变化。原生模块被存在链表中,原生模块的定义为:

struct node_module {
// 表示node的ABI版本号,node本身导出的符号极少,所以变更基本上由v8、libuv等依赖引起
// 引入模块时,node会检查ABI版本号
// 这货基本跟v8对应的Chrome版本号一样
  int nm_version; 
// 暂时只有NM_F_BUILTIN和0俩玩意
  unsigned int nm_flags;
// 存动态链接库的句柄
  void* nm_dso_handle;
  const char* nm_filename;
// 下面俩函数指针,一个模块只会有一个,用于初始化模块
  node::addon_register_func nm_register_func;
// 这货是那种支持多实例的原生模块,不过扩展写成这个也无法支持原生模块
  node::addon_context_register_func nm_context_register_func;
  const char* nm_modname;
  void* nm_priv;
  struct node_module* nm_link;
};

原生模块被分为了三种,内建(builtint)扩展(addon)已链接的扩展(linked),分别含义为:

  • 内建:Node.js 的原生 C++ 模块,
  • 扩展: 用 require 来进行引入的模块
  • 已链接的扩展:非 Node 原生模块,但是链接到了 node 可执行文件上(这货几乎没用)

所有原生模块的加载均使用的是 extern "C" void node_module_register(void* mod) 函数,而 mod 这个参数实际上就是上面的 node_module,不过 node_module 被放在了 node 这个 namespace 中,所以只能设置为 void*, 函数的实现很简单:

extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(m);
  // node实例创建之前注册的模块挂对应链表上
  if (mp->nm_flags & NM_F_BUILTIN) {
    mp->nm_link = modlist_builtin;
    modlist_builtin = mp;
  } else if (!node_is_initialized) {
    // "Linked" modules are included as part of the node project.
    // Like builtins they are registered *before* node::Init runs.
    mp->nm_flags = NM_F_LINKED;
    mp->nm_link = modlist_linked;
    modlist_linked = mp;
  } else {
// 这货是调用`process.dlopen`时出现
    modpending = mp;
  }
}

不过代码里面并不会直接去调用 node_module_register,而是通过宏来生成调用这个函数的代码:

  • NODE_MODULE: 普通的原生模块
  • NODE_MODULE_CONTEXT_AWARE: 支持单进程多node实例的原生模块
  • NODE_MODULE_CONTEXT_AWARE_BUILTIN: 内建模块均支持多实例,跟上个宏只是多一个 flag

这些宏的作用都是使得模块的注册在main函数之前发生(如果模块被链接到了 node 上),或者在 uv_dlopen 返回前完成。值得注意的是,真正的模块初始化是要执行 nm_**_register_func 的。

内存中共有四个存储 node_module 的链表,均是 static 变量(所以并不是线程安全的…),分别为:

  • modpending: 主要用于加载 C++ addon 时传递当前加载的模块
  • modlist_builtin: 存储内建模块的链表,process.binding 函数会查找这个链表来获取模块并初始化
  • modlist_linked: 存储已链接模块, process._linkedBinding 函数查此表
  • modlist_addon: 存储 C++ addon,可能会问为啥有了modpending还会要这货,实际上当单进程有多个node实例时,都依赖C++ addon时第二次加载动态链接库时,不会设定modpending,但是现在 node 并没有解决这个问题,这个变量应该是准备用来辅助解决这个问题的。

模块在被实际使用时(也就是 require 时),才会被初始化(执行 nm_**_register_func)好,初始化完当然大家都知道会缓存起来。大多数内建模块并不会一开始就被初始化,所以 node 启动时的开销相当小。内建模块都会被包装一下,这些包装模块会去调用 process.binding 获取到原生模块,而启动node时对包装模块的引用在 lib/internal/bootstrap_node.js 中可以找到(主要是fs等)。

模块加载的细节到这里基本上就差不多, 因为我们更可能接触扩展模块的编写,所以详细说说扩展模块。

C++ addon 的加载

我们知道,引用一个原生扩展的方式是 require('./xxx/xxx.node'),而 Node.js 的 require 支持所谓的 扩展,也就是针对不同的后缀可以实现不同的加载方式(这就是所谓的 loader,babel-register 就是利用了这货),具体代码是:

// 位置: lib/module.js
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  return process.dlopen(module, path._makeLong(filename));
};

这货就是仅仅调用了 process.dlopen 嘛,而既然是要跟 C++ 模块通信,那么肯定 process.dlopen 也是 C++ 的比较合适咯,的确,这个函数就是用 C++ 写的 ~,这个函数有点长,主要的逻辑如下:

......
  uv_lib_t lib;
  CHECK_EQ(modpending, nullptr);
......
  const bool is_dlopen_error = uv_dlopen(*filename, &lib);

  node_module* const mp = modpending;
  modpending = nullptr;
......
  mp->nm_dso_handle = lib.handle;
  mp->nm_link = modlist_addon;
  modlist_addon = mp;
......
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;
  }
......

上述代码中 mp->nm_priv 可以直接忽略,以为都被设置成了 NULL

主要逻辑是:

  1. 确定 modpending 为空,非空直接crash
  2. 使用 uv_dlopen 加载动态链接库(也就是编译好的扩展),这个函数执行过程是会运行 node_module_register
  3. 通过 modpending 获取到当前模块(很久以前使用 uv_dlsym
  4. 置空 modpending,将 handler存储起来,在多实例环境中可能是有用的,可以帮助实例的销毁
  5. 真正初始化 module,然后返回给调用方

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
23 人气
更多

推荐作者

娇女薄笑

文章 0 评论 0

biaggi

文章 0 评论 0

xiaolangfanhua

文章 0 评论 0

rivulet

文章 0 评论 0

我三岁

文章 0 评论 0

薆情海

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文