模块initpkidll加载失败 请确保该二进制_5 图看懂 Node 模块加载原理

本文详细介绍了Node.js的模块加载原理,包括JS模块、JSON模块、C++扩展模块和核心模块。重点解析了C++扩展模块的加载过程,涉及到C++动态链接库的加载与自注册机制,以及核心模块的JS2C转换和C++模块注册。通过对Node.js源码的解读,帮助理解模块系统的工作方式。

4a8a3de8fa9018c43febf8f5a362a172.png

关注「前端向后」微信公众号,你将收获一系列「用心原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术

一.模块类型

Node.js 默认支持 2 种模块:

  • 核心模块(Core Modules):编译成二进制,其源码位于lib/目录下

  • 文件模块(File Modules):包括 JavaScript 文件(.js)、JSON 文件(.json)、C++扩展文件(.node)

由易到难,先看最常打交道的 JS 模块

二.JS 模块

618c8f1bf6b06219aa9293c6324eea72.png

js module

注意一个细节,是在加载&执行模块文件前会先缓存module实例,而不是之后才缓存,这是Node.js 能够从容应对循环依赖的根本原因

When there are circular require() calls, a module might not have finished executing when it is returned.

如果模块加载过程中出现了循环引用,导致尚未加载完成的模块被引用到,按照图示的模块加载流程也会命中缓存(而不至于进入死递归),即便此时的module.exports可能不完整(模块代码没执行完,有些东西还没挂上去)

P.S.关于如何根据模块标识找到对应模块(入口)文件的绝对路径,同名模块加载优先级,以及相关 Node.js 源码的解读,见详解Node 模块加载机制

三.JSON 模块

类似于 JS 模块,JSON 文件也可以作为模块直接通过require加载,具体流程如下:

28b2ce7a94b842a361327e7fe8ba1196.png

json module

除加载&执行方式不同外,与 JS 模块的加载流程完全一致

四.C++扩展模块

与 JS、JSON 模块相比,C++扩展模块(.node)的加载过程与 C++层关系更密切:

90bb93d2b9826db59b4419846f52898d.png

addon module

JS 层的处理流程到process.dlopen()为止,实际加载、执行、以及扩展模块暴露出的属性/方法如何传入 JS 运行时都是由 C++层来完成的:

58dbf54c4fdbce35c7ad86fe12b9c9f5.png

addon module cpp

关键在于通过dlopen()/uv_dlopen加载 C++动态链接库(即.node文件)。相关 Node.js 源码见(Node v14.0.0):

  • 模块加载:DLOpen、DLib::Open、DLib::Close

  • 模块自注册:NODE_MODULE宏、node_module_register

  • N-API:napi_module_register_by_symbol

之所以能够从外部取到扩展模块的module实例,是因为扩展模块有自注册机制

// 模块注册时
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast(m);
if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = 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 {
// 将模块实例挂到全局变量上,暴露出去
thread_local_modpending = mp;
}
}
// 加载模块时
void DLOpen(const FunctionCallbackInfo& args) {
/* ...略去部分非关键代码 */
const bool is_opened = dlib->Open();
// 加载动态链接库后,读全局变量,取出模块实例
node_module* mp = thread_local_modpending;
thread_local_modpending = nullptr;
// 最后将 exports 和 module 传给模块入口函数,把模块暴露出的属性/方法带出来
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, context, mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
}
}

P.S.关于 C++扩展模块开发、编译、运行的详细信息,见Node.js C++扩展入门指南

五.核心模块

类似于 C++扩展模块,核心模块实现上大多依赖相应的下层 C++模块(如文件 I/O、网络请求、加密/解密等),只是通过 JS 封装出面向用户的上层接口(如fs.writeFilefs.writeFileSync等)

本质上都是 C++类库,最主要的区别在于核心模块会被编译到 Node.js 安装包中(包括上层封装的 JS 代码,编译时就已经链接到可执行文件中了),而扩展模块需要在运行时动态加载

P.S.关于 C++动态链接库、静态库的更多信息,见Node.js C++扩展入门指南

因此,与前几种模块相比,核心模块的加载过程稍复杂些,分为 4 部分:

  • (预编译阶段)“编译”JS 代码

  • (启动时)加载 JS 代码

  • (启动时)注册 C++模块

  • (运行时)加载核心模块(包括 JS 代码及其引用到的 C++模块)

bcd511478a5b99fc315fece878aab00f.png

core module

其中比较有意思的是 JS2C 转换与核心 C++模块注册两部分

JS2C 转换

通过编译前的预处理,核心模块的 JS 代码部分被转成了 C++文件(位于./out/Release/obj/gen/node_javascript.cc),进而打入可执行文件中:

NativeModule: a minimal module system used to load the JavaScript core modules found in lib/**/*.js and deps/**/*.js. All core modules are compiled into the node binary via node_javascript.cc generated by js2c.py, so they can be loaded faster without the cost of I/O. This class makes the lib/internal/*, deps/internal/* modules and internalBinding() available by default to core modules, and lets the core modules require itself via require(‘internal/bootstrap/loaders’) even when this file is not written in CommonJS style.

(摘自node/lib/internal/bootstrap/loaders.js)

生成的node_javascript.cc主要内容如下:

static const uint8_t internal_bootstrap_environment_raw[] = {
39,117,115,101, 32,115,116,114,105, 99,116, 39, 59, 10, 10, 47, 47, 32, 84,104,105,115, 32,114,117,110,115, 32,110,101,
99,101,115,115, 97,114,121, 32,112,114,101,112, 97,114, 97,116,105,111,110,115, 32,116,111, 32,112,114,101,112, 97,114
// ...
}

void NativeModuleLoader::LoadJavaScriptSource() {
source_.emplace("internal/bootstrap/environment", UnionBytes{internal_bootstrap_environment_raw, 374});
source_.emplace("internal/bootstrap/loaders", UnionBytes{internal_bootstrap_loaders_raw, 10110});
// ...
}

UnionBytes NativeModuleLoader::GetConfig() {
return UnionBytes(config_raw, 3030); // config.gypi
}

也就是说,翻遍源码也找不到的LoadJavaScriptSource其实是在预编译阶段自动生成的:

// ref https://github.com/nodejs/node/blob/v14.0.0/src/node_native_module.cc#L24
NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
// 该函数的实现不在源码中,而是位于编译生成的 node_javascript.cc 中
LoadJavaScriptSource();
}

核心 C++模块注册

所有核心模块依赖的 C++部分代码末尾都有一行注册代码,例如:

// src/node_file.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
// src/timers.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(timers, node::Initialize)
// src/js_stream.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(js_stream, node::JSStream::Initialize)

NODE_MODULE_CONTEXT_AWARE_INTERNAL宏展开之后是node_module_register,将注册过来的 C++模块记录到modlist_internal链表中:

extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast(m);
if (mp->nm_flags & NM_F_INTERNAL) {
// 记录内部C++模块
mp->nm_link = modlist_internal;
modlist_internal = 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 {
thread_local_modpending = mp;
}
}

运行时通过internalBinding加载这些内置的 C++模块

相关 Node.js 源码见(Node v14.0.0):

  • JS 层模块加载:Module._load、loadNativeModule、compileForInternalLoader、nativeModuleRequire、internalBinding

  • JS2C 转换:tools/js2c.py、LoadJavaScriptSource、NativeModule.map、moduleIds、ModuleIdsGetter、GetModuleIds

  • 核心 C++模块注册:NODE_MODULE_CONTEXT_AWARE_INTERNAL、node_module_register、InitModule

  • C++层模块加载:internalBinding、getInternalBinding、FindModule、InitModule

参考资料

  • Modules

联系我      

如果心中仍有疑问,请查看原文并留下评论噢。(特别要紧的问题,可以直接微信联系 ayqywx )

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值