今日から始めるNode.jsコードリーディング - libuv / V8 JavaScriptエンジン / Node.jsによるスクリプトの実行
ソフトウェアを正しく理解する唯一の方法はコードを読むことです。
ドキュメントを読めばそのソフトウェアが何を実装しているか分かりますが、どのように実装されているかまでは分かりません。
開発中に何らかのトラブルに悩まされたときや、効率的なコーディングをしたいと思ったとき、下位レイヤのソフトウェアを理解しておけば素早く対処できるシーンが多くあります。
ただ、コードを読むことは簡単なタスクではありません。
現代的なソフトウェアはそれなりの規模のコードを含んでいることがほとんどです。アーキテクチャ間の差異を吸収するためのコードなど、本質的な機能を理解する上ではあまり重要ではないコードも含まれています。
何らかの問題が発生してからコードを読もうと思っても、準備なしでは関連する箇所を探すだけでかなりの労力が必要な作業となります。
従って、普段からコードを読んでおくことが重要です。
また、コードを読むにあたり、解説があれば迷うことなく重要な箇所へ読み進めることができます。詳解 Linuxカーネル
やBSDカーネルの設計と実装
などの解説本を片手にカーネルのコードを読んだことのある方も多いのではないかと思います。
今回のエントリーでは、Tokyo Otaku Modeのアプリケーションサーバに使用しているNode.jsのコードを読み、Node.jsの主要コンポーネントであるlibuv
とV8 JavaScriptエンジン
と、ユーザーが記述したスクリプトをnodeバイナリが実行するまでをご説明したいと思います。
スクリプト言語の場合、自分で書いたスクリプトを動かしながらコードを読むケースも多いと思いますが、Node.jsの起動シークエンスはV8の初期化後JavaScriptから使用するオブジェクトの初期化をC++で直接行い、最終的にsrc/node.jsブートストラップスクリプトからユーザーのスクリプトが起動されるダイナミックなコードになっています。大変読み応えのあるコードであると同時に、Node.jsの他の部分を理解する上で必要となる前提知識を効率的に吸収できる部分です。
今までNode.jsのコードを読んだことがないという方にお勧めの内容です。
一緒にコードリーディングをはじめましょう。
1. はじめに
Node.jsはC/C++とJavaScriptで記述されたJavaScript実行環境で、主にWebアプリケーションサーバとして利用されています。プログラミング言語については説明しておりませんのでご了承ください。
また、執筆時点の2014年8月8日のstable版であるNode.js-0.10.30を元に構成しております。
最初にGithubからNode.jsをcloneし、v0.10.30-releaseタグをcheckoutしてください。
$ git clone https://github.com/joyent/node $ git checkout v0.10.30-release
2. Node.jsの主要コンポーネント
Node.jsの特徴はI/O処理を非同期で実行する点です。マルチスレッドサーバに起こり得るC10K問題の解決に期待され注目されました。
コードを読むにあたり非同期I/Oがどのように実装されているかが最も気になるところかと思いますが、現在の実装では非同期I/Oに関するコードはlibuvに分離されておりNode.js本体には含まれていません。
Node.jsはJavaScriptの実装にV8 JavaScriptエンジンを使用しており、主なコンポーネントはlibuv、V8 JavaScriptエンジンとNode.js本体となります。
本体のコードを読む前にlibuvとV8 JavaScriptエンジンの使用方法を把握しておく必要がありますので、まずはlibuvから見ていくことにします。
3. libuv
libuvはNode.jsのために開発された非同期I/Oライブラリで、Rust、Luvit、Julia、pyuv、その他のプロジェクトでも使用されています。(libuvのREADMEより)
libuv is a multi-platform support library with a focus on asynchronous I/O.
It was primarily developed for use by Node.js, but it’s also used by Mozilla’s Rust language, Luvit, Julia, pyuv, and others.
https://github.com/joyent/libuv
libuv自体はGithubやtarballでダウンロードしたNode.jsのアーカイブに同梱されていますが、開発は別のリポジトリで行われているようです。
READMEにlibuvの主な機能の一覧(Feature highlights
)が書かれており、ネットワーキングを含むI/O関係の処理と、プロセス、スレッド、シグナル、タイマー関係の非同期I/O版の処理が実装されています。
- Full-featured event loop backed by epoll, kqueue, IOCP, event ports.
- Asynchronous TCP and UDP sockets
- Asynchronous DNS resolution
- Asynchronous file and file system operations
- File system events
- ANSI escape code controlled TTY
- IPC with socket sharing, using Unix domain sockets or named pipes (Windows)
- Child processes
- Thread pool
- Signal handling
- High resolution clock
- Threading and synchronization primitives
libuvはCで書かれています。
3-1. libuvを使ったサンプルプログラム
libuvについてはuvbookという纏まったドキュメントがありますので、各機能の詳細に興味がある方はそちらを読んで頂きたいと思いますが、ファイルシステム周りのコードを理解しておくとNode.jsを理解する上で最低限必要なlibuvの仕組みを把握できますので、uvbookのFilesystemで紹介されているuvcat(を若干変えたもの)を以下に掲載します。
#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <uv.h> static uv_fs_t open_req, read_req, write_req; static char buffer[1024]; static void on_open(uv_fs_t *req); static void on_read(uv_fs_t *req); static void on_write(uv_fs_t *req); static void on_open(uv_fs_t *req) { if (req->result == -1) { fprintf(stderr, "Failed to open file: %s\n", strerror(errno)); exit(1); } uv_fs_read(uv_default_loop(), &read_req, req->result, buffer, sizeof(buffer), -1, on_read); uv_fs_req_cleanup(req); } static void on_read(uv_fs_t *req) { uv_fs_req_cleanup(req); if (req->result < 0) { fprintf(stderr, "Read error: %s\n", uv_strerror(uv_last_error(uv_default_loop()))); exit(1); } if (req->result == 0) { uv_fs_t close_req; /* synchronous */ uv_fs_close(uv_default_loop(), &close_req, open_req.result, NULL); } else { uv_fs_write(uv_default_loop(), &write_req, 1, buffer, req->result, -1, on_write); } } static void on_write(uv_fs_t *req) { uv_fs_req_cleanup(req); if (req->result < 0) { fprintf(stderr, "Write error: %s\n", uv_strerror(uv_last_error(uv_default_loop()))); exit(1); } uv_fs_read(uv_default_loop(), &read_req, open_req.result, buffer, sizeof(buffer), -1, on_read); } int main(int argc, char *argv[]) { uv_fs_open(uv_default_loop(), &open_req, argv[1], O_RDONLY, 0, on_open); uv_run(uv_default_loop(), UV_RUN_DEFAULT); return 0; }
上のコードでは以下の処理を行っています。
uv_fs_
プレフィクスを持つ関数は、libuvのファイルシステム関数です。uv_fs_open
、uv_fs_read
、uv_fs_write
は、システムコールのopen
、read
、write
に対応します。uv_fs_req_cleanup
はlibuvが内部的に使用したメモリを開放します。uv_run
はlibuvのイベントループを実装した関数です。main
関数から呼ばれたuv_fs_open
は、必要な処理を行いuv_default_loop
が返すデフォルトループへイベントを登録するとmain
関数へ戻ります。uv_run
はデフォルトループに登録されたイベントを問い合わせ、ディスパッチ可能なイベントがあればイベントハンドラを実行します。uv_fs_open
の場合、関数呼び出し時に引数に渡されたon_open
がイベントハンドラとなります。ファイルがオープンされると
uv_run
はon_open
イベントハンドラを呼び出します。on_open
イベントハンドラは、イベントハンドラon_read
を引数にuv_fs_read
を呼び出しuv_run
へ戻ります。ファイルからデータが読み込まれると
uv_run
はon_read
イベントハンドラを呼び出し、uv_fs_write
によりon_write
イベントハンドラが登録され、書き込みが完了すると再度uv_fs_read
が呼び出されon_read
イベントハンドラが登録されます。on_read
が呼び出された時点でファイルがEOFに到達している場合はuv_fs_close
が実行されます。uv_fs_close
は(コールバックにNULLが指定されているため)同期実行されます。そのためon_read
終了時点でuv_run
から呼び出し可能なイベントが無くなり制御がmain
へ戻りプログラムが終了します。
と、言葉にするといまいち分かりにくい説明ですが、Node.jsでI/Oコールバックを書くのと同じような処理をCで書いてる感じです。
上のコードと等価なコードを普通のCで(なるべく簡単に)書くと以下のようになります。単にcatするコードを書くだけで考えるとlibuvはかなり分かりにくいです。
#include <stdio.h> int main(int argc, char *argv[]) { int c; FILE *fp; fp = fopen(argv[1], "r"); while ((c = getc(fp)) != EOF) putchar (c); fclose(fp); return 0; }
C++11のラムダ関数とかを使えば多少コードが分かりやすくなる気がしますが、本質的なややこしさはあまり変わらないような気がします。
ネットワーク関係などのlibuvの他の機能も、システムコールやstdcにある関数の非同期I/O版が実装されている形になっており、上記のコードと同じようにコールバックで結果を受け取ります。Unix系のプログラミングをしたことのある方ならだいたい関数名を見れば何の関数の非同期版か分かると思います。
なお、(非同期I/Oですので)uv_run
を呼び出してイベントループを実行しないとコールバックに処理結果は返されません。
4. V8 JavaScriptエンジン
V8 JavaScriptエンジンはGoogleが開発しているJavaScriptエンジンです。Google Chromeにも使用されています。
Node.jsはV8 JavaScriptエンジンを組み込む形で利用します。Node.jsからMySQL等の組み込み処理系を使用するのと同じような感じです。組み込みでのV8の利用方法については、Chrome V8の以下のドキュメントに目を通しておけば概ね理解できると思います。
なお、node-0.10.30に同梱されているV8 JavaScriptエンジンのバージョンは若干古い3.14.5となっています。以下はGetting Started
のHello World
を3.14.5用に書き換えてみたものですが、
#include <v8.h> using namespace v8; int main(int argc, char* argv[]) { // Create a new context. Persistent<Context> context = Context::New(); // Enter the context for compiling and running the hello world script. Context::Scope context_scope(context); { HandleScope scope; // Create a string containing the JavaScript source code. Local<String> source = String::New("'Hello' + ', World!'"); // Compile the source code. Local<Script> script = Script::Compile(source); // Run the script to get the result. Local<Value> result = script->Run(); // Convert the result to an UTF8 string and print it. String::Utf8Value utf8(result); printf("%s\n", *utf8); } return 0; }
最新のV8の3.27.7と比べるとContextがPersistentテンプレートのオブジェクトとなっていたり、ローカル変数を返す際にHandleScope.CloseではなくEscapeHandleScope.Escapeを使用する必要がある等、Chrome V8ドキュメントの内容と異なる箇所が何カ所かあります。
Node.jsのコードを読むだけなら、Local<String> source = String::New("'Hello' + ', World!'");
がV8エンジン上のローカル変数source
に'Hello' + ', World!'
を代入してるのね、程度を理解できていれば問題ないと思います。必要な部分については以下のNode.jsのコードに補足を入れていますのでそちらをご覧ください。
5. Node.js
libuv、V8 JavaScriptエンジンとNode.jsが利用するコンポーネントを見てきましたので、Node.js本体のコードを見ていこうと思います。コマンドラインからnode
を起動し、ユーザーが記述したスクリプトを実行するまでを見て行きます。
5-1. デバッグビルドの作成
コードリーディングにはいくつかの手法があるようですが、デバッガで実行しながらコードを読む方法が分かりやすいと思います。
Node.jsでは--debug
オプションを指定してconfigureを実行するとデバッグビルドを作成できます。
$ cd node $ ./configure --debug $ make [...] ln -fs out/Debug/node node_g
以上でデバッギングオプション付きでコンパイルされたnodeバイナリがout/Debug/node
に作成され、デバッグ用バイナリへのシンボリックリンクnode_g
がソースツリーのルートディレクトリに作成されます。(ビルド方法は環境によって異なりますので、詳しくはBuilding and installing Node.jsをご覧ください。)
lldbやgdb等のデバッガから、node_g
にブレークポイントやトレースポイントを設定して実行できます。(どのデバッガが使用可能かはプラットフォームに依ります。)
$ lldb node_g (lldb) b main runBreakpoint 1: 13 locations. (lldb) run Process 29273 launched: '/src/node/node_g' (x86_64) 7 locations added to breakpoint 1 1 location added to breakpoint 1 4 locations added to breakpoint 1 Process 29273 stopped * thread #1: tid = 0xcdfd0, 0x00000001006b81b6 node_g`main(argc=1, argv=0x00007fff5fbff920) + 22 at node_main.cc:65, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x00000001006b81b6 node_g`main(argc=1, argv=0x00007fff5fbff920) + 22 at node_main.cc:65 62 #else 63 // UNIX 64 int main(int argc, char *argv[]) { -> 65 return node::Start(argc, argv); 66 } 67 #endif (lldb)
5-2. ソースツリー
今回はユーザーの記述したJavaScriptをNode.jsが実行するところまでを見て行こうと思いますので、cloneされたリポジトリ上の以下のディレクトリ/ファイルを主に参照します。
- src: Node.js本体のソースコードが収められたディレクトリ
- lib: Node.jsに埋め込まれるJavaScriptライブラリのソースコードが収められたディレクトリ
- deps: Node.jsが依存しているライブラリのソースコードが収められたディレクトリ、zlibやOpenSSLなどライブラリが置かれています
- deps/uv: libuv multi-platform support library with a focus on asynchronous I/O
- deps/v8: V8 JavaScript エンジン
- tools: ビルド用ツールが収められたディレクトリ
- node.gyp: ビルドルールを記述したgypファイル
5-3. Node.jsによるスクリプトの実行
5-3-1. main関数
まずNode.jsのエントリポイントですが、src/node_main.ccのmain関数です。
node_main.ccは#ifdef _WIN32
プリプロセッサディレクティブでWindowsとそれ以外の実装が切り替わるようになっています。Windowsではargvがワイド文字列になっているため、WideCharToMultiByte
でマルチバイト文字に変換する処理が入っていますが、特に重要な処理は無いので省略します。
24 #ifdef _WIN32 25 int wmain(int argc, wchar_t *wargv[]) { [...] 59 // Now that conversion is done, we can finally start. 60 return node::Start(argc, argv); 62 #else 63 // UNIX 64 int main(int argc, char *argv[]) { 65 return node::Start(argc, argv); 66 } 67 #endif
main関数はnode::Start
を呼び出し終了します。
5-3-2. node::Start
node::Start
は、src/node.ccで定義されます。
node::Start
の前半ではlibuvやv8 JavaScriptエンジンの初期化コードが実行されます。
3048 int Start(int argc, char *argv[]) { 3049 const char* replaceInvalid = getenv("NODE_INVALID_UTF8"); 3050 3051 if (replaceInvalid == NULL) 3052 WRITE_UTF8_FLAGS |= String::REPLACE_INVALID_UTF8; 3053 3054 // Hack aroung with the argv pointer. Used for process.title = "blah". 3055 argv = uv_setup_args(argc, argv); 3056 3057 // Logic to duplicate argv as Init() modifies arguments 3058 // that are passed into it. 3059 char **argv_copy = copy_argv(argc, argv); 3060 3061 // This needs to run *before* V8::Initialize() 3062 // Use copy here as to not modify the original argv: 3063 Init(argc, argv_copy); 3064 3065 V8::Initialize(); 3066 #if HAVE_OPENSSL 3067 // V8 on Windows doesn't have a good source of entropy. Seed it from 3068 // OpenSSL's pool. 3069 V8::SetEntropySource(crypto::EntropySource); 3070 #endif
初期化コードの実行後、JavaScriptコンテキストオブジェクトPersistent<Context> context
とローカルスコープオブジェクトContext::Scope context_scope
が作成されます。コンテキストオブジェクトはサンドボックス化された実行コンテキストで組み込みオブジェクトや関数を保持します。ローカルスコープオブジェクトは実行コンテキスト上のローカルスコープを表します。
3072 { 3073 Locker locker; 3074 HandleScope handle_scope; 3075 3076 // Create the one and only Context. 3077 Persistent<Context> context = Context::New(); 3078 Context::Scope context_scope(context);
Context
クラスとContext::Scope
クラスは、deps/v8/include/v8.hとdeps/v8/src/api.ccで定義されます。Persistent
はdeps/v8/include/v8.hで定義されるテンプレートクラスで、コンテキストに対して永続的なガベージコレクタで管理されるオブジェクトです。
実行コンテキスト作成後、SetupProcessObjectでprocessクラスを作成した後、v8_typed_array::AttachBindingsで組み込み配列クラスを実行コンテキストのグローバルスコープにバインドします。SetupProcessObject
については後述します。
3080 // Use original argv, as we're just copying values out of it. 3081 Handle<Object> process_l = SetupProcessObject(argc, argv); 3082 v8_typed_array::AttachBindings(context->Global());
SetupProcessObject
により作成されたprocess
クラスを引数にLoad関数を呼び出すと、src/node.jsが読み込まれ実行されます。このsrc/node.js
がJavaScriptのエントリポイントとなります。Load
src/node.js
については後述します。
3084 // Create all the objects, load modules, do everything. 3085 // so your next reading stop should be node::Load()! 3086 Load(process_l);
Load
が終了した時点でJavaScriptの実行が開始されます。node::Start
はuv_run
を呼び出しJavaScriptの終了を待ちます。uv_run
はイベントループを実装したlibuvの関数で、Timerやkqueueやepollによるカーネルイベント等をディスパッチします。
3088 // All our arguments are loaded. We've evaluated all of the scripts. We 3089 // might even have created TCP servers. Now we enter the main eventloop. If 3090 // there are no watchers on the loop (except for the ones that were 3091 // uv_unref'd) then this function exits. As long as there are active 3092 // watchers, it blocks. 3093 uv_run(uv_default_loop(), UV_RUN_DEFAULT);
JavaScriptが終了後(uv_run
が終了後)は処理系の終了処理を行います。process.exit
等でJavaScriptから直接終了した際は、uv_run
からnode::Start
に戻ることはありません。
3095 EmitExit(process_l); 3096 RunAtExit(); 3097 3098 #ifndef NDEBUG 3099 context.Dispose(); 3100 #endif 3101 } 3102 3103 #ifndef NDEBUG 3104 // Clean up. Not strictly necessary. 3105 V8::Dispose(); 3106 #endif // NDEBUG 3107 3108 // Clean up the copy: 3109 free(argv_copy); 3110 3111 return 0; 3112 }
SetupProcessObject
はsrc/node.ccで定義され、C++コード上でprocess
クラスを初期化します。
Persistent
オブジェクトとして実体化されたprocess
クラスに対して、version
等のプロパティがセットされます。これらのプロパティはJavaScriptからアクセス可能です。
2247 Handle<Object> SetupProcessObject(int argc, char *argv[]) { 2248 HandleScope scope; 2249 2250 int i, j; 2251 2252 Local<FunctionTemplate> process_template = FunctionTemplate::New(); 2253 2254 process_template->SetClassName(String::NewSymbol("process")); 2255 2256 process = Persistent<Object>::New(process_template->GetFunction()->NewInstance()); 2257 2258 process->SetAccessor(String::New("title"), 2259 ProcessTitleGetter, 2260 ProcessTitleSetter); 2261 2262 // process.version 2263 process->Set(String::NewSymbol("version"), String::New(NODE_VERSION)); 2264 2265 // process.moduleLoadList 2266 module_load_list = Persistent<Array>::New(Array::New()); 2267 process->Set(String::NewSymbol("moduleLoadList"), module_load_list);
後続するコードでprocess
クラスのメソッドがセットされます。メソッドもプロパティと同じくJavaScriptからアクセス可能です。
2389 // define various internal methods 2390 NODE_SET_METHOD(process, "_getActiveRequests", GetActiveRequests); 2391 NODE_SET_METHOD(process, "_getActiveHandles", GetActiveHandles); 2392 NODE_SET_METHOD(process, "_needTickCallback", NeedTickCallback); 2393 NODE_SET_METHOD(process, "reallyExit", Exit); 2394 NODE_SET_METHOD(process, "abort", Abort); 2395 NODE_SET_METHOD(process, "chdir", Chdir); 2396 NODE_SET_METHOD(process, "cwd", Cwd); 2397 2398 NODE_SET_METHOD(process, "umask", Umask); 2399 2400 #ifdef __POSIX__ 2401 NODE_SET_METHOD(process, "getuid", GetUid); 2402 NODE_SET_METHOD(process, "setuid", SetUid); 2403 2404 NODE_SET_METHOD(process, "setgid", SetGid); 2405 NODE_SET_METHOD(process, "getgid", GetGid); 2406 2407 NODE_SET_METHOD(process, "getgroups", GetGroups); 2408 NODE_SET_METHOD(process, "setgroups", SetGroups); 2409 NODE_SET_METHOD(process, "initgroups", InitGroups); 2410 #endif // __POSIX__ 2411 2412 NODE_SET_METHOD(process, "_kill", Kill); 2413 2414 NODE_SET_METHOD(process, "_debugProcess", DebugProcess); 2415 NODE_SET_METHOD(process, "_debugPause", DebugPause); 2416 NODE_SET_METHOD(process, "_debugEnd", DebugEnd); 2417 2418 NODE_SET_METHOD(process, "hrtime", Hrtime); 2419 2420 NODE_SET_METHOD(process, "dlopen", DLOpen); 2421 2422 NODE_SET_METHOD(process, "uptime", Uptime); 2423 NODE_SET_METHOD(process, "memoryUsage", MemoryUsage); 2424 2425 NODE_SET_METHOD(process, "binding", Binding); 2426 2427 NODE_SET_METHOD(process, "_usingDomains", UsingDomains);
最終的にSetupProcessObject
は初期化されたprocess
クラスを返します。
2429 // values use to cross communicate with processNextTick 2430 Local<Object> info_box = Object::New(); 2431 info_box->SetIndexedPropertiesToExternalArrayData(&tick_infobox, 2432 kExternalUnsignedIntArray, 2433 3); 2434 process->Set(String::NewSymbol("_tickInfoBox"), info_box); 2435 2436 // pre-set _events object for faster emit checks 2437 process->Set(String::NewSymbol("_events"), Object::New()); 2438 2439 return process; 2440 }
Load
はsrc/node.ccで定義され、src/node.jsを読み込み実行します。
2454 void Load(Handle<Object> process_l) { 2455 process_symbol = NODE_PSYMBOL("process"); 2456 domain_symbol = NODE_PSYMBOL("domain"); 2457 2458 // Compile, execute the src/node.js file. (Which was included as static C 2459 // string in node_natives.h. 'natve_node' is the string containing that 2460 // source code.) 2461 2462 // The node.js file returns a function 'f' 2463 atexit(AtExit); 2464 2465 TryCatch try_catch;
src/node.js
はビルド中にC++文字列に変換されnodeのバイナリに埋め込まれます。MainSource
はsrc/node_javascript.ccで定義されます。
ExecuteStringはMainSource
が返すsrc/node.cc
のコードをコンパイルします。
2467 Local<Value> f_value = ExecuteString(MainSource(), 2468 IMMUTABLE_STRING("node.js")); 2469 if (try_catch.HasCaught()) { 2470 ReportException(try_catch, true); 2471 exit(10); 2472 } 2473 assert(f_value->IsFunction());
src/node.js
には無名関数が記述されておりLocal<Function> f
としてアクセスします。
2474 Local<Function> f = Local<Function>::Cast(f_value); 2475 2476 // Now we call 'f' with the 'process' variable that we've built up with 2477 // all our bindings. Inside node.js we'll take care of assigning things to 2478 // their places. 2479 2480 // We start the process this way in order to be more modular. Developers 2481 // who do not like how 'src/node.js' setups the module system but do like 2482 // Node's I/O bindings may want to replace 'f' with their own function.
グローバルコンテキストとprocess
クラスを引数にf->Call
によりsrc/node.js
が実行され、Load
は終了します。
2484 // Add a reference to the global object 2485 Local<Object> global = v8::Context::GetCurrent()->Global(); 2486 Local<Value> args[1] = { Local<Value>::New(process_l) }; 2487 2488 #if defined HAVE_DTRACE || defined HAVE_ETW || defined HAVE_SYSTEMTAP 2489 InitDTrace(global); 2490 #endif 2491 2492 #if defined HAVE_PERFCTR 2493 InitPerfCounters(global); 2494 #endif 2495 2496 f->Call(global, 1, args); 2497 2498 if (try_catch.HasCaught()) { 2499 FatalException(try_catch); 2500 } 2501 }
src/node.jsの主関数となる無名関数は、SetupProcessObject
初期化されたprocess
クラスを引数に受け取ります。
24 // This file is invoked by node::Load in src/node.cc, and responsible for 25 // bootstrapping the node.js core. Special caution is given to the performance 26 // of the startup process, so many dependencies are invoked lazily. 27 (function(process) { 28 this.global = this;
無名関数はユーザーが記述したJavaScriptを実行するstartup
関数を定義し実行します。
30 function startup() { 31 var EventEmitter = NativeModule.require('events').EventEmitter; 32 33 process.__proto__ = Object.create(EventEmitter.prototype, { 34 constructor: { 35 value: process.constructor 36 } 37 }); 38 EventEmitter.call(process); 39 40 process.EventEmitter = EventEmitter; // process.EventEmitter is deprecated 41 42 // do this good and early, since it handles errors. 43 startup.processFatal(); 44 45 startup.globalVariables(); 46 startup.globalTimeouts(); 47 startup.globalConsole(); 48 49 startup.processAssert(); 50 startup.processConfig(); 51 startup.processNextTick(); 52 startup.processStdio(); 53 startup.processKillAndExit(); 54 startup.processSignalHandlers(); 55 56 startup.processChannel(); 57 58 startup.resolveArgv0(); [...] 906 startup(); 907 });
startup.processFatal
等はstartup
のサブルーチンで以下の初期化を実行します。
startup.processFatal
:process._fatalException
を初期化startup.globalVariables
: グローバル変数を初期化startup.globalTimeouts
:setTimeout
、setInterval
、setImmediate
等を初期化startup.globalConsole
:console
クラスを初期化startup.processAssert
:process.assert
を初期化startup.processConfig
:process.config
を初期化startup.processNextTick
:process.nextTick
を初期化startup.processStdio`:
process.std(in|out|err)を初期化
startup.processKillAndExit
:process.exit
、process.kill
を初期化startup.processSignalHandlers
:process.on
、process.(add|remove)Listener
を初期化startup.processChannel
:process.env.NODE_CHANNEL_FD
が指定されている場合通信用のパイプを開くstartup.resolveArgv0
: argv[0]のnode
のパスを必要な場合書き換える
以上でJavaScriptの実行準備がすべて完了します。Node.jsのコマンドライン引数に実行するスクリプトファイルのパスを指定した場合、lib/module.jsのModule.runMain
メソッドが呼ばれ、process.argv[1]
に指定されたファイルが実行されます。
118 // Main entry point into most programs: 119 Module.runMain();
Module.runMain
メソッドはModule._loadメソッドを呼び出します。
494 // bootstrap main module. 495 Module.runMain = function() { 496 // Load the main module--the command line argument. 497 Module._load(process.argv[1], null, true); 498 // Handle any nextTicks added in the first tick of the program 499 process._tickCallback(); 500 };
Module._load
メソッドはモジュールがキャッシュされていないか調べ、キャッシュされていればキャッシュを、キャッシュされていなければModule
オブジェクトを実体化しModule.prototype.load
メソッドを呼び出します。
275 Module._load = function(request, parent, isMain) { 276 if (parent) { 277 debug('Module._load REQUEST ' + (request) + ' parent: ' + parent.id); 278 } 279 280 var filename = Module._resolveFilename(request, parent); 281 282 var cachedModule = Module._cache[filename]; 283 if (cachedModule) { 284 return cachedModule.exports; 285 } 286 287 if (NativeModule.exists(filename)) { [...] 298 } 299 300 var module = new Module(filename, parent); 301 302 if (isMain) { 303 process.mainModule = module; 304 module.id = '.'; 305 } 307 Module._cache[filename] = module; 308 309 var hadException = true; 310 311 try { 312 module.load(filename); 313 hadException = false; 314 } finally { 315 if (hadException) { 316 delete Module._cache[filename]; 317 } 318 } 319 320 return module.exports; 321 };
Module.prototype.load
メソッドはfilename
から拡張子を取り出し、Module._extensions[extension]
メソッドを呼び出します。
346 Module.prototype.load = function(filename) { 347 debug('load ' + JSON.stringify(filename) + 348 ' for module ' + JSON.stringify(this.id)); 349 350 assert(!this.loaded); 351 this.filename = filename; 352 this.paths = Module._nodeModulePaths(path.dirname(filename)); 353 354 var extension = path.extname(filename) || '.js'; 355 if (!Module._extensions[extension]) extension = '.js'; 356 Module._extensions[extension](this, filename); 357 this.loaded = true; 358 };
ファイルの拡張子が.js
の場合、Module._extensions['.js']
メソッドが呼ばれます。このメソッドはreadFileSync
でファイルを読み込みModule.prototype._compile
メソッドでコンパイルします。(Module.prototype._compile
メソッドの詳細は省略しますがrunInThisContext
でスクリプトをコンパイルし実行します。)
このModule.prototype.load
メソッドの挙動は、require
でJavaScriptモジュールを読み込む際も同じです。初回ロード時にreadFileSync
でファイルが読み込まれる点は覚えておいた方が良いと思います。
471 // Native extension for .js 472 Module._extensions['.js'] = function(module, filename) { 473 var content = NativeModule.require('fs').readFileSync(filename, 'utf8'); 474 module._compile(stripBOM(content), filename); 475 };
runMain
によりスクリプトが実行されると制御はC++に戻りuv_run
のイベントループに入ります。
6. 最後に
以上、libuvとV8 JavaScriptエンジンの説明と、Node.jsでスクリプトを実行するまでのコードを見てきました。最後の方は若干駆け足になりましたが、足りない箇所いついてはNode.jsのコードをご覧頂ければと思います。
Tokyo Otaku ModeではNode.jsエンジニアを募集しています。
興味のある方はこちらからご応募ください。