今日から始めるNode.jsコードリーディング - libuv / V8 JavaScriptエンジン / Node.jsによるスクリプトの実行

ソフトウェアを正しく理解する唯一の方法はコードを読むことです。
ドキュメントを読めばそのソフトウェアが何を実装しているか分かりますが、どのように実装されているかまでは分かりません。
開発中に何らかのトラブルに悩まされたときや、効率的なコーディングをしたいと思ったとき、下位レイヤのソフトウェアを理解しておけば素早く対処できるシーンが多くあります。

ただ、コードを読むことは簡単なタスクではありません。
現代的なソフトウェアはそれなりの規模のコードを含んでいることがほとんどです。アーキテクチャ間の差異を吸収するためのコードなど、本質的な機能を理解する上ではあまり重要ではないコードも含まれています。
何らかの問題が発生してからコードを読もうと思っても、準備なしでは関連する箇所を探すだけでかなりの労力が必要な作業となります。
従って、普段からコードを読んでおくことが重要です。

また、コードを読むにあたり、解説があれば迷うことなく重要な箇所へ読み進めることができます。
詳解 LinuxカーネルBSDカーネルの設計と実装などの解説本を片手にカーネルのコードを読んだことのある方も多いのではないかと思います。

今回のエントリーでは、Tokyo Otaku Modeのアプリケーションサーバに使用しているNode.jsのコードを読み、Node.jsの主要コンポーネントであるlibuvV8 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ライブラリで、RustLuvitJuliapyuvその他のプロジェクトでも使用されています。(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_openuv_fs_readuv_fs_writeは、システムコールのopenreadwriteに対応します。uv_fs_req_cleanupはlibuvが内部的に使用したメモリを開放します。

  • uv_runはlibuvのイベントループを実装した関数です。main関数から呼ばれたuv_fs_openは、必要な処理を行いuv_default_loopが返すデフォルトループへイベントを登録するとmain関数へ戻ります。

  • uv_runはデフォルトループに登録されたイベントを問い合わせ、ディスパッチ可能なイベントがあればイベントハンドラを実行します。uv_fs_openの場合、関数呼び出し時に引数に渡されたon_openがイベントハンドラとなります。

  • ファイルがオープンされるとuv_runon_openイベントハンドラを呼び出します。on_openイベントハンドラは、イベントハンドラon_readを引数にuv_fs_readを呼び出しuv_runへ戻ります。

  • ファイルからデータが読み込まれるとuv_runon_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 StartedHello 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をご覧ください。)

lldbgdb等のデバッガから、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などライブラリが置かれています
  • 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.hdeps/v8/src/api.ccで定義されます。Persistentdeps/v8/include/v8.hで定義されるテンプレートクラスで、コンテキストに対して永続的なガベージコレクタで管理されるオブジェクトです。

実行コンテキスト作成後、SetupProcessObjectprocessクラスを作成した後、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::Startuv_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 }

SetupProcessObjectsrc/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 }

Loadsrc/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のバイナリに埋め込まれます。MainSourcesrc/node_javascript.ccで定義されます。

ExecuteStringMainSourceが返す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: setTimeoutsetIntervalsetImmediate等を初期化
  • 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.exitprocess.killを初期化
  • startup.processSignalHandlers: process.onprocess.(add|remove)Listenerを初期化
  • startup.processChannel: process.env.NODE_CHANNEL_FDが指定されている場合通信用のパイプを開く
  • startup.resolveArgv0: argv[0]のnodeのパスを必要な場合書き換える

以上でJavaScriptの実行準備がすべて完了します。Node.jsのコマンドライン引数に実行するスクリプトファイルのパスを指定した場合、lib/module.jsModule.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エンジニアを募集しています。
興味のある方はこちらからご応募ください。