图解Node模块加载原理

一.模块类型

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

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

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

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

二.JS 模块

js module

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加载,具体流程如下:

json module

json module

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

四.C++扩展模块

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

addon module

addon module

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

addon module cpp

addon module cpp

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

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

// 模块注册时
extern "C" void node_module_register(void* m) {
  struct node_module* mp = reinterpret_cast<struct node_module*>(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<Value>& 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++模块)

core module

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<struct node_module*>(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):

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code