Blog Me think, why waste time say lot word, when few word do trick

Fixing Kernel#singleton_method bug in Ruby.

Few days ago when I was browsing through StackOverflow I came across this question about weird behaviour of Kernel#singleton_method such as if you call Kernel#singleton_methods method on ActiveSupport::Deprecation class you would get you list of singleton methods (no surprise here) but when you try to get that singleton method with Kernel#singleton_method Ruby would throw NameError error:

ActiveSupport::Deprecation.singleton_methods(false) # => [:debug, :initialize, ...]
ActiveSupport::Deprecation.singleton_method(:debug) # => NameError (undefined singleton method `debug' for `ActiveSupport::Deprecation')

At first, I thought it’s ActiveSupport doing something fishy. And while I was looking through AS codebase this answer appeared. @michaelj discovered that it’s possible to reproduce this bug without ActiveSupport at all. All you needed to do is to prepend any module to a singleton class:

module Empty; end

class Foo
  singleton_class.prepend(Empty)

  def self.foo; end
end

Foo.singleton_methods(:false) # => [:foo]
Foo.singleton_method(:foo) # => NameError (undefined singleton method `foo' for `Foo')

So it was something wrong with Module#prepend.

I started looking for some bugs with Module#prepend on Ruby’s bugtracker. All I could find related was this bug with Object#methods and Module#prepend. So what I needed to do is to check how they fixed it and do something similar with Kernel#singleton_method.

DISCLAIMER: I’m not that good with Ruby internals so next part of the post might have some incorrect statements.

Main thing I learned from Object#methods fix was RCLASS_ORIGIN macro. This macro is used to get origin class of the passed class/module. And as I discovered Module#prepend makes a copy of a target class internally so if you need to access original one you can use that macro. Honestly, I don’t really understand why you can’t access singleton method but I got the idea.

That’s how rb_obj_singleton_method looked before the fix:

rb_obj_singleton_method(VALUE obj, VALUE vid)
{
  ...
  if (!id) {
    if (!NIL_P(klass = rb_singleton_class_get(obj))
  ...

as you can see the klass was retrieved by rb_singleton_class_get function which returns class’s copy if it was already prepended with some module. That means all I had to do is to apply RCLASS_ORIGIN on that class. And it did the trick. You can find the whole patch in this issue.

And may Ruby be with you.