一.モジュールタイプ
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[/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[/caption]
読み込み&実行方式が異なる以外は、JS モジュールの読み込みフローと完全に一致
四.C++ 拡張モジュール
JS、JSON モジュールと比較して、C++ 拡張モジュール(.node)の読み込みプロセスは C++ 層との関係がより密接です:
[caption id="attachment_2171" align="alignnone" width="532"]
addon module[/caption]
JS 層の処理フローはprocess.dlopen() までで、実際の読み込み、実行、および拡張モジュールが露出する属性/メソッドをどのように JS ランタイムに渡すかは、すべて C++ 層によって完了されます:
[caption id="attachment_2172" align="alignnone" width="625"]
addon module cpp[/caption]
鍵は dlopen()/uv_dlopen を通じて C++ 動的リンクライブラリ(つまり.node ファイル)を読み込むことです。関連 Node.js ソースコードは以下の通り(Node v14.0.0):
-
モジュール読み込み:DLOpen、DLib::Open、DLib::Close
-
モジュール自己登録:NODE_MODULE マクロ、node_module_register
外部から拡張モジュールの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.writeFile、fs.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[/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):
-
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
コメントはまだありません