ぼちぼち日記

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

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

さて、JavaScript Advent Calendar 2011 (Node.js/WebSocketsコース)2日目「Node.js のエラーメッセージの謎」の後半です。

前半編のおさらい

前半のおさらいをすると、Node.js は起動時に processオブジェクトなどを生成し、src/node.js を実行します。この src/node.js の中では、lib/module.js で定義されている Module.runMain 関数を process.nextTick() に登録を行い、ひとまず src/node.js が終了します。

src/node.js の実行を簡略化した疑似コードで表すと以下のようになります。

JSファイル実行の疑似コード(src/node.js実行時)
(function(process) {
  var Module = NativeModule.require('module');
  process.nextTick(Module.runMain);
})(process);

src/node.jsの実行が終了すると、uv_run() が呼ばれて Node.js のメインエンジンのイベントループが回り始めます。
イベントループが回る際、 process.nextTick() でコールバック関数が登録されていると、ループの一番最初に process._tickCallback() が呼ばれます。そして先ほど登録された Module.runMain 関数が実行されることになります。

これを疑似コードで表すとこのようになります。

JSファイル実行の疑似コード(src/node.js実行後でイベントループ開始時)
(function(process) {
  var process._tickCallback = function() {
     var nextTickQueue = [Module.runMain];
     nextTickQueue[0]();
  };
  process._tickCallback();
})(process);

ということで、図に表すとこんな流れになるでしょう。

後半編は、Node.js のイベントループが回り始めた後からの話を書きます。

イベントループ開始、Tickを刻み始める。

イベントループが開始されると process._tickCallback() が起動され、nextTickQueue 配列の最初に登録されているコールバック関数(Module.runMain)が実行されます。
この Module.runMain の関数の中身は以下のようになっています。

lib/module.js
   468  Module.runMain = function() {
   470    Module._load(process.argv[1], null, true);
   471  };

これは簡単。Module._load()を呼んでいるだけです。この場合の process.argv[1] は、第1引数で指定した JavaScriptファイル名(絶対パス付)になっています。(src/node.js:85)

本当のJSファイルの読み込み

ここからは、本当にファイルの JavaScriptの読み込みをするところです。

lib/module.js
   274  Module._load = function(request, parent, isMain) {
   279    var resolved = Module._resolveFilename(request, parent);
   280    var id = resolved[0];
   281    var filename = resolved[1];
          ・・・・・
   301    var module = new Module(id, parent);
          ・・・・・
   309    try {
   310      module.load(filename);
   311    } catch (err) {
            ・・・・・
   314    }
   316    return module.exports;
   317  };

274行目: request がJavaScriptファイル名が入るので、そのファイルディスクリプタを取得します。
本当はここで読み込むJSファイルが見つからないためエラーが出るのですが、とりあえず先の流れを説明すると、
301行目: モジュールオブジェクトをインスタンス化します。

ここでmoduleオブジェクトのスコープが明らかに!

実はここが Node.js の「Node.jsマニュアルのmoduleの項目」にある
「module は実際はグローバルではなく、各モジュール毎のローカルです。」
という記載の根拠になっています。 すなわち、 _load()内で生成された module オブジェクトは、_load()関数のスコープ内だけに閉じた変数であるため、各モジュールが require()されるたびに、それぞれ _load()関数スコープ内で別の module オブジェクトが生成されるのです。
310行目,316行目:ここで require()で指定されたファイルが読み込まれ、コンパイル&実行結果が module.exports を返り値として return されます。
本当はここまできて正常起動なんですが、今回はここにくるまでにエラーが発生するので279行目の Module._resolveFilename() の処理を見ていきます。

ファイルディスクリプタの取得

lib/module.js
   319  Module._resolveFilename = function(request, parent) {
          ・・・・・
   332    var filename = Module._findPath(request, paths);
   333    if (!filename) {
   334      throw new Error("Cannot find module '" + request + "'");
   335    }
   336    id = filename;
   337    return [id, filename];
   338  };

319行目: process.argv[1] を request 仮引数に渡されているため、Module._findPath()でファイルを探してファイルディスクリプタを取得しにいきます。
Module._findPath()内のコード解説まで行うと説明の流れがわけわからなくなるので解説しませんが、ファイル探索の方法は、
「Node.jsマニュアルのFile Modulesの項目」の記載されている順番で行われます。(ここの説明は手を抜いてます。)

エラーの throw

今回 存在しないファイル hoge.js を読み込もうとしているので 332行目:の Module._findPath()から false が返されます。
そして 334行目: において、

   334      throw new Error("Cannot find module '" + request + "'");

のエラーが throw されるわけです。長かった旅もこれで終わりです。

まとめ、

これまでの処理の流れをまとめると、以下のような図になります。

最初に示したエラーのスタックと全く同じ流れです。

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'(5)
    at Function._resolveFilename (module.js:334:11)(4)
    at Function._load (module.js:279:25)(3)
    at Array.0 (module.js:470:10)(2)
    at EventEmitter._tickCallback (node.js:192:40)(1)

ということで、
「Node.jsがファイルが見つからないときにどうしてこんなエラーを出力するのか?」
といった謎をソースコードから追っかけていって解説しました。

これで謎が解けました、めでたしめでたし…

と思いきや、そういえば一つ疑問を残したままでした。

さらに、謎の謎。

前半の最後に書きましたが、
「ではなぜに process.nextTick() を使ってJSファイルの読み込みを行わないといけないのか?」
という謎が残っています。

実はそのヒントはソースのコメントに残されています。

src/node.js
    94        var Module = NativeModule.require('module');
    95        // REMOVEME: nextTick should not be necessary. This hack to get
    96        // test/simple/test-exception-handler2.js working.
    97        // Main entry point into most programs:
    98        process.nextTick(Module.runMain);

95行目〜96行目: REMOVEME: nextTick は必ずしも必要ではない。このハックは test/simple/test-exception-handler2.js がちゃんと動作するためにある。
ということで、test/simple/test-exception-handler2.js を見てみますが、テストコード固有の実装も入っているので同様の試験コードを下に示します。

error.js
test/simple/test-exception-handler2.jsと同様の試験コード
process.on('exit', function() {
  console.log("Process is to exit.");
});
process.on('uncaughtException', function(err) {
  console.log('uncaughtException emitted:'+err);
});
var a = null;
a.hoge;

試験内容としては、「JavaScriptとして評価(コンパイル)する際ににエラーが発生したら process終了のイベント等のコールバックが正常に呼ばれるのか?」というテストです。実際にテストしてみます。

テスト結果
unix:~> node error.js
uncaughtException emitted:TypeError: Cannot read property 'hoge' of null
Process is to exit.

ちゃんと問題なくコールバック処理が行われています。
次に process.nextTick()を使わないならどうなるでしょうか? 実際に Node.js を改造してみましょう。

process.nextTick()を使わないよう src/node.js を改造
    94        var Module = NativeModule.require('module');
    95        // REMOVEME: nextTick should not be necessary. This hack to get
    96        // test/simple/test-exception-handler2.js working.
    97        // Main entry point into most programs:
    98        //process.nextTick(Module.runMain);
    99        Module.runMain();

これを再コンパイルして node_mod というコマンド名に変えます。同じテストをしてみると。

unix:~> node_mod error.js

module.js:313
    throw err;
          ^
TypeError: Cannot read property 'hoge' of null
    at Object.<anonymous> (/home/ohtsu/error.js:9:2)
    at Module._compile (module.js:432:26)
    at Object..js (module.js:450:10)
    at Module.load (module.js:351:31)
    at Function._load (module.js:310:12)
    at Function.runMain (module.js:470:10)
    at startup (node.js:99:14)
    at node.js:546:3

先程と違って process の uncaughtExceptionや exit イベントが拾えずにエラー終了してしまいます。
process のイベントが登録されていないのでしょうか?
否、前半で述べた src/node.js にある startup.processKillAndExit(); や startup.processSignalHandlers(); の関数で登録されています。(複雑になるのでここでは説明は省きます。)

上記エラーを見てもわかりますが、node_mode は process.nextTick() 経由で runMain を呼び出さないので src/node.js 内からすぐ呼び出されています。そうするとここでのエラーは、「uv_run() イベントループが起動する前に発生する」ことになります。
イベントループが回っていないのでいくらコールバック関数を登録してもイベント発生でコールバック関数が呼び出されることはありません。当たり前ですよね。図で表すと以下の通りです。

ということで、

process.nextTick()を使って Node.js起動時のJavaScript ファイルを読み込む理由は、先にイベントループを起動させることによって JavaScriptのエラー発生した場合でも process イベントを正常にハンドリングできるようにするため。」

になります。「謎の謎」もめでたく解決しました。
ちなみに process.nextTick() を使わない場合、存在しないファイル名を指定した場合のエラーは下記になります。
これだったらちょっとは驚かないかどうか??? 私は既に今ので慣れちゃいました。

unix:~> node_mod hoge.js

module.js:334
    throw new Error("Cannot find module '" + request + "'");
          ^
Error: Cannot find module '/home/ohtsu/hoge.js'
    at Function._resolveFilename (module.js:334:11)
    at Function._load (module.js:279:25)
    at Function.runMain (module.js:470:10)
    at startup (node.js:99:14)
    at node.js:546:3

以上、前半・後半の2部構成になってしまいましたが、長い謎解きにおつきあいくださいましてありがとうございました。
Node.js も中身を理解するとその挙動や APIの仕様・マニュアルで書かれている意味などの理解が一段と深まります。
皆さんも一度 Node.js のソースコードリーディングに挑戦してみてください。