Node 原生模块杂谈
网上谈 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
。
主要逻辑是:
- 确定
modpending
为空,非空直接crash - 使用
uv_dlopen
加载动态链接库(也就是编译好的扩展),这个函数执行过程是会运行node_module_register
- 通过
modpending
获取到当前模块(很久以前使用uv_dlsym
) - 置空
modpending
,将 handler存储起来,在多实例环境中可能是有用的,可以帮助实例的销毁 - 真正初始化 module,然后返回给调用方
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论