Ragel

http://www.cs.queensu.ca/~thurston/ragel/

Ragel は、ステートマシンコンパイラの一種で、パーサジェネレータとして使われることが多い。その実例として、MongrelHpricot がある。

パーサジェネレータとして Ragel を使う利点は、以下の通り。

  • プロトコルやデータフォーマットを正確にパーズできる
  • その結果、セキュアなソフトウェアが作れる
  • 外部ライブラリに依存しないコードができる
  • ものすごく速い

特に HTTP server など、速度が求められるサーバを書くときに便利なことがわかる。

参考: http://www.zedshaw.com/tips/ragel_state_charts.html

ここでは、簡単な例としてドメイン名のパーザを作ってみよう。ホスト言語は C で。

まずは、Ragel 6.0 をインストールする。
OSX の場合、MacPorts でインストールできるものは少し古いので、Ragel のページ の下のほうから ragel-6.0.tar.gz を入手し、インストールする。

$ ./configure
$ make
$ sudo make install

ステートマシンの定義は、こんな感じに書ける。

alnumhyphen = alnum | "-";
label = alnum (alnumhyphen* alnum)?;
domain = label ("." label)*;
main := domain;

BNF っぽくてわかりやすい。
これをコンパイルすると、

こんなステートマシンができあがるわけだ。(Graphviz で画像化できる)

この定義を yacc のように C のソースに埋め込んで、domain.rl というファイル名で保存する。

#include <stdio.h>
#include <string.h>

%%{
  machine domain_parser;
  
  action begin_label { begin = fpc; }
  action end_label {
    strncpy(buf, begin, fpc-begin);
    buf[fpc-begin] = 0;
    puts(buf);
  }
  action error { puts("! parse error"); }

  alnumhyphen = alnum | "-";
  label = (alnum (alnumhyphen* alnum)?) >begin_label %end_label;
  domain = label ("." label)*;
  main := domain @!error;
  
  write data;
}%%

#define BUFSIZE 1024

void parse(const char* data)
{
  const char* p = data;
  const char* pe = data + strlen(data);
  const char* eof = pe;
  int cs;
  const char* begin = NULL;
  char buf[BUFSIZE];
  
  printf("*** %s\n", data);
  
  %% write init;
  %% write exec;
}

int main(int argc, char** argv)
{
  parse("www.example.org");
  parse("www-host.example.org");
  parse("-www.example.org");
  parse("ww-.example.org");
  return 0;
}

とりあえず、実行してみよう。

$ ragel -C domain.rl
$ gcc -o domain domain.c
$ ./domain
*** www.example.org
www
example
org
*** www-host.example.org
www-host
example
org
*** -www.example.org
! parse error
*** ww-.example.org
! parse error

それぞれのドメイン名をパーズして、要素に分解できていることがわかる。(ラベルの最初か最後にハイフンがあるものはエラー)
それでは、具体的に Ragel の定義を見ていこう。

%%{
  machine domain_parser;
  
  action begin_label { begin = fpc; }
  action end_label {
    strncpy(buf, begin, fpc-begin);
    buf[fpc-begin] = 0;
    puts(buf);
  }
  action error { puts("! parse error"); }

  alnumhyphen = alnum | "-";
  label = (alnum (alnumhyphen* alnum)?) >begin_label %end_label;
  domain = label ("." label)*;
  main := domain @!error;
  
  write data;
}%%

Ragel の定義は、%%{ から }%% までの間に書いていく。

  machine domain_parser;

まず、ステートマシン名を宣言する。(必須)

  action begin_label { begin = fpc; }
  action end_label {
    strncpy(buf, begin, fpc-begin);
    buf[fpc-begin] = 0;
    puts(buf);
  }
  action error { puts("! parse error"); }

ここでは、アクションを定義している。
Ragel には、任意の状態遷移の前後にアクション (任意のコード) を実行できるという特徴がある。アクションの中では、fpc で現在のポインタ位置を参照できるので、それを利用して必要なところをキャプチャしていくわけだ。

  alnumhyphen = alnum | "-";
  label = (alnum (alnumhyphen* alnum)?) >begin_label %end_label;
  domain = label ("." label)*;
  main := domain @!error;

ステートマシンの定義は、Ragel 言語という正規言語 (regular language) で書く。alnum は組み込みのステートマシンで、[0-9a-zA-Z] に相当する。

label の定義には、アクションが使われている。

  label = (alnum (alnumhyphen* alnum)?) >begin_label %end_label;

こう書くと、label にマッチする直前に begin_label が、直後に end_label が呼び出される。
ここで、もう一度アクションの定義を確認してみると、

  action begin_label { begin = fpc; }
  action end_label {
    strncpy(buf, begin, fpc-begin);
    buf[fpc-begin] = 0;
    puts(buf);
  }

begin_label が呼ばれたときに begin にその時点の fpc を保存しておき、end_label では begin から fpc までの文字列を切り出して表示していることがわかる。これで、正規表現のキャプチャと同じことができる。

さらに、main の定義を見てみる。

  main := domain @!error;

こう定義しておくと、状態遷移にエラーが起きたときに error アクションを呼び出してくれる。
最後に、

  write data;

で、ここにステートマシンの定義コードを出力する。
ホストプログラムの本体を見ていこう。

#define BUFSIZE 1024

void parse(const char* data)
{
  const char* p = data;
  const char* pe = data + strlen(data);
  const char* eof = pe;
  int cs;
  const char* begin = NULL;
  char buf[BUFSIZE];
  
  printf("*** %s\n", data);
  
  %% write init;
  %% write exec;
}

int main(int argc, char** argv)
{
  parse("www.example.org");
  parse("www-host.example.org");
  parse("-www.example.org");
  parse("ww-.example.org");
  return 0;
}

パーザ部分は、parse 関数であることがわかる。

void parse(const char* data)
{
  const char* p = data;
  const char* pe = data + strlen(data);
  const char* eof = pe;
  int cs;

Ragel では、用途ごとに変数名が決められているので、ステートマシンを実行するスコープに、以下の変数をあらかじめ定義しておく必要がある。

data 入力データ
p データの最初。通常は data をセットしておく。
pe データの最後。通常は data + strlen(data) をセットしておく。
eof 入力の最後を示す。通常は pe と同じでよい。
cs 現在の状態

そして、

  %% write init;
  %% write exec;

で、ステートマシンの初期化コードと実行コードを出力する。

$ ragel -C domain.rl

コンパイルしてできる domain.c の中を見てみると、

write data;
write init;
write exec;

の部分が、ステートマシンのコードに置き換えられている様子がわかる。

IRC プロトコルパーザを含むサンプルを置いておきます。
http://limechat.net/sample/ragel_samples.zip