rb_funcallの先って

Rubyでオブジェクトの等値性あたりがどうなっているのか見てみた。Objectクラスの初期化時のメソッド登録は、

2496     rb_define_method(rb_cBasicObject, "==", rb_obj_equal, 1);
2497     rb_define_method(rb_cBasicObject, "equal?", rb_obj_equal, 1);
2498     rb_define_method(rb_cBasicObject, "!", rb_obj_not, 0);
2499     rb_define_method(rb_cBasicObject, "!=", rb_obj_not_equal, 1);
 :
 :
2514     rb_define_method(rb_mKernel, "nil?", rb_false, 0);
2515     rb_define_method(rb_mKernel, "===", rb_equal, 1); 
2516     rb_define_method(rb_mKernel, "=~", rb_obj_match, 1);
2517     rb_define_method(rb_mKernel, "!~", rb_obj_not_match, 1);
2518     rb_define_method(rb_mKernel, "eql?", rb_obj_equal, 1);
(object.c)

となっている。あれっ。「rb_obj_equal」は

  91 VALUE
  92 rb_obj_equal(VALUE obj1, VALUE obj2)
  93 {
  94     if (obj1 == obj2) return Qtrue;
  95     return Qfalse;
  96 }
(object.c)

となっていて、これは参照が同じかどうかを判定している。あれ、それじゃあ、

>> a = "hi"
=> "hi"
>> b = "hi"
=> "hi"
>> a == b
=> true
>> a.eql?(b)
=> true
>> a.equal?(b)
=> false
>> a.object_id
=> 69040500
>> b.object_id
=> 68879100

の説明がつかないぞと思ったけど、==演算子に関しては、各クラスで再定義しているのだった。string.cでは以下。

2117 VALUE
2118 rb_str_equal(VALUE str1, VALUE str2)
2119 {
2120     int len;
2121 
2122     if (str1 == str2) return Qtrue;
2123     if (TYPE(str2) != T_STRING) {
2124         if (!rb_respond_to(str2, rb_intern("to_str"))) {
2125             return Qfalse;
2126         }
2127         return rb_equal(str2, str1);
2128     }
2129     if (!rb_str_comparable(str1, str2)) return Qfalse;
2130     if (RSTRING_LEN(str1) == (len = RSTRING_LEN(str2)) &&
2131         memcmp(RSTRING_PTR(str1), RSTRING_PTR(str2), len) == 0) {
2132         return Qtrue;
2133     }
2134     return Qfalse;
2135 }
2136 
 :
 :
7062 void
7063 Init_String(void)
7064 {
7065 #undef rb_intern
7066 #define rb_intern(str) rb_intern_const(str)
7067 
7068     rb_cString  = rb_define_class("String", rb_cObject);
7069     rb_include_module(rb_cString, rb_mComparable);
7070     rb_define_alloc_func(rb_cString, str_alloc);
7071     rb_define_singleton_method(rb_cString, "try_convert", rb_str_s_try_convert, 1);
7072     rb_define_method(rb_cString, "initialize", rb_str_init, -1);
7073     rb_define_method(rb_cString, "initialize_copy", rb_str_replace, 1);
7074     rb_define_method(rb_cString, "<=>", rb_str_cmp_m, 1);
7075     rb_define_method(rb_cString, "==", rb_str_equal, 1);

(string.c)

実は===演算子の挙動がよく分からない。これが一番ルーズな比較で、whenと一緒に使うことでis_a?の真偽と一致するような処理をするというのは分かるけど、何だかモヤモヤしている。これもやっぱりクラス階層の中で、よしなに定義されているということで、その挙動は各クラス依存。つまり覚えるしかないってことだ。あるいはRuby的なカンを働かせるということ。

例えば、range.cには、

 725 static VALUE
 726 range_eqq(VALUE range, VALUE val)
 727 {
 728     return rb_funcall(range, rb_intern("include?"), 1, val);
 729 }
  :
  :
 934     rb_define_method(rb_cRange, "===", range_eqq, 1);

とかあって、range_eqqで定義されてる。これは間接的にinclude?を呼び出していて、

 745 static VALUE
 746 range_include(VALUE range, VALUE val)
 747 {
 748     VALUE beg = RANGE_BEG(range);
 749     VALUE end = RANGE_END(range);
 750     int nv = FIXNUM_P(beg) || FIXNUM_P(end) ||
 751              rb_obj_is_kind_of(beg, rb_cNumeric) ||
 752              rb_obj_is_kind_of(end, rb_cNumeric);
 753 
 754     if (nv ||
 755         !NIL_P(rb_check_to_integer(beg, "to_int")) ||
 756         !NIL_P(rb_check_to_integer(end, "to_int"))) {
 757         if (r_le(beg, val)) {
 758             if (EXCL(range)) {
 759                 if (r_lt(val, end))
 760                     return Qtrue;
 761             }
 762             else {
 763                 if (r_le(val, end))
 764                     return Qtrue;
 765             }
 766         }
 767         return Qfalse;
 768     }
 769     else if (TYPE(beg) == T_STRING && TYPE(end) == T_STRING &&
 770              RSTRING_LEN(beg) == 1 && RSTRING_LEN(end) == 1) {
 771         if (NIL_P(val)) return Qfalse;
 772         if (TYPE(val) == T_STRING) {
 773             if (RSTRING_LEN(val) == 0 || RSTRING_LEN(val) > 1)
 774                 return Qfalse;
 775             else {
 776                 char b = RSTRING_PTR(beg)[0];
 777                 char e = RSTRING_PTR(end)[0];
 778                 char v = RSTRING_PTR(val)[0];
 779 
 780                 if (ISASCII(b) && ISASCII(e) && ISASCII(v)) {
 781                     if (b <= v && v < e) return Qtrue;
 782                     if (!EXCL(range) && v == e) return Qtrue;
 783                     return Qfalse;
 784                 }
 785             }
 786         }
 787     }
 788     /* TODO: ruby_frame->this_func = rb_intern("include?"); */
 789     return rb_call_super(1, &val);
 790 }
 791 

という自明でない処理をやってたりする。

色々見てたら、メソッドのinvokeは主に、

rb_funcall(obj, id, argc, ...)

というAPIで行っているらしいことがうっすら分かった。objがレシーバで、id はメソッドのid。引数の数をargcで渡して、後は実際のメソッドの引数をずらずら。メソッドのシンボル(文字列?)からIDへの対応付けは、rb_internがやってるっぽいのかな。

VALUE tmp = rb_funcall(str2, rb_intern("<=>"), 1, str1);

rb_internは、parse.cあたりに記述がある。シンボルとIDの相互変換は、いくつかのAPIに分かれてるようで、C文字列を渡せるrb_internやRuby文字列オブジェクトを渡せるrb_intern_strもある。ただ、実際に名前をルックアップするのは、rb_intern3で、関数の宣言部は、

14707 ID
14708 rb_intern3(const char *name, long len, rb_encoding *enc)

となっている。当たり前だけど、最終的にはC文字列と、その長さ、エンコーディング情報が揃ってはじめて処理ができる。このrb_internのように、引数の種類や数によって多段式に、rb_hoge0、rb_hoge1、rb_hoge2、rb_hoge3のように関数を整理するのは常套手段なのか、結構あちこちにあるような気がする。エントリポイントを多く用意しつつ、処理を分けることができていいってことかしら。賢い。

で、rb_intern3は、st_lookup関数でシンボルのハッシュを引いている。st.cはRuby全体で利用しているハッシュ実装で、確か3種類の目的で使われているとRHGで読んだ。

14707 ID
14708 rb_intern3(const char *name, long len, rb_encoding *enc)
14709 {
14710     const char *m = name;
14711     const char *e = m + len;
14712     unsigned char c;
14713     VALUE str;
14714     ID id;
14715     int last;
14716     int mb;
14717     struct RString fake_str;
14718     fake_str.basic.flags = T_STRING|RSTRING_NOEMBED|FL_FREEZE;
14719     fake_str.basic.klass = rb_cString;
14720     fake_str.as.heap.len = len;
14721     fake_str.as.heap.ptr = (char *)name;
14722     fake_str.as.heap.aux.capa = len;
14723     str = (VALUE)&fake_str;
14724     rb_enc_associate(str, enc);
14725 
14726     if (st_lookup(global_symbols.sym_id, str, (st_data_t *)&id))
14727         return id;
14728 
14729     if (rb_cString && !rb_enc_asciicompat(enc)) {
14730         id = ID_JUNK;
14731         goto new_id;
14732     }
14733     last = len-1;
14734     id = 0;
14735     switch (*m) {
14736       case '$':
14737         id |= ID_GLOBAL;
14738         if ((mb = is_special_global_name(++m, e, enc)) != 0) {
14739             if (!--mb) enc = rb_ascii8bit_encoding();
14740             goto new_id;
14741         }
14742         break;
14743       case '@':
14744         if (m[1] == '@') {
14745             m++;
14746             id |= ID_CLASS;
14747         }
14748         else {
14749             id |= ID_INSTANCE;
14750         }
14751         m++;
14752         break;
14753       default:
14754         c = m[0];
14755         if (c != '_' && rb_enc_isascii(c, enc) && rb_enc_ispunct(c, enc)) {
14756             /* operators */
14757             int i;
14758 
14759             if (len == 1) {
14760                 id = c;
14761                 goto id_register;
14762             }
14763             for (i = 0; i < op_tbl_count; i++) {
14764                 if (*op_tbl[i].name == *m &&
14765                     strcmp(op_tbl[i].name, m) == 0) {
14766                     id = op_tbl[i].token;
14767                     goto id_register;
14768                 }
14769             }
14770         }
14771 
14772         if (m[last] == '=') {
14773             /* attribute assignment */
14774             id = rb_intern3(name, last, enc);
14775             if (id > tLAST_TOKEN && !is_attrset_id(id)) {
14776                 enc = rb_enc_get(rb_id2str(id));
14777                 id = rb_id_attrset(id);
14778                 goto id_register;
14779             }
14780             id = ID_ATTRSET;
14781         }
14782         else if (rb_enc_isupper(m[0], enc)) {
14783             id = ID_CONST;
14784         }
14785         else {
14786             id = ID_LOCAL;
14787         }
14788         break;
14789     }
14790     mb = 0;
14791     if (!rb_enc_isdigit(*m, enc)) {
14792         while (m <= name + last && is_identchar(m, e, enc)) {
14793             if (ISASCII(*m)) {
14794                 m++;
14795             }
14796             else {
14797                 mb = 1;
14798                 m += rb_enc_mbclen(m, e, enc);
14799             }
14800         }
14801     }
14802     if (m - name < len) id = ID_JUNK;
14803     if (enc != rb_usascii_encoding()) {
14804         /*
14805          * this clause makes sense only when called from other than
14806          * rb_intern_str() taking care of code-range.
14807          */
14808         if (!mb) {
14809             for (; m <= name + len; ++m) {
14810                 if (!ISASCII(*m)) goto mbstr;
14811             }
14812             enc = rb_usascii_encoding();
14813         }
14814       mbstr:;
14815     }
14816   new_id:
14817     id |= ++global_symbols.last_id << ID_SCOPE_SHIFT;
14818   id_register:
14819     return register_symid(id, name, len, enc);
14820 }

あれ? なんでいきなりグローバルなテーブルを引いてるんだ? メソッドのルックアップって、下から行くんじゃないのかしら。

ともあれ、実際のメソッドの実行は、vm_eval.cの、rb_call0らしい。と思って、眺めてみると、そろそろNodeという構造体が出てきたりthread生成したりで、処理系の第3の胃袋あたりに近づくのかなと思ったりして、眺め続けている。