メインコンテンツへ移動

図解 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++ モジュール登録の 2 部分です

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++ 部分コードの末尾には、1 行の登録コードがあります。例えば:

// 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):

参考資料

コメント

コメントはまだありません

コメントを書く