blog
blog copied to clipboard
Node 原生模块杂谈
网上谈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
。
主要逻辑是:
- 确定
modpending
为空,非空直接crash - 使用
uv_dlopen
加载动态链接库(也就是编译好的扩展),这个函数执行过程是会运行node_module_register
- 通过
modpending
获取到当前模块(很久以前使用uv_dlsym
) - 置空
modpending
, 将handler存储起来,在多实例环境中可能是有用的,可以帮助实例的销毁 - 真正初始化module,然后返回给调用方