[前][次][番号順一覧][スレッド一覧]

ruby-changes:63619

From: Alan <ko1@a...>
Date: Tue, 17 Nov 2020 07:41:33 +0900 (JST)
Subject: [ruby-changes:63619] ebb96fa880 (master): Fix singleton class cloning

https://git.ruby-lang.org/ruby.git/commit/?id=ebb96fa880

From ebb96fa8808317ad53a4977bff26cf755d68077e Mon Sep 17 00:00:00 2001
From: Alan Wu <XrXr@u...>
Date: Wed, 11 Nov 2020 16:38:03 -0500
Subject: Fix singleton class cloning

Before this commit, `clone` gave different results depending on whether the original object
had an attached singleton class or not.

Consider the following setup:
```
class Foo; end
Foo.singleton_class.define_method(:foo) {}

obj = Foo.new

obj.singleton_class if $call_singleton

clone = obj.clone
```

When `$call_singleton = false`, neither `obj.singleton_class.singleton_class` nor
`clone.singleton_class.singleton_class` own any methods.

However, when `$call_singleton = true`, `clone.singleton_class.singleton_class` would own a copy of
`foo` from `Foo.singleton_class`, even though `obj.singleton_class.singleton_class` does not.

The latter case is unexpected and results in a visibly different clone, depending on if the original object
had an attached class or not.

Co-authored-by: Ufuk Kayserilioglu <ufuk.kayserilioglu@s...>

diff --git a/class.c b/class.c
index 48450a5..709d49c 100644
--- a/class.c
+++ b/class.c
@@ -40,6 +40,9 @@ https://github.com/ruby/ruby/blob/trunk/class.c#L40
 
 #define id_attached id__attached__
 
+#define METACLASS_OF(k) RBASIC(k)->klass
+#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
+
 void
 rb_class_subclass_add(VALUE super, VALUE klass)
 {
@@ -457,22 +460,35 @@ rb_singleton_class_clone(VALUE obj) https://github.com/ruby/ruby/blob/trunk/class.c#L460
     return rb_singleton_class_clone_and_attach(obj, Qundef);
 }
 
+// Clone and return the singleton class of `obj` if it has been created and is attached to `obj`.
 VALUE
 rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
 {
     const VALUE klass = RBASIC(obj)->klass;
 
-    if (!FL_TEST(klass, FL_SINGLETON))
-	return klass;
+    // Note that `rb_singleton_class()` can create situations where `klass` is
+    // attached to an object other than `obj`. In which case `obj` does not have
+    // a material singleton class attached yet and there is no singleton class
+    // to clone.
+    if (!(FL_TEST(klass, FL_SINGLETON) && rb_attr_get(klass, id_attached) == obj)) {
+        // nothing to clone
+        return klass;
+    }
     else {
 	/* copy singleton(unnamed) class */
+        bool klass_of_clone_is_new;
 	VALUE clone = class_alloc(RBASIC(klass)->flags, 0);
 
 	if (BUILTIN_TYPE(obj) == T_CLASS) {
+            klass_of_clone_is_new = true;
 	    RBASIC_SET_CLASS(clone, clone);
 	}
 	else {
-	    RBASIC_SET_CLASS(clone, rb_singleton_class_clone(klass));
+            VALUE klass_metaclass_clone = rb_singleton_class_clone(klass);
+            // When `METACLASS_OF(klass) == klass_metaclass_clone`, it means the
+            // recursive call did not clone `METACLASS_OF(klass)`.
+            klass_of_clone_is_new = (METACLASS_OF(klass) != klass_metaclass_clone);
+            RBASIC_SET_CLASS(clone, klass_metaclass_clone);
 	}
 
 	RCLASS_SET_SUPER(clone, RCLASS_SUPER(klass));
@@ -496,7 +512,9 @@ rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach) https://github.com/ruby/ruby/blob/trunk/class.c#L512
 	    arg.new_klass = clone;
 	    rb_id_table_foreach(RCLASS_M_TBL(klass), clone_method_i, &arg);
 	}
-	rb_singleton_class_attached(RBASIC(clone)->klass, clone);
+        if (klass_of_clone_is_new) {
+            rb_singleton_class_attached(RBASIC(clone)->klass, clone);
+        }
 	FL_SET(clone, FL_SINGLETON);
 
 	return clone;
@@ -518,11 +536,6 @@ rb_singleton_class_attached(VALUE klass, VALUE obj) https://github.com/ruby/ruby/blob/trunk/class.c#L536
     }
 }
 
-
-
-#define METACLASS_OF(k) RBASIC(k)->klass
-#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
-
 /*!
  * whether k is a meta^(n)-class of Class class
  * @retval 1 if \a k is a meta^(n)-class of Class class (n >= 0)
diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb
index 82a2e55..6a8234a 100644
--- a/test/ruby/test_class.rb
+++ b/test/ruby/test_class.rb
@@ -483,6 +483,53 @@ class TestClass < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_class.rb#L483
     assert_equal(:foo, d.foo)
   end
 
+  def test_clone_singleton_class_exists
+    klass = Class.new do
+      def self.bar; :bar; end
+    end
+
+    o = klass.new
+    o.singleton_class
+    clone = o.clone
+
+    assert_empty(o.singleton_class.instance_methods(false))
+    assert_empty(clone.singleton_class.instance_methods(false))
+    assert_empty(o.singleton_class.singleton_class.instance_methods(false))
+    assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
+  end
+
+  def test_clone_when_singleton_class_of_singleton_class_exists
+    klass = Class.new do
+      def self.bar; :bar; end
+    end
+
+    o = klass.new
+    o.singleton_class.singleton_class
+    clone = o.clone
+
+    assert_empty(o.singleton_class.instance_methods(false))
+    assert_empty(clone.singleton_class.instance_methods(false))
+    assert_empty(o.singleton_class.singleton_class.instance_methods(false))
+    assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
+  end
+
+  def test_clone_when_method_exists_on_singleton_class_of_singleton_class
+    klass = Class.new do
+      def self.bar; :bar; end
+    end
+
+    o = klass.new
+    o.singleton_class.singleton_class.define_method(:s2_method) { :s2 }
+    clone = o.clone
+
+    assert_empty(o.singleton_class.instance_methods(false))
+    assert_empty(clone.singleton_class.instance_methods(false))
+    assert_equal(:s2, o.singleton_class.s2_method)
+    assert_equal(:s2, clone.singleton_class.s2_method)
+    assert_equal([:s2_method], o.singleton_class.singleton_class.instance_methods(false))
+    assert_equal([:s2_method], clone.singleton_class.singleton_class.instance_methods(false))
+  end
+
   def test_singleton_class_p
     feature7609 = '[ruby-core:51087] [Feature #7609]'
     assert_predicate(self.singleton_class, :singleton_class?, feature7609)
-- 
cgit v0.10.2


--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]