読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

言語バインディングを書こう

mixi

世田谷の某所から原宿まで自転車通勤しているのですが、そろそろ寒くなってきたので電車に切り替えようかと悩み中のmikioです。今回はTokyo Cabinetのスクリプト言語バインディングについて述べます。

スクリプト言語バインディングとは

TCはC言語で実装されたライブラリで、C言語(C89、C99)およびC++言語のプログラムから利用することができます。CやC++は各種の計算処理やシステムコールの呼び出しを直接的に記述できるので高速に動作するプログラムを作ることができる反面、ポインタ演算やメモリ管理などで致命的なバグを潜ませやすいので非常に注意深くコーディングを進めなければいけません。つまり、プログラムの実行速度は速いが、開発速度は遅いということです。

それに対して、PerlやRubyをはじめとするいわゆるスクリプト言語は、実行速度はCやC++に劣るものの、高水準かつ直感的な文法と強力なライブラリ群によって楽に素早く開発を進めることができます。mixiのシステムの多くの部分がPerlで記述されているのはそのためです。

プログラムの実行時間の8割9割がコードの1割2割の部分(ホットスポット)で費されるという経験則は広く知られているところです。したがって、ホットスポットをCで記述して実行効率の改善を図り、ホットスポット以外をスクリプト言語で記述するという手法がよく行われています。そうすれば、実行速度と開発速度の双方を高めることができるのです。そこで、CやC++で書かれたモジュールを各種のスクリプト言語から呼び出せるようにする機構が必要になりますが、それをスクリプト言語バインディングと呼びます。

データベースを使うアプリケーションでは、ホットスポットはデータベースを操作する部分であることがほとんどです。DBMのアプリケーションもその例外ではありません。そこでTCのスクリプト言語バインディングの登場です。Cで書かれた高速なDBMをスクリプト言語で楽々操作するなんて、幸せなことじゃないですか。

基本的な構造

基本的に、C言語のプログラムは関数として記述されます。全ての処理は関数の中に書かれ、個々の処理は関数を呼び出すことで実行されます。コマンドラインで実行される場合も結局はmain関数が呼び出されることで処理が開始されるわけです。したがって、スクリプト言語の処理系からC言語のプログラムを実行する際にも、必ず関数を呼び出す形になります。この構造は、Perlから呼ぶ場合でも、RubyからでもJavaからでもPythonからでもPHPからでも同じなので、1つの言語でのバインディングの書き方を覚えれば、他の言語でも比較的楽に進めることができます。細かい手順については残念ながら各種の処理系でまちまちのノウハウを学ばなければなりませんが、既に公開されているパッケージを真似すればそれほど迷うことはないでしょう。特にDBMの言語バインディングは例として最適です。Rubyの拡張ライブラリのマニュアルでもDBMが題材として使われているくらいです。

スクリプト言語から何とかしてCの関数を呼ぶということはわかったとして、次に以下のことを学ぶ必要があります。各言語ごとにAPIが整備されているので、対象の言語で具体的にどういう手順になるかを下調べしておくとよいでしょう。

  • Cの関数が入ったライブラリをロードする。
  • Cの関数を呼び出す。
  • 関数の引数として渡すために、スクリプト言語のオブジェクトをCのオブジェクトに変換する。
  • 関数の戻り値を受け取るために、Cのオブジェクトをスクリプト言語のオブジェクトに変換する。
  • Cのオブジェクトとスクリプト言語のオブジェクトのスコープと寿命を操作する。

その他に、作ったモジュールをインストールする方法やパッケージを作ったり配布したりする方法についても知っておく必要があります。

まずはPerlバインディング

正直言って私はPerlが苦手なのですが、Perlが強力な言語であり弊社や世の中の多くのシステムを支えている事実は個人的な好みを超越するところです。ということでTCのPerlバインディングの作成にまずは着手したわけですが、Perlの仕組みをCの側から見ることで、魔術的だと思っていた部分のカラクリがわかるようになり、少しずつPerlが好きになってきました。

Perlバインディングの作り方の全手順を紹介すると雑誌の特集並の分量になってしまうので、ここでは雰囲気がわかるくらいの説明を試みます。まずは、TCをPerlだけで書くつもりになって、インターフェイスだけをTokyoCabinet.pmというモジュールとして定義します。名前空間はTokyoCabinetにして、その中にHDB(ハッシュデータベース)とBDB(B+木データベース)のクラスを定義しましょう。HDBのコード例はこんな感じです。

package TokyoCabinet::HDB;
require XSLoader;
our $VERSION = '1.0';
XSLoader::load('TokyoCabinet', $VERSION);

sub new {
  my $class = shift;
  my $self = [0];
  $$self[0] = TokyoCabinet::hdb_new();
  bless($self, $class);
  return $self;
}

sub open {
    my $self = shift;
    my $path = shift;
    my $omode = shift;
    return TokyoCabinet::hdb_open($$self[0], $path, $omode);
}

sub close {
    my $self = shift;
    return TokyoCabinet::hdb_close($$self[0]);
}

リストへの参照にblessしてオブジェクトとして扱えるようにして、openとcloseというメソッドを定義しているというところはいたって普通ですね(スカラへの参照をblessしてもいいのですが、後で何か拡張したくなる予感がするのでリストを使っています)。見慣れないのは、XSLoaderというモジュールを取り込んで、そのload関数を呼んでいるところです。これはCで実装したライブラリのコードを呼び出せるようにする手順です。これによって、TokyoCabinet.soというライブラリを読み込んで、その中の関数を呼び出すことができるようになります。そして、TokyoCabinet::hdb_new() はTokyoCabinet.soの中にあるhdb_newという関数を呼び出すようになるわけです。

さて、ここからが本番です。Perlから呼び出すCの関数をXSUBと呼びます。XSUBは、XSというプリプロセッサの書式とCのコードを混在させて記述します。XSの主な仕事は、XSUBの呼び出し元であるPerlから渡されるPerlのオブジェクト(スカラや配列やハッシュ)をCのオブジェクトに変換することと、XSUBが終了した際にPerl側に返されるCのオブジェクトをPerlのオブジェクトに変換することです。hdb_newの例を見てみましょう。

void *
hdb_new()
PREINIT:
        TCHDB *hdb;
CODE:
        hdb = tchdbnew();
        tchdbsetmutex(hdb);
        RETVAL = hdb;
OUTPUT:
        RETVAL

TCのAPIを叩いてハッシュデータベースオブジェクトを作ってそのポインタをPerl側に返しています。先頭行の「void *」は「ポインタを返す」という意味です。その次の「hdb_new()」は「名前がhdb_newで、引数はない」という意味です。「PREINIT:」のセクションではXSUBの中で使うローカル変数を宣言しています。「CODE:」のセクションは実際のCのコードです。ここで「RETVAL」というのが唐突に出てきますが、これは冒頭で「void *」と宣言したCのオブジェクトをPerlで扱える状態で格納するための変数っぽいものだと思ってください。ここではデータベースオブジェクトのポインタを返したいのでそれをRETVALに入れます。そして最後に「OUTPUT:」のセクションでRETVALに入っているデータをPerl側に返すことを指示します。

hdb_newが返したポインタはPerlの世界では数値のスカラとして扱われるわけですが、それを他のメソッドに渡してまたCのポインタに戻せば、Cの世界ではそれをTCのAPIに渡して通常どおりにプログラミングができるわけです。次にhdb_openの例を見てみましょう。

int
hdb_open(hdb, path, omode)
        void *  hdb
        char *  path
        int     omode
CODE:
        RETVAL = tchdbopen(hdb, path, omode);
OUTPUT:
        RETVAL

Perl側でhdb_newの戻り値を$$self[0]に代入し、それがめぐりめぐってhdb_openの引数として渡されることになります。そしてXSの「void * hdb」の部分で1番目の引数をポインタとして扱うと宣言しておくことで、変数hdbには以前にhdb_newで作ったポインタがきちんと入っていることになるわけです。pathやomodeも、C側で期待する型を宣言しておけば、XSが勝手にそれっぽく変換た値を代入してくれます。あとは何事もなかったかのようにCでプログラムを書けばよいのです。ちなみにtchdbopenは処理の成否を真偽値で返すので、それをintとして返すことでPerl側のアプリケーションが処理の成否を判断できるようにしています。

念のためhdb_closeの例も見てみましょう。hdb_openの時と要領は同じですね。こんな感じで、その他のメソッドもバシバシ書いていけばよいのです(むしろ単調すぎる作業で、長時間やってると目がショボショボしてきます)。

int
hdb_close(hdb)
        void *  hdb
CODE:
        RETVAL = tchdbclose(hdb);
OUTPUT:
        RETVAL

XSで書いたプログラムは、TokyoCabinet.xsという名前にして、TokyoCabinet.pmと同じ場所に置いておきます。あとは通常のMakefile.PLを使ったパッケージングをすれば、めでたくTCのPerlバインディングの完成です。思ったより簡単ですよね? びびんないでやってみれば案外できるもんなんです。作成したモジュールを使うサンプルアプリケーションはこんな風になります。

そしてRubyバインディング

Rubyの場合はもっと簡単で楽しいです。ガベージコレクタのおかげでPerlでてこずることになるオブジェクトの寿命管理(リファレンスカウント)の問題に煩わされずに済みます。しかも、PerlバインディングではPerlで書かれたpmファイルとC(とXSマクロ)で書かれたxsファイルを組み合わせていましたが、RubyバインディングはCだけで記述できます(Perlでも同様のことはできますが、より繁雑です)。rbファイルすらいらないんです。tokyocabinet.cというファイルにこんなようなことを書きます。

#include "ruby.h"
#include <tchdb.h>

VALUE mod_tokyocabinet;
VALUE cls_hdb;
VALUE cls_hdb_data;

static VALUE hdb_initialize(VALUE vself){
  VALUE vhdb;
  TCHDB *hdb;
  hdb = tchdbnew();
  tchdbsetmutex(hdb);
  vhdb = Data_Wrap_Struct(cls_hdb_data, 0, tchdbdel, hdb);
  rb_iv_set(vself, "ptr", vhdb);
  return Qnil;
}

static VALUE hdb_open(VALUE vself, VALUE vpath, VALUE vomode){
  TCHDB *hdb;
  const char *path;
  int omode;
  Check_Type(vname, T_STRING);
  path = RSTRING(vname)->ptr;
  omode = (vomode == Qnil) ? HDBOREADER : NUM2INT(vomode);
  vhdb = rb_iv_get(vself, "ptr");
  Data_Get_Struct(vhdb, TCHDB, hdb);
  return tchdbopen(hdb, path, omode) ? Qtrue : Qfalse;
}

static VALUE hdb_close(VALUE vself){
  VALUE vhdb;
  TCHDB *hdb;
  vhdb = rb_iv_get(vself, "ptr");
  Data_Get_Struct(vhdb, TCHDB, hdb);
  return tchdbclose(hdb) ? Qtrue : Qfalse;
}

int Init_tokyocabinet(void){
  mod_tokyocabinet = rb_define_module("TokyoCabinet");
  cls_hdb = rb_define_class_under(mod_tokyocabinet, "HDB", rb_cObject);
  cls_hdb_data = rb_define_class_under(mod_tokyocabinet, "HDB_data", rb_cObject);
  rb_define_method(cls_hdb, "open", hdb_open, 2);
  rb_define_method(cls_hdb, "close", hdb_close, 0);
}

一見複雑そうですが、部分部分を見ていけばわかりやすい構造になっています。まずは、hdb_initializeに着目してください。Rubyの世界のオブジェクトはCの世界では全てがVALUEという型のオブジェクトとして扱うことができます(美しい!)。引数のオブジェクトをvselfという変数で受けていますが、これはもちろんレシーバオブジェクトです。Data_Wrap_StructというのはCのオブジェクトへのポインタをRubyのオブジェクトとして扱えるようにするものです。ここでポインタとtchdbdelを関連付けているのが重要なところで、ガベージコレクタでRubyのオブジェクトが回収された際にポインタの先にあるCのオブジェクトもきちんと片付けるように指示できるのです。そして、rb_iv_setは、ポインタをラップしたオブジェクトを "ptr" という名前でレシーバのプロパティとして持たせて、後で使えるようにしています。

次にopenを見ましょう。引数としてレシーバの他にvpathとvomodeを受け取っています。vpathは文字列であることが期待されていますので、Check_Typeで文字列オブジェクトであることを確認して、それがわかったところで安心してRSTRINGというマクロでキャストしてCの文字列のポインタを取得しています。vomodeは数値であることが期待されているので、NUM2INTというマクロで数値を取り出しています(このマクロは同時に型チェックもやってくれます)。あとはhdb_initializeの時に "ptr" として持たせておいたデータベースオブジェクトを取り出して、TCのAPIを叩きます。

最後にInit_tokyocabinetを見てください。モジュール名の前に「Init_」をつけた関数がライブラリのロード時に呼び出されることになっています。中では、rb_define_moduleで"TokyoCabinet"というモジュールを定義した上で、その配下に"HDB"や"HDB_data"というクラスを定義しています。"HDB"はユーザに公開するクラスで、"HDB_data" はポインタをラップするための内部クラスです。そして、rb_define_methodでCの関数をクラスのメソッドとして関連付けています。

パッケージの方法も特に難しいことはなく、extconf.rbを使って楽々できます。C言語だけでRubyの処理系を使ったプログラムができるってのは、逆転の発想で何だか面白いですよね。作成したモジュールを使うサンプルアプリケーションはこんな風になります。

まとめ

実行時の性能が求められる部分は、最適なアルゴリズムをカリカリにチューンしたCで記述して悦に浸りましょう。それ以外の部分は、お好みのスクリプト言語で書きやすく読みやすいコードを記述して、人に愛されるソフトウェアをばんばん作っていきましょう。

TCのPerlバインディングとRubyバインディングは既にリリースしていますし、Javaバインディングも目下開発中です。それらは私がメンテナンスしていきます。私の知る限りでは、他にもPythonバインディングとPHPバインディングを書いている方々がおられますので、UNIX系のほとんどプログラマにはTCを使っていただけるようになると思います。

その他の言語バインディングも作ってやろうという方、大歓迎です。その際には、こちらのIDL(インターフェイス定義言語)を参考にしてAPIを設計していただけると、使い方が習得しやすくなってより喜ばれると思います。

そもそも「スクリプト言語バインディング」という言い方はC言語側からの視点で、スクリプト言語側から見れば「C言語による拡張モジュール」です。拡張モジュールを使うとプログラミングの世界がものすごく広がります。詳しいやり方は「プログラミングPerl」や「プログラミングRuby」でわかりやすく説明されているので、それらを見ながらぜひ挑戦してみてください。

追記:ほぼXSだけでモジュールを作るサンプルを小山浩之氏が作ってくれましたので、そちらも参考になるかと思います。