node.jsとは何か(4)

さて、前回まで基礎部分をいろいろと説明したので今回からは実装について。現在のソースやその成り立ちを説明するのもいいんだろうけど、今日からはちょっぴりハンズオン形式に趣向を変えてみよう。ってことで


node.js を作っていくよ!



実装編その一はJSエンジンであるV8にJavaScriptのソースを食わせて実行する、つまりはオレオレJS環境を作るまでを扱うのだ。
V8はもともと他のソフトウェアに組み込まれて使用されることを想定(例えばChromeとかね)されているのでこういう作業が必要になる。


手順は大きくわけて二つ
1. まずはV8のソースを落としてきてV8のビルド
2. V8のソースディレクトリに自作のC++のソースを作ってコンパイル&実行

C++が出てきた時点で引いちゃったかもしれないけど、C++を使えるようになるのが今回の目的ではないのでまずはリラックス。C++っていったってそんなに難しく構える必要ないから!

例えば

int main(int argc, char* argv[]) {
  return 0;
}

っていうコード。

  • C++で実行プログラムを作るのにはmain関数が必要。
  • 関数には戻り値の型と引数の型を指定する。
  • 戻り値は指定されているとおりint型で返す。

ってことを実装しただけだ。いやそれC言語でしょって思うかもしれないけど気分は++。ちゃんとコンパイルすれば動くからね。簡単でしょ?もちろん Hello World さえ表示しないけどそれはそれで置いておこう。

あと、おそらくmain関数の引数が気になる、もしくはトラウマ的に嫌なイメージを抱く人もいるんだろうけど、引数の数と引数の値を渡されるんだというくらいの認識にとどめておいてぜーんぜん構わない。

もちろん興味を持ってC++をやろうって思った人はこんなでたらめな説明で満足せずにどんどん突っ込んで勉強していってほしい。


じゃあまずV8のビルドから。手順は
http://code.google.com/intl/ja/apis/v8/build.html
に載っている。

$ svn checkout http://v8.googlecode.com/svn/trunk/ v8

としてソースをsubversionリポジトリから引っ張り出して、そのディレクトリで

$ scons

とするだけ、ここで注意したいのがsconsでビルドする際にx86_64環境の人は

$ scons arch=x64

としてCPUアーキテクチャを指定する必要があるってところかな。

さて、これをもとに本日のお題となるのがこちらのページ
http://code.google.com/intl/ja/apis/v8/get_started.html
V8の「さあ始めよう」だね。

このページを見てみると中ほどに Hello World 出力プログラムのコードが載っている。
まずはこのプログラムを実際に動かしてみよう。

このソースをコピペし、コンパイルして実行する。つまり

#include <v8.h>

using namespace v8;

int main(int argc, char* argv[]) {

  // Create a stack-allocated handle scope.
  HandleScope handle_scope;

  // Create a new context.
  Persistent<Context> context = Context::New();
  
  // Enter the created context for compiling and
  // running the hello world script. 
  Context::Scope context_scope(context);

  // Create a string containing the JavaScript source code.
  Handle<String> source = String::New("'Hello' + ', World!'");

  // Compile the source code.
  Handle<Script> script = Script::Compile(source);
  
  // Run the script to get the result.
  Handle<Value> result = script->Run();
  
  // Dispose the persistent context.
  context.Dispose();

  // Convert the result to an ASCII string and print it.
  String::AsciiValue ascii(result);
  printf("%s\n", *ascii);
  return 0;
}

これをさっきビルドしたV8のディレクトリにhello_world.cppとして保存し、次に

g++ -Iinclude hello_world.cpp -o hello_world libv8.a -lpthread

としてコンパイルしたら

$ ./hello_world

で実行だ。

成功すると Hello, World! とプロンプトに表示される。

おめでとう!


と、ここで終わらせるのはあんまりだ。なので解説を試みるね。


main関数が始まってすぐの

HandleScope handle_scope;

という行はHandleScope型のhandle_scopeっていう変数を作り出しているだけ。
これがないと下の方でHandle<...>と始まる行があるんだけど、そこでコケちゃう。イメージとしてはHandle型の変数達が棲息する場所を作るっていう感じかな。V8ではこの行はおまじないとしてほぼ確実に使うので気にしないったら気にしない。位置的にはHandle型の変数が現れる前に書いておけばオッケーだ。

Persistent<Context> context = Context::New();

この行では実行コンテキストを作っている。必ず一つはないとどこ実行していいのかわからないので落ちちゃうんだ。

Context::Scope context_scope(context);

作られたコンテキストに対し、この行で実際に実行コンテキストを設定する。で、その後に下の方で

context.Dispose();

として破棄しているね。
ちなみにこのコンテキストはいくつも作ることができて、Context::Scopeで設定することでそれぞれ実行可能。ブラウザの中でiframeの中に書いたjsをメインの画面のjsと一緒に実行するときなんかはこの仕組みを使って複数のコンテキストが切り替わっているってわけ。

コンテキストとはそもそも何かというと、例えば2つのプロセス、AとBっていうのがあってそれをマルチタスク環境上で同時に実行しているとしよう。1CPUだとこの2つのプロセスのうちどちらか一方しか瞬間的には処理できなくって、これをOSの中にあるタスクスケジューラってヤツがあたかもこの2つのプロセスを同時に実行しているように見せかけるためA->B->A->Bみたいに高速にスイッチングしているんだ。で、このうちCPUが現在こなしている処理っていうのをコンテキストと呼んでいる。

で、次の

Handle<String> source = String::New("'Hello' + ', World!'");

では"'Hello' + ', World!'"って文字列をV8用のJSソースとして設定している。

Handle<Script> script = Script::Compile(source);

ここがV8の結構特徴的なところだと思うんだけど、V8では読み込んだソースを初回実行時にコンパイルするんだ。バイトコードみたいな中間コードじゃなくってきちんとマシン語にまでね。ここで内部的に使われているアセンブラはSun謹製のアセンブラをさらにGoogleでチューンしたもの。

そうしてコンパイルしたものを

Handle<Value> result = script->Run();

で実行して今度はresultの中に結果を格納している。

後はその結果を文字列にしてプリント。

String::AsciiValue ascii(result);
printf("%s\n", *ascii);

どうだろうか、C++の知識よりもV8を使うためのお約束ばかりだ。
このようにものすんごく簡単にオレオレJSを作れちゃう。


さあ、このサンプルをもとにもうちょっとだけ踏み込んでみようか。次はJavaScriptファイル名を指定して、このプログラムに実行させたい。
先ほどのプログラムに対して加える変更内容は

1. 引数として指定されたファイルを読み込む
2. 先ほどの Handle source に読み込んだ内容を食わせる。

と、こんだけ。


でだ。こっからはなるべく楽をしたい。なのでサンプルからパクれるものを探すのだ。V8のディレクトリの
sample/shell.cc
をみたらちょうどいい感じに見つかった。持ってきたのはファイルを読み込むその名もReadFile関数。

#include <v8.h>

using namespace v8;


v8::Handle<v8::String> ReadFile(const char* name) {
  FILE* file = fopen(name, "rb");
  if (file == NULL) return v8::Handle<v8::String>();

  fseek(file, 0, SEEK_END);
  int size = ftell(file);
  rewind(file);

  char* chars = new char[size + 1];
  chars[size] = '\0';
  for (int i = 0; i < size;) {
    int read = fread(&chars[i], 1, size - i, file);
    i += read;
  }
  fclose(file);
  v8::Handle<v8::String> result = v8::String::New(chars, size);
  delete[] chars;
  return result;
}


int main(int argc, char* argv[]) {

  Persistent<Context> context = Context::New();
  Context::Scope context_scope(context);

  // 引数が渡されなかったらエラーで終了
  if(argc < 2)  {
    fprintf(stderr, "script was not specified.\n");
    return 1;
  }
  HandleScope hs;
  // 引数に指定されたファイルを読み込む
  Handle<String> source = ReadFile(argv[1]);

  Handle<Script> script = Script::Compile(source);
  Handle<Value> result = script->Run();

  String::AsciiValue ascii(result);
  printf("%s\n", *ascii);

  context.Dispose();
  return 0;
}

これをolele.cppという名前で保存して、

g++ -Iinclude oleole.cpp -o oleole libv8.a -lpthread

としてコンパイルして実行してみる。

$ ./oleole

するとまずは

script was not specified.

と冷たくあしらわれて終了する。じゃ次に何か適当なJSのファイルをコイツに食わせてみよう。

var i = 1;
var j = 2;
i + j;

という内容のファイルをsample.jsとして保存し、今度は

$ ./oleole sample.js

として実行だ。

「3」という結果が返ってきて無事JSファイルをV8で実行することができただろうか。
エラー処理してないからちょっとしたミスでプログラムが落ちちゃうのはご愛嬌。

ということで実装編第一回のオレオレJS作成編は終了なのだ。
次回はlibevを使ったイベントループ実装編の予定。



ここからは補足説明。さっきのソースの中のReadFile関数で「v8::」っていうのがそこらじゅうに書かれているけどそれ取っちゃっても動くからね。

using namespace v8;

っていう行がそれ取っちゃっても動くようにするおまじないだ。

あとargcとargvっていうのはそれぞれコマンドラインからの引数の数と内容を含む。分かり辛いから例を挙げてみよう。

$ ./oleole sample.js

こうやって実行すると./oleoleとsample.jsの二つがmain関数に渡されるのでargcは2になり、argv[0]に./oleole、argv[1]にはsample.jsが入るって寸法。
ちなみにargcはargument count、argvはargument vectorの略だ。
UNIX系のOSやWindowsではmain関数に渡されるのはargcとargvだけではなくさらにもう一つenvpというものがあり、環境変数を受け取ることができる。

int main ( int argc, char* argv[], char* envp[] )

さらにMacOS Xでは4つ目の、その名もappleというのがある。

int main ( int argc, char* argv[], char* envp[], char* apple[] )

手元にMacがないので確認できないけどapple[0]にはパス情報が入っているハズ。
あ、ついでにもう一つ。mainの戻り値を「0」に設定しているけど「0」っていうのは正常終了を表す。それ以外はエラーってことね。