CRuby の hash.c を一部読んだ

ツイッターで「Ruby の Hash のキーにするオブジェクトに hash と eql? を定義しないといけないのなんでなんだろう」という話題が出て、「おそらく Hash の名前の通り、Hash テーブルを lookup するのにキーの hash 値を計算するのに使ってるんだろうね」という話になったのだけれど、「じゃあhash.c 読んでみようぜ」って感じになって、一部読んだ。2.0.0-p0のコードです。

(一部markdownが注釈と解釈されておかしなことになってるの防ぐためにスペースを余分に入れてある)

まずは、Hash#[ ] の実装がどこにあるのか見る。

hash.c の 3448 行目見るとわかる。

rb_define_method(rb_cHash,"[ ]", rb_hash_aref, 1);

Hash#[ ] の実体は rb_hash_aref というCの関数である。じゃあそこを見る。hash.c の L560から。

VALUE
rb_hash_aref(VALUE hash, VALUE key)
{
    st_data_t val;

    if (!RHASH(hash)->ntbl || !st_lookup(RHASH(hash)->ntbl, key, &val)) {
        return hash_default_value(hash, key);
    }
    return (VALUE)val;
}

VALUEってのはRubyの世界のオブジェクトを表すっぽい。RHASHは、Rubyの世界のオブジェクトをCの世界の構造体に変換するマクロっぽい。じゃあCの世界の構造体がどこに定義されるかっていうと、include/ruby.h の L921

struct RHash {
    struct RBasic basic;
    struct st_table *ntbl;      /* possibly 0 */
    int iter_lev;
    VALUE ifnone;
};

たしかに ntbl っていうメンバーがある。

さっきの rb_hash_aref に話は戻って、引数にhash, keyがある。どちらも VALUE だし、hash は Ruby の世界の Hash オブジェクト、そして key はその key というのが推察できると思う。

if 文のところ。 hashの内部で保持してるテーブルが無いか、あるいはst_lookupの返り値が偽ならばデフォルトの値を返すっぽい。

st_lookupにvalというものをポインタ渡ししている。もし st_lookup でハッシュテーブルのエントリが見つかればそれを val に入れてるんだなってわかると思う。

で、見つかったばあいは valを(VALUE)にして(つまりRubyの世界のオブジェクトにして)返してる。

さて、ここでキモは st_lookup だとわかったので、st_lookup 見る。st.c L409。

int
st_lookup(st_table *table, register st_data_t key, st_data_t *value)
{
    st_index_t hash_val;
    register st_table_entry *ptr;

    hash_val = do_hash(key, table);
    ...

do_hashというところでハッシュ値を求めてるっぽいのが見れる。じゃあdo_hash見る。st.c の L87

#define do_hash(key,table) (st_index_t)(*(table)->type->hash)( ( key ) )

関数ポインタだ。 *(table)->type->hash で得られる関数をkeyを引数に呼び出している。

じゃあ table ってなんだろう。st_lookupの第一引数だ。st_lookupを呼び出してる部分みてみると、これは RHASH(hash)->ntbl であることがわかる。

RHASH(hash)で得られるものは RHash 構造体であった。この構造体に実際に何が入っているかは、初期化してるところを見れば良い。

rb_hash_initialize から rb_hash_modify に飛んで、さらに rb_hash_tbl (hash.c L266)に飛ぶ。ここで以下のようなコードで初期化している。

struct st_table *
rb_hash_tbl(VALUE hash)
{
  if (!RHASH(hash)->ntbl) {
    RHASH(hash)->ntbl = st_init_table(&objhash);
  }
  return RHASH(hash)->ntbl;
}

st_init_tableに objhash を渡している。

st_init_tableを見る(st.c L266)

st_table*
st_init_table(const struct st_hash_type *type)
{
    return st_init_table_with_size(type, 0);
}

st_init_table_with_size 呼んでるだけなので st_init_table_with_size(st.c L229)見る。

st_table*
st_init_table_with_size(const struct st_hash_type *type, st_index_t size)
{
    st_table *tbl;

    (略)

    tbl = st_alloc_table();
    tbl->type = type;
    (略)

    return tbl;
}

ここまで見ると、tbl->type に objhash が渡されているのが見て取れる。

ではobjhashを見る。hash.c L104

static const struct st_hash_type objhash = {
  rb_any_cmp,
  rb_any_hash,
};

st_hash_typeってのが出てきた。読む。(st.h L70)

struct st_hash_type {
    int ( *compare )(ANYARGS /*st_data_t, st_data_t*/); /* st_compare_func* */
    st_index_t ( *hash )(ANYARGS /*st_data_t*/);        /* st_hash_func* */
};

ANYARGSは C 環境だと空文字列に展開される。(include/ruby/defines.h)

ふむ。なるほど。まず、(table)->type が objhashだとわかって、 objhash->hash が rb_any_hash への関数ポインタであることがわかる。

これで、do_hash の 中で出てきた *(table)->type->hash の正体が rb_any_hashであることがわかった。あとひといきだ。

rb_any_hash は hash.rb L84

static st_index_t
rb_any_hash(VALUE a)
{
  VALUE hval;
  st_index_t hnum;

  if (SPECIAL_CONST_P(a)) {
    if (a == Qundef) return 0;
    hnum = rb_hash_end(rb_hash_start( (st_index_t)a) );
  }
  else if (BUILTIN_TYPE(a) == T_STRING) {
    hnum = rb_str_hash(a);
  }
  else {
    hval = rb_hash(a);
    hnum = FIX2LONG(hval);
  }
  hnum <<= 1;
  return (st_index_t)RSHIFT(hnum, 1);
}

組み込みでないような型の値 a に対しては、rb_hash(a) でハッシュ値を求めている。では rb_hash を見る。(hash.c L66)

VALUE
rb_hash(VALUE obj)
{
  VALUE hval = rb_funcall(obj, id_hash, 0);
 retry:
  switch (TYPE(hval)) {
  case T_FIXNUM:
    return hval;

  case T_BIGNUM:
    return LONG2FIX(((long*)(RBIGNUM_DIGITS(hval)))[0]);

  default:
    hval = rb_to_int(hval);
    goto retry;
  }
}

rb_funcall で引数に渡されたオブジェクトのhashメソッドを呼んでいる。じゃあここの引数にわたって来ているものはなんだ、って今度は上に辿ってみて行くと、 rb_any_hashに渡されている引数で、それはどこから来ているかというと、

#define do_hash(key,table) (st_index_t)(*(table)->type->hash)( (key) )

ここであった。引数は key である。

というわけで、無事に「なぜ Hash のキーに渡すためのオブジェクトに hash メソッドを定義しなければならないのか」が理解できた。eql? についてはまだ読んでないけど、同じ感じで突き止めていけばわかると思う。

追記 2013 3 29

eql? についても調べた。ここまでちゃんと読んだひとなら普通に読める

st_lookup で呼ばれている find_entry(st.c L381) の中で呼ばれている PTR_NOT_EQUAL の中で呼ばれている EQUAL の中で *(table)->type->compare されており、(table)->type->compare の実体は rb_any_cmp であり、そこで rb_eql が呼ばれている。rb_eql の定義は object.c のL67 、rb_funcall で id_eql がkeyに対して呼ばれている

id_eql = rb_intern("eql?");とされている(object.c L3171)ので、やはり ハッシュテーブルの lookup に eql? も使われていることがわかる。

ちなみにこの同値性をどう使っているのかというと、key.hashでハッシュ値を求め ハッシュテーブルの slot を確定させ、その slot の中からオブジェクトを取り出すときに、eql? で一致したオブジェクトを取り出すという実装になっている。