Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node 原生模块杂谈 #21

Open
renaesop opened this issue Feb 24, 2017 · 0 comments
Open

Node 原生模块杂谈 #21

renaesop opened this issue Feb 24, 2017 · 0 comments

Comments

@renaesop
Copy link
Owner

renaesop commented Feb 24, 2017

网上谈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 renaesop changed the title Node C++ addon杂谈 Node 原生模块杂谈 Feb 25, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant