网上谈 Node C++ 扩展的文章种类比较单一,基本上都是在说怎么去写扩展,而对模块本身的解读相当少,笔者恰巧拜读了相关代码,在此做个记录。注: 文中的 原生模块 均是指代 C++ 模块
但是随着 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),分别含义为:
所有原生模块的加载均使用的是 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,而是通过宏来生成调用这个函数的代码:
这些宏的作用都是使得模块的注册在main函数之前发生(如果模块被链接到了 node 上),或者在 uv_dlopen 返回前完成。值得注意的是,真正的模块初始化是要执行 nm_**_register_func 的。
内存中共有四个存储 node_module 的链表,均是 static 变量(所以并不是线程安全的…),分别为:
模块在被实际使用时(也就是 require 时),才会被初始化(执行 nm_**_register_func)好,初始化完当然大家都知道会缓存起来。大多数内建模块并不会一开始就被初始化,所以 node 启动时的开销相当小。内建模块都会被包装一下,这些包装模块会去调用 process.binding 获取到原生模块,而启动node时对包装模块的引用在 lib/internal/bootstrap_node.js 中可以找到(主要是fs等)。
模块加载的细节到这里基本上就差不多, 因为我们更可能接触扩展模块的编写,所以详细说说扩展模块。
我们知道,引用一个原生扩展的方式是 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。
主要逻辑是:

