豪鬼メモ

MT車練習中

TkrzwのC言語インターフェイス

C++言語で書かれているデータベースライブラリTkrzwは、当然ながらC++のAPIを提供するが、C言語からは使えない。なので、主要機能をクラスでない関数でラップしたC言語のAPIも提供することにした。C言語のプロジェクトで使えることはもちろん、その他の言語で利用する際にもC言語のAPIをラップした方が楽なことがあるからだ。具体的に言えば、Go言語に組み込むためにC言語のAPIがあると便利だ。


結論としては、こんな感じでC言語からも使えるようになった。まるでANSI-C標準のFILEポインタを扱っているかのような簡単さで、データベースプログラミングができる。

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

int main(int argc, char** argv) {
  // データベースファイルを開く
  TkrzwDBM* dbm = tkrzw_dbm_open(
      "casket.tkh", true, "truncate=true,num_buckets=100");
  
  // レコードを格納する。サイズが-1だと、strlenでサイズを決めてくれる
  tkrzw_dbm_set(dbm, "foo", -1, "hop", -1, true);
  tkrzw_dbm_set(dbm, "bar", -1, "step", -1, true);
  tkrzw_dbm_set(dbm, "baz", -1, "jump", -1, true);

  // レコードを検索する。バイナリが取得できるが、文字列としても扱える
  char* value_ptr = tkrzw_dbm_get(dbm, "foo", -1, NULL);
  if (value_ptr) {
    puts(value_ptr);
    free(value_ptr);
  }

  // データベース内の全レコードをイテレータで巡る
  TkrzwDBMIter* iter = tkrzw_dbm_make_iterator(dbm);
  tkrzw_dbm_iter_first(iter);
  while (true) {
    char* key_ptr = nullptr;
    if (!tkrzw_dbm_iter_get(iter, &key_ptr, NULL, &value_ptr, NULL)) {
      break;
    }
    printf("%s:%s\n", key_ptr, value_ptr);
    free(key_ptr);
    free(value_ptr);
    tkrzw_dbm_iter_next(iter);
  }
  tkrzw_dbm_iter_free(iter);
  
  // データベースファイルを閉じる
  tkrzw_dbm_close(dbm);

  return 0;
}

C++が使える環境でC言語を使う意味は今となってはほとんどない。TkrzwがC++のランタイムライブラリに依存している時点で、たとえCのAPIを利用したとしても、オブジェクトファイルのサイズやプロセスのRSS(resident set size = 共有ライブラリの使用分を含めたメモリ使用量)を節約する効果はない。よって、組み込み系などでCしか使えないという場合にTkrzwを使えることはない。しかし、プログラミングの初心者であったり、学生が授業で取り組むなどの場合には、C言語しか学んでいないという理由で選択することもあるだろう。DBMは練習用の課題で使うにも最適な単純さを備えるので、そういう人たちにも是非使ってもらいたいところだ。

話は逸脱するが、上述の例でも何事もなかったかのようにポインタが出てくるが、学習者にとってはポインタって厄介なものだ。「オブジェクトのハンドルの直接的な表現であって、実際には仮想メモリ上のアドレスなんだよ」とかいう説明をしようものなら、プログラミングが嫌いになること請け合いだ。C++ならポインタを隠蔽しやすくはなるのだが、代わりに覚えにゃならん仕様が膨大になるし、どっちにしろポインタやリソース管理の概念は習得しないと言語を使いこなせないので、C++で学習曲線が改善するとも思えない。関数やデータ抽象やオブジェクト指向の概念を身につけ、さらにはプログラミングの楽しさを味わうのであれば、JavaScriptやPythonやRubyを先に学ぶべきだろう。それらを身に着けて自信をつけてから、JavaやC++を学んでも遅くはない。それと前後して、原始的なリソース管理の修行としてCを学ぶのもありだとは思うが、まあ正直そんなに意味があるとは思えない。

話を戻すと、C言語を積極的に使う必要はないと私は思うが、C言語を使わざるを得ない場合は実際にある。ところで、TkrzwのGo言語のインターフェイスを書こうとしたところ、cgoという機能でCの変数や関数をラップするのが常道だということになった。C++のシンボルを使うのはマングリングの問題があって一筋縄ではいかないらしい。そこは何とか頑張ってよと正直思うところだが、Go言語の開発動機はC++が嫌いだからということらしいので、Goの標準機能であるcgoがC++に歩み寄ることはあんまり期待できないような気もする。ということで、C言語を使わざるを得ない場合に自分が当てはまってしまった。使いたいのはGo言語なんだけども。

C++のAPIをC用に書き換えるのはそんなに難しい話ではない。Tkrzwの主要機能はPolyDBMというアダプタクラスから利用できるようにしているので、そのラッパーを書けば良い。Pythonのように、メソッドのレシーバを第1引数にして、その他の引数をその後に並べればよい。また、Cには名前空間がないので、名前の不意な衝突を避けるために全ての識別子には明示的に接頭辞tkrzwをつけ、さらにメソッドであった関数には元のクラスの名前を接頭辞として並べる。

C++:
dbm.Set("hello", "world");

C:
tkrzw_dbm_open(dbm, "hello", -1, "world", -1);

細かいところで面倒事がいっぱいある。まず、stringクラスやstring_viewクラスが使えないので、バイト列はポインタとサイズの二つの変数で管理する必要がある。また、stringクラスを使えばRAIIとデストラクタによってリソースの暗黙的な解放ができるが、Cではその機構がないので、明示的にfreeを書かないといけない。

C++:
string value;
dbm.Get("hello", &value);
cout << value << endl;

C:
char* value_ptr = dbm_get(dbm, "hello", 5, NULL);
puts(value_ptr);
free(value_ptr);

レコードの検索結果を戻り値として返すと、失敗した時のステータスコードをどうやって返すかというのが問題になる。ステータスコードを戻り値にする手もあるが、そうすると検索結果を格納した領域へのポインタを引数のポインタの参照先に格納することになる。つまり、ポインタポインタを引数として受け取ることになり、やたら複雑になってしまう。学習者にとってはポインタでさえ嫌なのに、ポインタポインタって何だよと。

int32_t dbm_get(TkrzwDBM*, const char* key_ptr, size_t key_size,
                char** value_ptr_ptr, size_t* value_size_ptr);
...
char* value_ptr = NULL;
size_t value_size = 0;
int32_t error_code = dbm_get("dbm, "hello", &value_ptr, &value_size);

そこで、POSIXのerrnoやWindowsのGetLastErrorと同じように、ステータスコードはスレッドローカルストレージで管理することにした。tkrzw_last_status_codeやtkrzw_last_status_messageといった関数で、直前の関数呼び出しのエラー情報を取得することができる。それらはスレッド毎に別々のデータを保持するので、スレッドセーフである。

char* value_ptr = dbm_get(dbm, "hello", 5, NULL);
if (value_ptr) {
  puts(value_ptr);
  free(value_ptr);
} else {
  print("Error: %d: %s\n", tkrzw_last_status_code(), tkrzw_last_status_message());
}

ついでに言うと、文字列を表すのにstringの代わりにchar*を使う場合、末尾の先にヌルコード0x00を番兵として置く必要がある。バイナリデータも扱えるようにするため、ポインタとサイズで管理するのが前提ではあるが、getメソッドなどが返すバイト列にはおまけでヌルコードの番兵が置かれる仕様になっている。よって、返されたポインタをそのままC文字列前提の処理に使うことができる。

他にも、いろいろ面倒だ。stringもvectorやmapもないので、名前付きパラメータのkey/value連想配列を簡潔に表現するのが難しい。苦肉の策として、"key1=value1,key2=value2,..." のような書式文字列を渡す方法にした。その文字列を動的に作るのはかえって面倒くさい気もするが、ハードコードする分には楽だ。

TkrzwDBM *dbm = tkrzw_dbm_open("casket.tkh", true, "truncate=true,num_buckets=100");

Cのリソース管理の煩雑さを端的に表すのが、この関数だろう。tkrzw_dbm_searchは、モードとパターンの文字列を受け取り、それにマッチするキーのリストをデータベース内で探して返す。モードには中間一致、前方一致、後方一致、正規表現、編集距離が指定できる。アルゴリズムの詳細は内部に隠蔽され、データベースのクラスに応じた最善の手段が自動的に採用される。要は、めちゃ便利な機能なのだが、Cだと妙に複雑なインターフェイスになる。C++だと、結果は vector < string > として返せばよいのだが、Cにはvectorもstringもない。よって、mallocで確保したポインタの配列の個々の要素にmallocしたバイト列とそのサイズを持たせ、要素数をまた別の変数で持たせるということになる。いわゆるIliffe vectorというやつだ。

typedef struct {
  char* ptr;
  int32_t size;
} TkrzwStr;

TkrzwStr* tkrzw_dbm_search(
    TkrzwDBM* dbm, const char* mode, const char* pattern_ptr, int32_t pattern_size,
    int32_t capacity, bool utf, int32_t* num_matched);

void tkrzw_free_str_array(TkrzwStr* array, int32_t size);

TkrzwStrはstringの代用で、その配列はvectorの代用だ。それらを使い終わったら明示的に破棄する必要があるため、専用のtkrzw_free_str_arrayという関数も用意してある。この手の再発明をあちこちでしなきゃいけないというのが、Cプログラミングの最も悲しいところだ。逆に言えば、C++の便利さの半分くらいはSTLの便利さから来ていると言えるかもしれない。

Cには関数オブジェクトやラムダ式がないので、コールバックも、関数ポインタとそのコンテキストのvoidポインタに分けて渡す必要がある。例えば、レコードの値を10進数の文字列とみなしてインクリメントするコールバック関数は以下のように使う。

const char* proc_increment(void* arg, const char* key_ptr, int32_t key_size,
                           const char* value_ptr, int32_t value_size, int32_t* new_value_size) {
  // 新しい値の格納先を呼び出し側から受け取る
  char* num_buf = (char*)arg;
  // 古い値を数値に変換する
  int64_t num_value = 0;
  if (value_ptr != nullptr) {
    num_value = atoi(value_ptr);
  }
  // 新しい値を格納先に記憶する
  *new_value_size = sprintf(num_buf, "%d", num_value + 1);
  // 新しい値の場所をデータベースに教える
  return num_buf;
}

void do_increment(TkrzwDBM* dbm, const char* key_ptr, int32_t key_size) {
  // 値の格納先をコールバック外に確保して寿命を管理する
  char num_buf[32];
  // 該当のレコードにコールバックを呼び出す。
  tkrzw_dbm_process(dbm, key_ptr, key_size, proc_increment, &num_buf, true);
}

といった感じで、Cに無理やりオブジェクト指向的な作法を持ち込んでいて、20世紀に戻ったかのような懐かしささえ湧き上がる。ともあれ、イテレータやCompareExchangeやその他のユーティリティも実装したので、CからもTkrzwの主要機能が普通に使える。実際のAPIについてはこちらの文書をご覧いただきたい。


まとめ。TkrzwのC++のAPIをラップしたC言語のインターフェイスを用意した。これによって、C言語使いにも気軽にデータベースプログラミングを始めてもらえる。また、シンボルがマングリングされなくて済むので、他の言語のインターフェイスを書く際にも楽になるだろう。