ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Node.js のエラーメッセージの謎 (前半)

JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)の2日目です。初めての「はてなダイヤリー」で書き方に慣れていないので、見苦しところがあればご了承ください。

実は書いているうちにボリュームが膨らんでしまったので前半・後半の2部制で掲載します。

Node.js のエラーメッセージ

皆さん Node.js を使ってみて以下のようなエラーメッセージを見た覚えがあるでしょうか?

unix:~> node hoge.js

node.js:201
        throw e; // process.nextTick error, or 'error' event on first tick
              ^
Error: Cannot find module '/home/ohtsu/hoge.js'
    at Function._resolveFilename (module.js:334:11)
    at Function._load (module.js:279:25)
    at Array.0 (module.js:470:10)
    at EventEmitter._tickCallback (node.js:192:40)

このエラーは、存在しないJavaScriptファイルを指定して Node.js を起動しようとした時に出力されます。
Node.jsの超初心者だった頃はこのエラーメッセージを見て、「process.nextTick って何??」と非常に驚いたことを覚えています。
よく見れば "Cannot find module"(モジュールが見つかりません。)と次に書いてあるんですが、よくあるような、

unix:~> cat hoge.js
cat: hoge.js: No such file or directory

といった単純なエラーメッセージに見慣ていると、この Node.js のエラーはちょっとギョッとしますよね。でも何故こんなエラーメッセージが出力されるのか不思議に思いませんか?

そこで今回の Node.js Advent Calenderブログは、このエラーメッセージがどのような理由で出力されているのかをソースコードを見ながら解説したいと思います。 (Node.js のバージョンは執筆時の v0.6.3 を基にしています。ちょうどこのブログの公開日に v0.6.4 がリリースされちゃいました。)

Node.js はどうやって起動するのか?

上のエラーメッセージは、よく見るとエラーが発生した時点のスタックトレース(関数名、ソースファイル名、行数)を表しているのは明らかです。最初 node.js から始まっているようですが、この「node.js」は、Node.js を指しているわけではなく、Node.js のソースツリー内の src/node.js を指しています。
ここを理解するには Node.js のプログラムがどうやって起動するのかを見ないといけません。細かく説明するときりがないので、以前 「Node塾 その1」で説明した資料
Node.js の中身を見てみよう src/node.cc のざっとした歩き方
をもとに簡単に説明します。(興味のある方は、上の資料なり src/node.cc を読んでみてください。)

まずは起動までのざぁ〜っとした流れです。

最初は、 main ルーチンから src/node.cc 内で libuvやV8など Node.js の基盤を成すライブラリ群の初期化、process オブジェクトの生成を行うと、V8で生成した globalオブジェクトの下で processオブジェクトを引数として src/node.js の関数が呼びだされます。

今回はこの src/node.js の起動するところから詳細に説明を始めます。

Node.js のソースコード(src/node.js)リーディングを始めよう。

src/node.js はざっと以下のような構造になっています。

src/node.js
(function(process) { 
  global = this;
  var EventEmitter;
  function startup() {
    EventEmitter = NativeModule.require('events').EventEmitter;
    ・・・・・
  }
  function NativeModule(id) {
    ・・・・・ 
  }
  NativeModule._source = process.binding('natives');
    ・・・・・
  startup();
});

最初は globalオブジェクトの設定とEventEmitter変数の宣言を行い、startup()関数を呼び出すだけです。

startup()関数

これからは、行番号をつけて細かく startup() 関数から始まるソースコードを追っかけます。(一部のコードやエラー処理、コメント・空白などは見やすいよう除いています。)

src/node.js
    32    function startup() {
    33      EventEmitter = NativeModule.require('events').EventEmitter;
    34      process.__proto__ = EventEmitter.prototype;
    35      process.EventEmitter = EventEmitter; // process.EventEmitter is deprecated

33行目: lib/events.js が読み込まれて EventEmitter オブジェクトが設定されています。どんなオブジェクトでしょうか? そこで lib/events.js の中身を見てみると、

lib/event.js
    24  function EventEmitter() { }
    25  exports.EventEmitter = EventEmitter;

おっと、空のコンストラクタ関数が require からの返り値(exports オブジェクトのプロパティ)として渡されているだけです。(当然 EventEmitter の prototype に定義する各種プロパティやメソッド(onやemit等)も lib/event.js 内で定義されています。)
実は NativeModule.require()内では、これ以外にモジュールコードのコンパイルやキャッシュなどの処理が行われていますが、今回は直接関係ないので説明を省略します。

次に、
34行目: processオブジェクトのプロトタイプオブジェクトに EventEmitter のプロトタイプオブジェクトの代入。これによって processオブジェクトは、EventEmitter オブジェクトと同じプロトタイプオブジェクトを共有する兄弟になって(emitや addListener など)EventEmitter と同じメソッドを持つインスタンスに早変わりということになります。
35行目: 昔は C++ で実装された EventEmitter が process.EventEmitter として存在していました。今はもう削除されていてjs版が使われるよう、ここでエイリアスしています。

次は globalオブジェクトの各種プロパティの設定です。

src/node.js
    37      startup.globalVariables();
    38      startup.globalTimeouts();
    39      startup.globalConsole();

37行目〜39行目: global変数のプロパティ(global.process/global.global(小文字・大文字)/global.root/global.Buffer) 、タイマー系関数(global.setTimeout(), global.setInterval(), global.clearTimeout(), global.clearInterval()) 、入出力用の関数 global.console() の設定しています。

src/node.js
    41      startup.processAssert();
    42      startup.processNextTick();
    43      startup.processStdio();
    44      startup.processKillAndExit();
    45      startup.processSignalHandlers();
    47      startup.processChannel();
    49      startup.removedMethods();
    51      startup.resolveArgv0()

41行目〜47行目: process オブジェクトに対してsrc/node.cc 内で設定されなかった残りのプロパティ/メソッドの設定を行っています。

process.nextTick()関数と process._tickCallback()関数

process に設定されている項目を全部説明するのはボリュームが多すぎて大変なので、今回の説明で最も重要な2つのメソッド process._tickCallback()process.nextTickCallback() の部分を説明します。該当するソースは以下の部分になります。(エラー時の処理の表示は省いています。)

   181    startup.processNextTick = function() {
   182      var nextTickQueue = [];
   184      process._tickCallback = function() {
   185        var l = nextTickQueue.length;
   186        if (l === 0) return;
   188        var q = nextTickQueue;
   189        nextTickQueue = [];
   191        try {
   192          for (var i = 0; i < l; i++) q[i]();
   193        }
   194        catch (e) {
                ・・・・・
   202        }
   203      };
   205      process.nextTick = function(callback) {
   206        nextTickQueue.push(callback);
   207        process._needTickCallback();
   208      };
   209    };

205行目〜208行目: process.nextTick()は、引数としてコールバック関数を受けて単に nextTickQueue 配列にコールバック関数を追加しています。この中で呼び出している process._needTickCallback()関数は、src/node.cc:258 で定義されていますが、次のイベントループの一番最初に process._tickCallback() を呼び出というフラグを立てているだけです。(ここでは詳細は説明しません。)
184行目〜203行目: process._tickCallback()は、 nextTickQueue 配列に格納されているコールバック関数を順番に実行します。 process._tickCallback() は先に説明したフラグが立っていればイベントループが回る一番最初に呼び出されるメソッドです。

オプションによる処理分岐

以上で global/process オブジェクト周りの初期設定が終わりました。いよいよ JavaScriptファイルの読み込み処理です。残りは大きく5つの分岐処理に分かれます。

src/node.js
    58      if (NativeModule.exists('_third_party_main')) {
             ・・・・・
    66      } else if (process.argv[1] == 'debug') {
             ・・・・・
    71      } else if (process._eval != null) {
             ・・・・・
    82      } else if (process.argv[1]) {
             ・・・・・
   100      } else {
             ・・・・・
   122      }

最初の処理(58行目)は、サードパーティのコアモジュールを使う場合。そうです、Node.js はビルトインではなく独自のコアモジュールを読み込ませて動かすことが可能です。(名称ライセンスとかいろいろあって本当に本格的にやっていいのかは微妙ですけど、)これはこれでまた別の機会にということで、今回の条件は false とします。
2つ目の処理:(66行目)は、node 起動の第1引数に debug が指定している場合です。これは debugger を起動して debug モードで動作させる場合ですが今回はこれも対象外。
3番目の処理:(71行目)、node の起動オプションとして -e もしくは --eval が指定している場合。この場合、続く文字列が eval 評価されます。これも今回はスキップします。

ということで、
この次の第一引数でNodeで起動するJavaScriptファイルが指定している処理(82行目〜100行目)が今回の分岐処理の対象になります。

ちなみに最後 100行目〜122行目 は、REPL もしくは標準入力で node を起動する場合の処理になります。(これも今回はスキップします。)

JavaScriptの読み込み処理

だいぶ長くなってしまいましたが、これが前半最後の部分です。先ほど説明したNodeで起動する JavaScriptファイルの読み込み(82行目〜100行目)の説明です。

src/node.js
    82      } else if (process.argv[1]) {
    84        var path = NativeModule.require('path');
    85        process.argv[1] = path.resolve(process.argv[1]);
    89-93     ・・・・・
    94        var Module = NativeModule.require('module');
    98        process.nextTick(Module.runMain);
   100      } else {

84行目〜85行目: 第1引数で指定したJSファイル名を絶対パス付に置き換えます。
89行目〜92行目: クラスターの worker プロセスだった時の処理が行われています。今回は関係ないので記載も省略してます。
94行目: lib/module.js を読み込みます。 EventEmitterと同じくコンストラクター関数 Module が exports されて代入しています。
95行目: lib/module.js:468 で定義してある Module.runMain 関数を process.nextTick()によってコールバックとして登録します。

ということで、驚いたことに Module.runMain 関数をコールバックとして登録するだけで、すぐに JavaScriptファイルを実行しないんですねぇ。

ここで一旦 src/node.js の処理がすべて終了になります。その後はイベントループの起動 uv_run() されて Node.js のメインエンジンが回り始めます。

前半まとめ

以上をまとめると、下記のような流れになっていることがわかりました。

だからエラーメッセージに process.nextTick が含まれているのがわかりますね。

だいぶ長くなっちゃいましたが、いったんこれで前半を終了させます。 後半ではイベントループ開始後、実際にエラーが throw されるところまでを説明します。お楽しみにぃ。