blog icon indicating copy to clipboard operation
blog copied to clipboard

Node 原生模块杂谈

Open renaesop opened this issue 8 years ago • 0 comments

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

注: 文中的“原生模块”均是指代C++模块

Node如何加载原生模块

朴灵老师的《深入浅出Node.js》一书其实有谈过这个问题,但是随着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,然后返回给调用方

node.gyp工具

renaesop avatar Feb 24 '17 09:02 renaesop