mruby-redis で Redis の HMGET と HMSET を使いたかったのでパッチを送った。
mruby も Hiredis もよくわからない状態からの出発だったので、小さいパッチの割に時間がかかってしまった。Hiredis は README.md に書かれている API の説明を読み、mruby はソースコードのコメントアウトに書かれた仕様を読み漁った。
内容の正しさは十分に保証出来ないが、調べたことを備忘録として書いておく。なお、mruby はバージョン 1.2.0 のコードを読んだ。
HMGET の実装
まず、HMGET のコードは以下の通りである。HMSET も似たような実装なので、そちらは割愛する。
static mrb_value mrb_redis_hmget(mrb_state *mrb, mrb_value self)
{
mrb_value *mrb_argv;
mrb_int argc = 0;
mrb_get_args(mrb, "*", &mrb_argv, &argc);
argc++;
const char *argv[argc];
size_t argvlen[argc];
argv[0] = "HMGET";
argvlen[0] = sizeof("HMGET") - 1;
int ai = mrb_gc_arena_save(mrb);
for (mrb_int argc_current = 1; argc_current < argc; argc_current++) {
mrb_value curr = mrb_str_to_str(mrb, mrb_argv[argc_current - 1]);
argv[argc_current] = RSTRING_PTR(curr);
argvlen[argc_current] = RSTRING_LEN(curr);
mrb_gc_arena_restore(mrb, ai);
}
mrb_value array = mrb_nil_value();
redisContext *rc = DATA_PTR(self);
redisReply *rr;
rr = redisCommandArgv(rc, argc, argv, argvlen);
if (rr->type == REDIS_REPLY_ARRAY) {
if (rr->elements > 0) {
array = mrb_ary_new(mrb);
for (int i = 0; i < rr->elements; i++) {
if (rr->element[i]->len > 0) {
mrb_ary_push(mrb, array, mrb_str_new(mrb, rr->element[i]->str, rr->element[i]->len));
} else {
mrb_ary_push(mrb, array, mrb_nil_value());
}
}
}
} else {
mrb_raise(mrb, E_ARGUMENT_ERROR, rr->str);
}
freeReplyObject(rr);
return array;
}
...
mrb_define_method(mrb, redis, "hmget", mrb_redis_hmset, (MRB_ARGS_REQ(2) | MRB_ARGS_REST()));
最終行の mrb_define_method() で Redis#redis メソッドを定義しており、ここが今回の起点となる。
mrb_define_method()
mrb_define_method() はメソッドを定義する mruby API である。第2引数がクラス名、第3引数がメソッド名、第4引数がメソッドの呼び出す関数、第5引数がメソッドの引数情報である。引数情報には、引数の数やキーワード、ブロックなどを指定する。
// https://github.com/mruby/mruby/blob/1.2.0/include/mruby.h#L260
MRB_API void mrb_define_method(mrb_state *mrb, struct RClass *cla, const char *name, mrb_func_t func, mrb_aspec aspec);
ソースコードのコメントアウトに “Defines a global function in ruby.” とある通り、第2引数に mrb->kernel_module を指定し、Kernelモジュールにグローバル関数を定義することもできる。
今回の実装では、redis
クラスの "hmget"
メソッドが mrb_redis_hmget
関数を呼び出している。また、HMGET key field [field …] というコマンドなので、引数情報は (MRB_ARGS_REQ(2) | MRB_ARGS_REST())
で「2つまたはそれ以上」としたつもり。
引数に関するマクロは include/mruby.h#L682-L730 に定義がある。今回使った MRB_ARGS_REQ(n) と MRB_ARGS_REST() は以下のように定義されている。
// https://github.com/mruby/mruby/blob/1.2.0/include/mruby.h#L688
#define MRB_ARGS_REQ(n) ((mrb_aspec)((n)&0x1f) << 18)
// https://github.com/mruby/mruby/blob/1.2.0/include/mruby.h#L709
#define MRB_ARGS_REST() ((mrb_aspec)(1 << 12))
これで Redis#hmget というメソッドが定義された。次はメソッドの内部実装である mrb_redis_hmget() を見ていく。
mrb_redis_hmget()
mrb_redis_hmget() は Redis に HMGET key field [field …] というコマンドを送信し、リプライを Ruby の Array として返す処理を行っている。
はじめに、引数とその数を取得するところから始まる。
引数の取得
mrb_get_args() は引数を取得する mruby API である。mrb_argv に引数が、argc には引数の数がそれぞれ格納される。HMGET は任意のフィールド数を指定できるため、mrb_args_format は引数を配列で受け取る *
にした(が、今思えば a
が良かったかも)。フォーマットの詳細は mruby/mruby.h#L732-L758 にある。
static mrb_value mrb_redis_hmget(mrb_state *mrb, mrb_value self)
{
mrb_value *mrb_argv;
mrb_int argc = 0;
mrb_get_args(mrb, "*", &mrb_argv, &argc);
Redis へ送るコマンドの組立
続いて、引数を元に、Redis に送信するコマンドを組み立てていく。
以下のコードでは、配列 argv の各要素に引数のアドレスを、配列 argvlen の各要素に引数の長さをそれぞれ格納している。 argv の最初の要素は "HMGET"
で、 argv[1] から mrb_argv の中身を格納していくため、 argc を1つインクリメントして数を合わせている。
argc++;
const char *argv[argc];
size_t argvlen[argc];
argv[0] = "HMGET";
argvlen[0] = sizeof("HMGET") - 1;
int ai = mrb_gc_arena_save(mrb);
for (mrb_int argc_current = 1; argc_current < argc; argc_current++) {
mrb_value curr = mrb_str_to_str(mrb, mrb_argv[argc_current - 1]);
argv[argc_current] = RSTRING_PTR(curr);
argvlen[argc_current] = RSTRING_LEN(curr);
mrb_gc_arena_restore(mrb, ai);
}
例えば、 client.hmget "myhash", "field1", "field2"
のようにコマンドを組み立てた場合、argv は以下のようなイメージになるはず。
また、ループ内の mrb_value curr は一時的なオブジェクトなので、mrb_gc_arena_save() と mrb_gc_arena_restore() で GC arena 領域の消費を抑えている。正しく理解できているか分からないので、詳しくは Matz の日記を読んでもらいたい。
Hiredis を使った Redis との通信
コマンド argv, argc の準備ができたら、次は Hiredis を用いて Redis に送信し、リプライを得る。
以下のコードでは、redisCommandArgv() を用いてコマンドを Redis に送信し、得られたリプライを mrb_value array に格納している。ただし、引数に問題がある場合は ArgumentError 例外が発生する(この例外が荒っぽいので直したい)。
mrb_value array = mrb_nil_value();
redisContext *rc = DATA_PTR(self);
redisReply *rr;
rr = redisCommandArgv(rc, argc, argv, argvlen);
if (rr->type == REDIS_REPLY_ARRAY) {
if (rr->elements > 0) {
array = mrb_ary_new(mrb);
for (int i = 0; i < rr->elements; i++) {
if (rr->element[i]->len > 0) {
mrb_ary_push(mrb, array, mrb_str_new(mrb, rr->element[i]->str, rr->element[i]->len));
} else {
mrb_ary_push(mrb, array, mrb_nil_value());
}
}
}
} else {
mrb_raise(mrb, E_ARGUMENT_ERROR, rr->str);
}
redisContext は Redis との接続状態を保持する構造体である。DATA_PTR は include/mruby/data.h で定義されるマクロである(詳細な解説は「mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法」にあるので、詳しくは記事を読んでもらいたい。)mruby-redis の DATA_PTR(self) には、Redis#new (mrb_redis_connect()) で redisContext が格納されている。
redisReply はコマンドに対して返されたリプライを格納する構造体である。HMGET の場合はマルチバルクリプライ (REDIS_REPLY_ARRAY) が返るため、rr->elements
にリプライの要素数が格納され、rr->element[..index..]
から各要素の値にアクセスできる。
// https://github.com/redis/hiredis/blob/v0.13.3/hiredis.h#L112-L119
typedef struct redisReply {
int type; /* REDIS_REPLY_* */
long long integer; /* The integer when type is REDIS_REPLY_INTEGER */
int len; /* Length of string */
char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */
size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */
struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */
} redisReply;
さて、Hiredis のコマンド送信 API は数種あるが、今回は redisCommandArgv() を用いた。得られた結果は redisReply 型の rr
に格納している。
// https://github.com/redis/hiredis/blob/v0.13.3/hiredis.h#L217
void *redisCommandArgv(redisContext *c, int argc, const char **argv, const size_t *argvlen);
リプライの種類 rr->type
がマルチバルクリプライ (REDIS_REPLY_ARRAY) で、要素数が 0 より大きい場合、リプライの各要素を mrb_ary_push() で array にプッシュしていく。要素が nil の場合もあるので、要素の長さが 0 の場合は mrb_nil_value() をプッシュしている。
// https://github.com/mruby/mruby/blob/1.2.0/include/mruby/array.h#L77
MRB_API void mrb_ary_push(mrb_state *mrb, mrb_value array, mrb_value value);
Ruby の Array オブジェクトを返す
最後は、freeReplyObject() で不要になった redisReply オブジェクトを解放し、array を返す。
freeReplyObject(rr);
return array;
終わりに
まだまだ理解出来ていないところは多いが、mruby や Hiredis を駆使したパッチを書いてみることによって、理解が深まった気がする。とはいえ、C言語の知識不足は明らかなので、引き続き勉強していきたい。