今日から始める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エンジニアを募集しています。
興味のある方はこちらからご応募ください。