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

mixi engineer blog

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

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

mixi

東京ディズニーシーで買ってきたDuffyというテディベアがお気に入りで、頭に載せて寝るとよく眠れることを発見してウキウキのmikioです。さて今回は、Tokyo Cabinet(TC)のJavaバインディングとLuaバインディングの作り方と使い方について紹介します。

Javaバインディング

TCのJavaバインディングの初版は昨年11月には出ていて別に新しい話題でもないのですが、以前とりあげた言語バインディングの記事ではJavaについて解説しなかったので、ここで改めてとりあげます。なお、この解説ででてくるサンプルコードを試すべく、環境変数「CLASSPATH」と環境変数「LD_LIBRARY_PATH」にカレントディレクトリを含めてください。

CLASSPATH=.
LD_LIBRARY_PATH=.
export CLASSPATH LD_LIBRARY_PATH

Javaバインディングは、JNI(Java Naitive Interface)という規約に乗っ取って書くことになるわけですが、まあそんなに難しいものでもありません。JavaからCやその他のネイティブコードを呼び出すには、以下のようなクラスを定義することになります。

public class Hello {
  static {
    System.loadLibrary("hello");
  }
  public static native void hello();
  public static void main(String[] args){
    hello();
  }
}

「static {}」 のブロックはスタティックイニシャライザという仰々しい名前なのですが、要はクラスファイルがロードされた時に暗黙的に実行されるべき処理を書くものです。その中でSystemクラスのloadLibraryメソッドを呼んで、ネイティブのライブラリの中の関数を使えるようにするのです。また、「hello」というメソッドにはnative宣言がされているだけで本体の定義がありません。このメソッドが呼び出されると、リンク済みのライブラリの中から関数を探して本体として実行されます。

上記をHello.javaという名前のファイルとして保存したならば、以下のように普通にクラスファイルをビルドすることができます。

javac Hello.java

さらに、以下のコマンドを実行することで、HelloクラスのネイティブメソッドをCで実装するためのヘッダファイルを自動生成することができます。

javah Hello

そうすると、Hello.hというヘッダができます。そこにhelloメソッドが呼び出された時に探される関数のシグネチャ(名前や引数の型指定)が書いてあります。以下のようになっているはずです。

JNIEXPORT void JNICALL Java_Hello_hello
  (JNIEnv *, jclass);

シグネチャが分かったところで、Hallo.hをインクルードして本体を実装してあげます。以下のコードをhello.cとして保存してください。

#include "Hello.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_Hello_hello
  (JNIEnv *env, jclass cls){
  printf("Hello, World\\n");
}

そしたら、以下のコマンドでライブラリをビルドすることができます。「/usr/java」の部分はJDKをインストールした場所に、「linux」の部分はお使いのプラットフォームの名前に読み替えてください。

gcc -I/usr/java/include -I/usr/java/include/linux \\
  -fPIC -shared -o libhello.so hello.c

これで、実行に必要な「Hello.class」と「libhello.so」の二つが手に入りました。前者はクラスパスの通った場所に、後者はライブラリ検索パスの通った場所にインストールすることになります。今回はカレントディレクトリで両者とも大丈夫なので、さっそく動かしてみましょう。

$ java Hello
Hello, World

ここまで理解できたら、DBMのバインディングを作るのも簡単です。ハッシュデータベースAPIの骨格部分だけ抜き出すと、以下のJavaプログラムになります。

public class HDB {
  private long ptr = 0;

  static {
    System.loadLibrary("jtokyocabinet");
    init();
  }
  private static native void init();

  public HDB(){
    initialize();
  }
  private native void initialize();

  protected void finalize(){
    destruct();
  }
  private native void destruct();

  public native boolean open(String path, int omode);
  public native boolean close();
  public native boolean put(byte[] key, byte[] value);
  public native byte[] get(byte[] key);
}

スタティックイニシャライザでライブラリをロードし、initというネイティブメソッドを呼んでいます。スタティックイニシャライザ自身はネイティブにできないので、initというネイティブメソッドを分離してC側で必要なクラス初期化処理を行わせるのがポイントです。同じくコンストラクタやファイナライザもネイティブメソッドにできないので、initializeというメソッドを分離してC側で必要なオブジェクト初期化処理を行わせ、destructというメソッドを分離してC側で必要なオブジェクト終了処理を行わせています。C側のコードは以下のようになります。

jfieldID hdb_fid_ptr;

JNIEXPORT void JNICALL Java_tokyocabinet_HDB_init
(JNIEnv *env, jclass cls){
  cls_hdb = cls;
  hdb_fid_ptr = (*env)->GetFieldID(env, cls, "ptr", "J");
}

JNIEXPORT void JNICALL
Java_tokyocabinet_HDB_initialize(JNIEnv *env, jobject self){
  TCHDB *hdb = tchdbnew();
  (*env)->SetLongField(env, self, hdb_fid_ptr, (intptr_t)hdb);
}

JNIEXPORT void JNICALL
Java_tokyocabinet_HDB_destruct(JNIEnv *env, jobject self){
  TCHDB *hdb = (TCHDB *)(intptr_t)(*env)->GetLongField(env, self, hdb_fid_ptr);
  tchdbdel(hdb);
}

クラス初期化を担うinitでは、hdb_fid_ptrというグローバル変数にptrというフィールドにアクセスするためのIDを入れています。このptrはJava側で宣言されているlong型の変数で、C側のリソースをそれに割り付けて管理するためのものです。Javaにはポインタのための型がないので、現状のほとんどの処理系のポインタを扱えるであろう64ビットの数値型をポインタ型として代用しています。

「(*env)->GetFieldID(env, cls, "ptr", "J");」という表現がとにかく気持ち悪いわけですが、envというのは現在実行されているスレッドの環境にアクセスするためのハンドルで、JNIのAPI関数は全てそこに納められた関数ポインタを経由して呼び出すことになっているのです。第1引数にもenvが出てきますが、オブジェクト指向っぽく関数を呼び出したとしてもレシーバを暗黙的に指示できないC言語の宿命で仕方なくつけているようなものです。ほとんどのJNI関数の呼び出し形式は「(*env)->XXX(env, ...)」となるので慣れるしかありません。なお、clsはクラスオブジェクトで、"J" はlong型のシグネチャを意味します。

さて、オブジェクト初期化を担うinitializeでは、Cのハッシュデータベースオブジェクトを生成した上で、hdb_fid_ptrに入れておいたフィールドIDを介して、オブジェクトのptrフィールドに代入しています。オブジェクト終了を担うdestructでは、逆にptrフィールドからポインタを取り出して破棄しています。openとかcloseとかもだいたい同じ感じで、オブジェクトのフィールドからポインタを取り出して、引数として受け取ったJavaの値からCの値を取り出して、何か処理して、戻り値としてJavaの値を返します。

JNIEXPORT jboolean JNICALL Java_tokyocabinet_HDB_open
(JNIEnv *env, jobject self, jstring path, jint omode){
  if(!path){
    throwillarg(env);
    return false;
  }
  TCHDB *hdb = (TCHDB *)(intptr_t)(*env)->GetLongField(env, self, hdb_fid_ptr);
  jboolean icpath;
  const char *tpath = (*env)->GetStringUTFChars(env, path, &icpath);
  if(!tpath){
    throwoutmem(env);
    return false;
  }
  bool rv = tchdbopen(hdb, tpath, omode);
  if(icpath) (*env)->ReleaseStringUTFChars(env, path, tpath);
  return rv;
}

JNIEXPORT jboolean JNICALL Java_tokyocabinet_HDB_close
(JNIEnv *env, jobject self){
  TCHDB *hdb = (TCHDB *)(intptr_t)(*env)->GetLongField(env, self, hdb_fid_ptr);
  return tchdbclose(hdb);
}

openの引数を見てみると、selfはレシーバオブジェクト、pathはString型のオブジェクト、omodeはint型の値であることがわかります。で、jstringからCのchar*文字列を取り出すことが必要となるわけですが、そのためにGetStringUTFCharsというAPIを使っています。それの第3引数でicpathというbool型へのポインタを渡していますが、JavaVM内ではString型やArray型の各要素は連続した領域にあるとは限らない(実際にはほとんど連続しているのですが)ので、それでもC側に連続した領域tpathを返すべく領域がコピーされたかどうかのフラグをicpathに代入してくれるのです。なので、icpathが真になっている場合は、そのtpathを使い終わった時点でReleaseStringUTFCharsで領域を開放しています。jintはintそのものなので、特にAPIなどを呼ばなくてもint型の値として利用することができます。

ここまで来れば、あとはJNIの仕様書でAPIを調べつつ、ひたすらJavaとCの間で値の相互変換を書いていくという単純作業をするだけです。実例としてTCのコードが参考になると思います。あと、javadocでドキュメントを書く工数を見積もっておくのも重要です。TCのJavaバインディングのドキュメントも結構一生懸命書きました。

使い方は簡単、Cと同じく、オブジェクトを作ってDBを開いて、レコードを格納したり取得したりして、最後にDBを閉じるのです。こんな風になります。

// パッケージをインポート
import tokyocabinet.*;

public class TCHDBEX {
  public static void main(String[] args){

    // データベースオブジェクトを作る
    HDB hdb = new HDB();

    // データベースを開く
    if(!hdb.open("casket.hdb", HDB.OWRITER | HDB.OCREAT)){
      System.err.println("open error: " + hdb.errmsg());
    }

    // レコードを格納する
    if(!hdb.put("foo", "hop")){
      System.err.println("put error: " + hdb.errmsg());
    }

    // レコードを取得する
    String value = hdb.get("foo");
    if(value != null){
      System.out.println(value);
    } else {
      System.err.println("get error: " + hdb.errmsg());
    }

    // データベースを閉じる
    if(!hdb.close()){
      System.err.println("close error: " + hdb.errmsg());
    }

  }
}

Luaバインディング

Luaバインディングは、ここ2週間くらいでうぁーっと書いた新作です。C用のAPIが簡潔で分かりやすいという点では、Lua > Ruby >> Java > Perl が個人的な感想なのですが、「Luaスタック」の存在を常に念頭に置いてプログラミングしなければならないので、バインディングの書きやすさでは Ruby >> Lua > Java > Perl といったところでしょうか。

自作のプログラムにLuaを組み込む方法については前の記事で紹介しましたが、今回はLuaインタープリタから自作のバインディングを呼び出して連携する方法について考えます。LuaではJavaと違って、ネイティブコードのプロトタイプをクラスとしてLua言語で定義しておく必要はありません。Rubyの場合と同じく、直接CでLuaのオブジェクトを定義することができます。ということで、Hello Worldは以下のCプログラムだけでOKです。

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

int do_hello(lua_State *lua){
  printf("Hello, World\\n");
  return 0;
}

int luaopen_hello(lua_State *lua){
  lua_register(lua, "hello", do_hello);
}

関数do_helloがネイティブメソッドです。Luaにおいてはレシーバも引数も戻り値も全てLuaスタックを介してやりとりされるので、全てのユーザ定義関数の型は void (*)(lua_State *lua) になります。この簡潔さにしびれますね。luaopen_hello(モジュール名に応じてluxopen_xxxxのxxxxを変える)という関数が、ライブラリが呼び出された時に実行されることになっているので、そこで初期化処理を行います。lua_registerというAPI関数をつかって、do_hello関数を "hello" というグローバル変数として登録しています。上記をhello.cとして保存したら、以下のコマンドでコンパイルできます。

gcc -I/usr/include/lua5.1 -fPIC -shared -o hello.so hello.c

必要なファイルは上記で生成される「hello.so」のみです。これはLuaのライブラリ検索パスのどこか(lua -e 'print(package.clibpath)' でわかる)にインストールすることになります。大抵の場合、デフォルトでカレントディレクトリにライブラリ検索パスが通っているので、以下のようなプログラムだけ書けばテストできます。

require("hello")
hello()

上記をtest.luaとして保存したならば、さっそく実行してみましょう。なお、Luaプログラムをhello.luaとして保存するとカレントディレクトリにあるhello.soが隠蔽されてしまうので、別の名前にしてください。

$ lua test.lua
Hello, World

ここまで理解できたら、DBMのバインディングを作るのも簡単です。ハッシュデータベースAPIの骨格部分だけ抜き出すと、以下のLuaプログラムになります。

module("tokyocabinet")

function tokyocabinet.hdbnew() end

function hdb:open(path, omode) end
function hdb:close() end
function hdb:put(key, value) end
function hdb:get(key) end

実際にはLuaバインディングを書く際にLuaコードは必要ないので、上記は単なるメモにすぎません。C側のコードは以下のようになります。

// CのポインタをLua側で管理するためのラッパー
typedef struct {
  TCHDB *hdb;
} HDBDATA;

// ロード時の初期化:モジュールとコンストラクタのの定義
int luaopen_tokyocabinet(lua_State *lua){
  // Luaスタックの初期化
  lua_settop(lua, 0);
  // モジュールのテーブルを作成してスタックに積む
  lua_newtable(lua);
  // コンストラクタをスタックに積む
  lua_pushcfunction(lua, hdb_new);
  // モジュールのテーブルにコンストラクタを配備
  lua_setfield(lua, -2, "hdbnew");
  // モジュールをグローバル変数として配備
  lua_setglobal(lua, "tokyocabinet");
}

// コンストラクタの実装
static int hdb_new(lua_State *lua){
  // DBオブジェクトのテーブルを生成してスタックに積む
  lua_newtable(lua);

  // ラッパーを生成してスタックに積む
  HDBDATA *data = lua_newuserdata(lua, sizeof(*data));
  // CのDBポインタをラッパーに入れる
  data->hdb = tchdbnew();

  // メタテーブルを生成してスタックに積む
  lua_newtable(lua);
  // ファイナライザをスタックに積む
  lua_pushcfunction(lua, hdb_del);
  // メタテーブルのGCイベントにファイナライザを配備
  lua_setfield(lua, -2, "__gc");
  // メタテーブルをDBラッパーに配備
  lua_setmetatable(lua, -2);

  // ラッパーをオブジェクトに配備
  lua_setfield(lua, -2, "_hdbdata_");

  // openメソッドをスタックに積む
  lua_pushcfunction(lua, hdb_open);
  // openメソッドをDBオブジェクトに配備
  lua_setfield(lua, -2, "open");
  // closeメソッドをスタックに積む
  lua_pushcfunction(lua, hdb_close);
  // closeメソッドをDBオブジェクトに配備
  lua_setfield(lua, -2, "close");
  // putメソッドをスタックに積む
  lua_pushcfunction(lua, hdb_put);
  // putメソッドをDBオブジェクトに配備
  lua_setfield(lua, -2, "put");
  // getメソッドをスタックに積む
  lua_pushcfunction(lua, hdb_get);
  // getメソッドをDBオブジェクトに配備
  lua_setfield(lua, -2, "get");

  // スタックトップにあるDBオブジェクトを戻り値にする
  return 1;
}

// ファイナライザの実装
static int hdb_del(lua_State *lua){
  // 暗黙の第1引数からラッパーオブジェクトを取得
  HDBDATA *data = lua_touserdata(lua, 1);
  // DBポインタを取り出す
  TCHDB *hdb = data->hdb;
  // DBポインタを破棄
  tchdbdel(hdb);
  // 戻り値はなし
  return 0;
}

なんだか複雑な感じですが、処理内容はコメントを読めばわかると思います。大まかな流れは、モジュールにコンストラクタを登録し、コンストラクタはオブジェクトを作り、その中でCポインタのラッパーをフィールドに持たせ、また各種メソッドもフィールドに持たせる、ということです。

常にLuaスタックを意識することが大事です。lua_new*とかlua_push*というAPIを実行するとスタックトップに新しい要素が追加されるので、それをlua_set*でスタックトップから別の要素のフィールドなどに退避させていくのです。「-2」という数はなんなのかという話ですが、この番号はスタックトップを-1とした序数で、つまり-2はスタックトップから2番目という意味です。例えば「lua_setfield(lua, -2, "open")」は、今スタックトップにある要素を、スタックトップから2番目にあるテーブルの "open" というフィールドとして退避させるという意味です。

メタテーブルとは何かと言うと、オブジェクトに対するいろんなイベントを捕捉して何らかの処理を行うためのイベントハンドラのテーブルです。ここでは、Cポインタのラッパーオブジェクトに対するガベージコレクションをイベントとして捕捉して、ファイナライザを呼ぶようにしています。

openとかcloseとかもだいたい同じ感じで、DBオブジェクトのフィールドからラッパーオブジェクトを取り出して、引数として受け取ったLuaの値からCの値を取り出して、何か処理して、戻り値としてLuaの値を返します。Luaは関数の引数の一致を検査しないので、C側でスタックトップの位置などで引数の数や型を調べているのがポイントです。アプリケーション側で引数の記述ミスをすることは日常茶飯事(特にLuaのような宣言の不要な言語ではありがち)なので、引数の検査は絶対にやっておいた方がいいです。

static int hdb_open(lua_State *lua){
  int argc = lua_gettop(lua);
  if(argc < 2 || argc > 3 || !lua_istable(lua, 1)){
    lua_pushstring(lua, "open: invalid arguments");
    lua_error(lua);
  }
  lua_getfield(lua, 1, "_hdbdata_");
  HDBDATA *data = lua_touserdata(lua, -1);
  const char *name = lua_tostring(lua, 2);
  int omode = argc > 2 ? lua_tointeger(lua, 3) : HDBOREADER;
  if(!data || !name){
    lua_pushstring(lua, "open: invalid arguments");
    lua_error(lua);
  }
  TCHDB *hdb = data->hdb;
  if(tchdbopen(hdb, name, omode)){
    lua_pushboolean(lua, true);
  } else {
    lua_pushboolean(lua, false);
  }
  return 1;
}

static int hdb_close(lua_State *lua){
  int argc = lua_gettop(lua);
  if(argc != 1 || !lua_istable(lua, 1)){
    lua_pushstring(lua, "close: invalid arguments");
    lua_error(lua);
  }
  lua_getfield(lua, 1, "_hdbdata_");
  HDBDATA *data = lua_touserdata(lua, -1);
  if(!data){
    lua_pushstring(lua, "close: invalid arguments");
    lua_error(lua);
  }
  TCHDB *hdb = data->hdb;
  if(tchdbclose(hdb)){
    lua_pushboolean(lua, true);
  } else {
    lua_pushboolean(lua, false);
  }
  return 1;
}

ここまで来れば、あとはLuaのリファレンスマニュアルでAPIを調べつつ、ひたすらLuaとCの間で値の相互変換を書いていくという単純作業をするだけです。実例としてTCのコードが参考になると思います。あと、luadocでドキュメントを書く工数を見積もっておくのも重要です。TCのLuaバインディングのドキュメントも結構一生懸命書きました。

使い方は簡単、Cと同じく、オブジェクトを作ってDBを開いて、レコードを格納したり取得したりして、最後にDBを閉じるのです。こんな風になります。

-- パッケージをインポート
require("tokyocabinet")

-- データベースオブジェクトを作る
hdb = tokyocabinet.hdbnew()

-- データベースを開く
if not hdb:open("casket.hdb", hdb.OWRITER + hdb.OCREAT) then
   ecode = hdb:ecode()
   print("open error: " .. hdb:errmsg(ecode))
end

-- レコードを格納する
if not hdb:put("foo", "hop") then
   print("put error: " .. hdb:errmsg())
end

-- レコードを取得する
value = hdb:get("foo")
if value then
   print(value)
else
   print("get error: " .. hdb:errmsg())
end

-- データベースを閉じる
if not hdb:close() then
   print("close error: " .. hdb:errmsg())
end

おまけ:TCのLuaバインディングで使える便利機能

Luaの標準の配布セットはANSI Cに準拠する全ての環境で動作するように可搬性のポリシーが明確化されています。したがって、ANSI Cの標準関数として定義されている機能以外は使うことができないのです。例えばディレクトリを開いてファイルのリストを取ることもできないし、秒を越える精度の時間を知ることもできません。もちろん別途に配布されている各種モジュールを利用すれば他の言語でできることはほとんどできるようになるのですが、多少面倒です。なので、TCを入れるだけでもちょっとしたアプリがすぐに作れるように、最低限必要だろうという機能群はつけておきました。TTのLua拡張にも同様の機能は入れてあります。

tokyocabinet.pack(format, ary, ...)
数値配列を文字列に変換。変換文字 c(int8_t)、C(uint8_t)、s(int16_t)、S(uint16_t)、i(int32_t)、I(uint32_t)、l(int64_t)、L(uint64_t)、f(float)、d(double)、n(uint16_t in network byte order)、N(uint32_t in network byte order)、M(uint64_t in network byte order)、w(BER encoding)をサポート。
tokyocabinet.unpack(format, ary, ...)
文字列を数値配列に変換。変換文字はpackと同じ。
tokyocabinet.codec(mode, str)
文字列を符号化/復号。modeには "url"(URL encoding)、"~url"(URL decoding)、"base"(Base64 encoding)、"~base"(Base64 decoding)、"hex"(hexadecimal encoding)、"~hex"(hexadecimal decoding)、"pack"(PackBits encoding)、"~pack"(PackBits decoding)、"tcbs"(TCBS encoding)、"~tcbs"(TCBS decoding)、"deflate"(Deflate encoding)、"~deflate"(Deflate decoding)、"gzip"(GZIP encoding)、"~gzip"(GZIP decoding)、"bzip"(BZIP2 encoding)、"~bzip"(BZIP2 decoding)、"xml"(XML escaping)、"~xml"(XML unescaping)のいずれかを指定。
tokyocabinet.hash(mode, str)
文字列のハッシュ値を取得。modeには "md5"(MD5 in hexadecimal format)、"md5raw"(MD5 in raw format)、"crc32"(CRC32 checksum number)のいずれかを指定。
tokyocabinet.time()
現在時刻の秒(マイクロ秒精度)を取得。
tokyocabinet.sleep(sec)
マイクロ秒精度でスリープ
tokyocabinet.stat(path)
ファイルのメタ情報を取得。戻り値のテーブルに "dev", "ino", "mode", "nlink", "uid", "gid", "rdev", "size", "blksize", "blocks", "atime", "mtime", "ctime" を代入。さらに、"_regular"(通常ファイルかどうか)、"_directory"(ディレクトリかどうか)、"_readable"(読めるかどうか)、"_writable"(書けるかどうか)、"_executable"(実行できるかどうか)、"_realpath"(正規化パス)も代入。
tokyocabinet.glob(pattern)
パターンに一致するファイルパスのリストを取得。
tokyocabinet.remove(path)
ファイルやディレクトリを削除する。ディレクトリの場合は中身を再帰的に削除する。
tokyocabinet.mkdir(path)
ファイルやディレクトリを生成する。
tokyocabinet.chdir(path)
カレントディレクトリを変更する。

まとめ

TCのJavaバインディングとLuaバインディングの作り方と使い方を紹介しました。新しいプログラミング言語を覚えたついでにお気に入りのCライブラリのバインディングを作ることで、処理系の挙動をとてもよく理解することができます。Javaの例とLuaの例を比較しても、やっていることはそんなに変わらないですよね。プログラミング言語を5個も6個も使いこなす人が結構いますが、大体似ていることが多いので、学習の要領を身につけたであろう3言語目以降になると苦労せずに覚えられるようになるんでしょうね。バインディングの作成もおそらく同じようなものです。

ユーザ人口がまだそれほど多くないLuaのバインディングをなぜわざわざ書いたかと言えば、TCとLuaは相性がとてもよいと考えるからです。双方とも、あまり複雑でない処理を簡単に記述して高速に実行することを目的としているからです。Luaはゲーム業界での採用実績が多いようですが、そういったユースケースでもTCが活用できるようになると素敵ですね。

ということでLuaバインディングが加わり、今やTCは少なくともC、C++、Perl、Ruby、Java、Lua、Python、PHP、Scheme、Common Lisp、Erlangから使えるようになっています。もうさすがに十分かなと。それよりWin32版を作れという声が多いのですが、保守しきれないので私自身は絶対にやりません(とはいえ着手してくださってる方がいるみたいですが...)。