跳到主要內容
黯羽輕揚每天積累一點點

圖解 Node 模塊加載原理

免費2020-05-11#Node#nodejs native module self-register#how nodejs native module works#nodejs js2c#Node 启动流程#Node 核心模块#Nodejs 源码分析

require 支持的這 4 種模塊到底是怎麼加載的?

一、模塊類型

Node.js 默認支持 2 種模塊:

  • 核心模塊(Core Modules):編譯成二進制,其源碼位於 lib/ 目錄下

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

由易到難,先看最常打交道的 JS 模塊

二、JS 模塊

[caption id="attachment_2169" align="alignnone" width="496"]js module js module[/caption]

注意一個細節,是在加載&執行模塊文件前會先緩存 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 模塊加載機制](/articles/node 模塊加載機制/)

三、JSON 模塊

類似於 JS 模塊,JSON 文件也可以作為模塊直接通過 require 加載,具體流程如下:

[caption id="attachment_2170" align="alignnone" width="541"]json module json module[/caption]

除加載&執行方式不同外,與 JS 模塊的加載流程完全一致

四、C++ 擴展模塊

與 JS、JSON 模塊相比,C++ 擴展模塊(.node)的加載過程與 C++ 層關係更密切:

[caption id="attachment_2171" align="alignnone" width="532"]addon module addon module[/caption]

JS 層的處理流程到 process.dlopen() 為止,實際加載、執行、以及擴展模塊暴露出的屬性/方法如何傳入 JS 運行時都是由 C++ 層來完成的:

[caption id="attachment_2172" align="alignnone" width="625"]addon module cpp addon module cpp[/caption]

關鍵在於通過 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++ 擴展入門指南](/articles/node-js-c 擴展入門指南/)

五、核心模塊

類似於 C++ 擴展模塊,核心模塊實現上大多依賴相應的下層 C++ 模塊(如文件 I/O、網絡請求、加密/解密等),只是通過 JS 封裝出面向用戶的上層接口(如 fs.writeFilefs.writeFileSync 等)

本質上都是 C++ 類庫,最主要的區別在於核心模塊會被編譯到 Node.js 安裝包中(包括上層封裝的 JS 代碼,編譯時就已經鏈接到可執行文件中了),而擴展模塊需要在運行時動態加載

P.S. 關於 C++ 動態鏈接庫、靜態庫的更多信息,見 [Node.js C++ 擴展入門指南](/articles/node-js-c 擴展入門指南/#articleHeader1)

因此,與前幾種模塊相比,核心模塊的加載過程稍複雜些,分為 4 部分:

  • (預編譯階段)「編譯」JS 代碼

  • (啟動時)加載 JS 代碼

  • (啟動時)註冊 C++ 模塊

  • (運行時)加載核心模塊(包括 JS 代碼及其引用到的 C++ 模塊)

[caption id="attachment_2173" align="alignnone" width="625"]core module core module[/caption]

其中比較有意思的是 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):

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論