We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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 C++扩展的文章种类比较单一,基本上都是在说怎么去写扩展,而对模块本身的解读相当少,笔者恰巧拜读了相关代码,在此做个记录。
注: 文中的“原生模块”均是指代C++模块
朴灵老师的《深入浅出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),分别含义为:
所有原生模块的加载均使用的是extern "C" void node_module_register(void* mod)函数,而mod这个参数实际上就是上面的node_module,不过node_module被放在了node这个namespace中,所以只能设置为void*, 函数的实现很简单:
extern "C" void node_module_register(void* mod)
node_module
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_register
NODE_MODULE
NODE_MODULE_CONTEXT_AWARE
NODE_MODULE_CONTEXT_AWARE_BUILTIN
这些宏的作用都是使得模块的注册在main函数之前发生(如果模块被链接到了node上),或者在uv_dlopen返回前完成。值得注意的是,真正的模块初始化是要执行nm_**_register_func的。
main
uv_dlopen
nm_**_register_func
内存中共有四个存储node_module的链表,均是static变量(所以并不是线程安全的……),分别为:
modpending
modlist_builtin
process.binding
modlist_linked
process._linkedBinding
modlist_addon
模块在被实际使用时(也就是require时),才会被初始化(执行nm_**_register_func)好,初始化完当然大家都知道会缓存起来。大多数内建模块并不会一开始就被初始化,所以node启动时的开销相当小。内建模块都会被包装一下,这些包装模块会去调用process.binding获取到原生模块,而启动node时对包装模块的引用在lib/internal/bootstrap_node.js中可以找到(主要是fs等)。
require
lib/internal/bootstrap_node.js
模块加载的细节到这里基本上就差不多, 因为我们更可能接触扩展模块的编写,所以详细说说扩展模块。
我们知道,引用一个原生扩展的方式是require('./xxx/xxx.node'),而Node.js的require支持所谓的“扩展”,也就是针对不同的后缀可以实现不同的加载方式(这就是所谓的loader,babel-register就是利用了这货),具体代码是:
require('./xxx/xxx.node')
// 位置: 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++写的~,这个函数有点长,主要的逻辑如下:
process.dlopen
...... 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。
mp->nm_priv
NULL
主要逻辑是:
uv_dlsym
The text was updated successfully, but these errors were encountered:
No branches or pull requests
网上谈Node C++扩展的文章种类比较单一,基本上都是在说怎么去写扩展,而对模块本身的解读相当少,笔者恰巧拜读了相关代码,在此做个记录。
Node如何加载原生模块
朴灵老师的《深入浅出Node.js》一书其实有谈过这个问题,但是随着Node项目的演进,已经发生了一些微妙的变化。
原生模块被存在链表中,原生模块的定义为:
原生模块被分为了三种,内建(builtint)、扩展(addon)、已链接的扩展(linked),分别含义为:
所有原生模块的加载均使用的是
extern "C" void node_module_register(void* mod)
函数,而mod这个参数实际上就是上面的node_module
,不过node_module
被放在了node这个namespace中,所以只能设置为void*
, 函数的实现很简单:不过代码里面并不会直接去调用
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就是利用了这货),具体代码是:这货就是仅仅调用了
process.dlopen
嘛,而既然是要跟C++模块通信,那么肯定process.dlopen
也是C++的比较合适咯,的确,这个函数就是用C++写的~,这个函数有点长,主要的逻辑如下:上述代码中
mp->nm_priv
可以直接忽略,以为都被设置成了NULL
。主要逻辑是:
modpending
为空,非空直接crashuv_dlopen
加载动态链接库(也就是编译好的扩展),这个函数执行过程是会运行node_module_register
modpending
获取到当前模块(很久以前使用uv_dlsym
)modpending
, 将handler存储起来,在多实例环境中可能是有用的,可以帮助实例的销毁node.gyp工具
The text was updated successfully, but these errors were encountered: