芝と申します。
クラスオブジェクトを clone したときの挙動について質問させてください。

# サンプルコード
String.class_eval do
  def self.singleton_method_added(mid)
    puts("singleton_method_added: self = #{self}, mid = #{mid}")
  end
end

puts("start clone")
StrClone = String.clone
puts("finish clone")

# サンプルコードの実行結果
singleton_method_added: self = String, mid = singleton_method_added
start clone
singleton_method_added: self = String, mid = try_convert
singleton_method_added: self = String, mid = singleton_method_added
finish clone



上記のサンプルコードを実行すると、String クラスを clone し、メソッドテー
ブルを StrClone にコピーしているときに、String クラスに対して
singleton_method_added が呼び出され、実行結果のように出力されます。
質問なんですが、クラスオブジェクトの clone 時に clone 元のクラスオブジェ
クトの singleton_method_added が呼び出されるのは仕様なのでしょうか。

上記のサンプルコードの場合、StrClone の singleton_method_added が呼び出
されるか、もしくは singleton_method_added が呼び出されないことを期待して
singleton_method_added を使用していました。

上記のサンプルコードで String クラスの singleton_method_added が呼び出さ
れる原因は、rb_singleton_class_clone(class.c) にあります。
rb_singleton_class_clone では、以下のように特異クラスのインスタンス変数
をコピーするのですが、このとき、"__attached__" というインスタンス変数を
コピーしています。



VALUE
rb_singleton_class_clone(VALUE obj)
{
        ...
        if (RCLASS_IV_TBL(klass)) {
            /* インスタンス変数のコピー */
            RCLASS_IV_TBL(clone) = st_copy(RCLASS_IV_TBL(klass));
        }
        ...
        RCLASS_M_TBL(clone) = st_init_numtable();
        data.tbl = RCLASS_M_TBL(clone);
        data.klass = (VALUE)clone;
        /* メソッドテーブルのコピー、method_added の呼び出し */
        st_foreach(RCLASS_M_TBL(klass), clone_method,
                   (st_data_t)&data);
        ...
}



今回の例の場合、この、"__attached__" は、クラスのコピー元である String
クラスを指しています。singleton_method_added を呼び出す CALL_METHOD_HOOK
では、以下のように、レシーバを特異クラスの "__attached__" によって決定す
るので、メソッドテーブルをコピーするときに呼び出す
singleton_method_added のレシーバが、String になっています。



#define CALL_METHOD_HOOK(klass, hook, mid) do {         \
        const VALUE arg = ID2SYM(mid);                  \
        VALUE recv_class = (klass);                     \
        ID hook_id = (hook);                            \
        if (FL_TEST((klass), FL_SINGLETON)) {           \
            recv_class = rb_ivar_get((klass), attached);\
            hook_id = singleton_##hook;                 \
        }                                               \
        rb_funcall2(recv_class, hook_id, 1, &arg);      \
    } while (0)



このようになるのが仕様として定まっているのかどうかが分からないのですが、
singleton_method_added のレシーバをクローン先のクラスに変更するようにし
たパッチを作成したので、本メールの末尾に張っておきます。
rb_obj_clone での
RBASIC(clone)->klass = rb_singleton_class_clone_and_attach(obj, clone);
の位置をずらしていいのかどうかが不安なんですが、参考にしていただければ幸
いです。

以上、よろしくお願いいたします。


# パッチ

Index: object.c
===================================================================
--- object.c    (revision 33202)
+++ object.c    (working copy)
@@ -268,6 +268,7 @@
  *  the class.
  */

+VALUE rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach);
 VALUE
 rb_obj_clone(VALUE obj)
 {
@@ -277,9 +278,9 @@
         rb_raise(rb_eTypeError, "can't clone %s", rb_obj_classname(obj));
     }
     clone = rb_obj_alloc(rb_obj_class(obj));
-    RBASIC(clone)->klass = rb_singleton_class_clone(obj);
     RBASIC(clone)->flags = (RBASIC(obj)->flags | FL_TEST(clone,
FL_TAINT) | FL_TEST(clone, FL_UNTRUSTED)) &
~(FL_FREEZE|FL_FINALIZE|FL_MARK);
     init_copy(clone, obj);
+    RBASIC(clone)->klass = rb_singleton_class_clone_and_attach(obj, clone);
     rb_funcall(clone, id_init_clone, 1, obj);
     RBASIC(clone)->flags |= RBASIC(obj)->flags & FL_FREEZE;

Index: class.c
===================================================================
--- class.c     (revision 33202)
+++ class.c     (working copy)
@@ -219,9 +219,17 @@
     return rb_mod_init_copy(clone, orig);
 }

+VALUE rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach);
+
 VALUE
 rb_singleton_class_clone(VALUE obj)
 {
+    return rb_singleton_class_clone_and_attach(obj, Qundef);
+}
+
+VALUE
+rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
+{
     VALUE klass = RBASIC(obj)->klass;

     if (!FL_TEST(klass, FL_SINGLETON))
@@ -246,6 +254,10 @@
            RCLASS_CONST_TBL(clone) = st_init_numtable();
            st_foreach(RCLASS_CONST_TBL(klass), clone_const_i,
(st_data_t)RCLASS_CONST_TBL(clone));
        }
+       if (attach != Qundef) {
+           rb_singleton_class_attached(clone, attach);
+       }
        RCLASS_M_TBL(clone) = st_init_numtable();
        data.tbl = RCLASS_M_TBL(clone);
        data.klass = (VALUE)clone;