Skip to content

Commit ebb96fa

Browse files
XrXrparacycle
andcommitted
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 <[email protected]>
1 parent 084e7e3 commit ebb96fa

File tree

2 files changed

+69
-9
lines changed

2 files changed

+69
-9
lines changed

class.c

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040

4141
#define id_attached id__attached__
4242

43+
#define METACLASS_OF(k) RBASIC(k)->klass
44+
#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
45+
4346
void
4447
rb_class_subclass_add(VALUE super, VALUE klass)
4548
{
@@ -457,22 +460,35 @@ rb_singleton_class_clone(VALUE obj)
457460
return rb_singleton_class_clone_and_attach(obj, Qundef);
458461
}
459462

463+
// Clone and return the singleton class of `obj` if it has been created and is attached to `obj`.
460464
VALUE
461465
rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
462466
{
463467
const VALUE klass = RBASIC(obj)->klass;
464468

465-
if (!FL_TEST(klass, FL_SINGLETON))
466-
return klass;
469+
// Note that `rb_singleton_class()` can create situations where `klass` is
470+
// attached to an object other than `obj`. In which case `obj` does not have
471+
// a material singleton class attached yet and there is no singleton class
472+
// to clone.
473+
if (!(FL_TEST(klass, FL_SINGLETON) && rb_attr_get(klass, id_attached) == obj)) {
474+
// nothing to clone
475+
return klass;
476+
}
467477
else {
468478
/* copy singleton(unnamed) class */
479+
bool klass_of_clone_is_new;
469480
VALUE clone = class_alloc(RBASIC(klass)->flags, 0);
470481

471482
if (BUILTIN_TYPE(obj) == T_CLASS) {
483+
klass_of_clone_is_new = true;
472484
RBASIC_SET_CLASS(clone, clone);
473485
}
474486
else {
475-
RBASIC_SET_CLASS(clone, rb_singleton_class_clone(klass));
487+
VALUE klass_metaclass_clone = rb_singleton_class_clone(klass);
488+
// When `METACLASS_OF(klass) == klass_metaclass_clone`, it means the
489+
// recursive call did not clone `METACLASS_OF(klass)`.
490+
klass_of_clone_is_new = (METACLASS_OF(klass) != klass_metaclass_clone);
491+
RBASIC_SET_CLASS(clone, klass_metaclass_clone);
476492
}
477493

478494
RCLASS_SET_SUPER(clone, RCLASS_SUPER(klass));
@@ -496,7 +512,9 @@ rb_singleton_class_clone_and_attach(VALUE obj, VALUE attach)
496512
arg.new_klass = clone;
497513
rb_id_table_foreach(RCLASS_M_TBL(klass), clone_method_i, &arg);
498514
}
499-
rb_singleton_class_attached(RBASIC(clone)->klass, clone);
515+
if (klass_of_clone_is_new) {
516+
rb_singleton_class_attached(RBASIC(clone)->klass, clone);
517+
}
500518
FL_SET(clone, FL_SINGLETON);
501519

502520
return clone;
@@ -518,11 +536,6 @@ rb_singleton_class_attached(VALUE klass, VALUE obj)
518536
}
519537
}
520538

521-
522-
523-
#define METACLASS_OF(k) RBASIC(k)->klass
524-
#define SET_METACLASS_OF(k, cls) RBASIC_SET_CLASS(k, cls)
525-
526539
/*!
527540
* whether k is a meta^(n)-class of Class class
528541
* @retval 1 if \a k is a meta^(n)-class of Class class (n >= 0)

test/ruby/test_class.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,53 @@ def foo; :foo; end
483483
assert_equal(:foo, d.foo)
484484
end
485485

486+
def test_clone_singleton_class_exists
487+
klass = Class.new do
488+
def self.bar; :bar; end
489+
end
490+
491+
o = klass.new
492+
o.singleton_class
493+
clone = o.clone
494+
495+
assert_empty(o.singleton_class.instance_methods(false))
496+
assert_empty(clone.singleton_class.instance_methods(false))
497+
assert_empty(o.singleton_class.singleton_class.instance_methods(false))
498+
assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
499+
end
500+
501+
def test_clone_when_singleton_class_of_singleton_class_exists
502+
klass = Class.new do
503+
def self.bar; :bar; end
504+
end
505+
506+
o = klass.new
507+
o.singleton_class.singleton_class
508+
clone = o.clone
509+
510+
assert_empty(o.singleton_class.instance_methods(false))
511+
assert_empty(clone.singleton_class.instance_methods(false))
512+
assert_empty(o.singleton_class.singleton_class.instance_methods(false))
513+
assert_empty(clone.singleton_class.singleton_class.instance_methods(false))
514+
end
515+
516+
def test_clone_when_method_exists_on_singleton_class_of_singleton_class
517+
klass = Class.new do
518+
def self.bar; :bar; end
519+
end
520+
521+
o = klass.new
522+
o.singleton_class.singleton_class.define_method(:s2_method) { :s2 }
523+
clone = o.clone
524+
525+
assert_empty(o.singleton_class.instance_methods(false))
526+
assert_empty(clone.singleton_class.instance_methods(false))
527+
assert_equal(:s2, o.singleton_class.s2_method)
528+
assert_equal(:s2, clone.singleton_class.s2_method)
529+
assert_equal([:s2_method], o.singleton_class.singleton_class.instance_methods(false))
530+
assert_equal([:s2_method], clone.singleton_class.singleton_class.instance_methods(false))
531+
end
532+
486533
def test_singleton_class_p
487534
feature7609 = '[ruby-core:51087] [Feature #7609]'
488535
assert_predicate(self.singleton_class, :singleton_class?, feature7609)

0 commit comments

Comments
 (0)