概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Fixing Kernel#singleton_method bug in Ruby. – hyperoslo – Medium
- 原文公開日: 2018/04/05
- 著者: Vasiliy Ermolovich
RubyのKernel#singleton_methodバグを修正した話(翻訳)
数日前Stackoverflowをざっと眺めていると、この質問でKernel#singleton_methodの奇矯な振る舞いを目にしました。たとえばActiveSupport::DeprecationクラスでKernel#singleton_methodsを呼ぶとシングルトンメソッドのリストが取れます(当たり前ですね)が、Kernel#singleton_methodで同じことをやってみるとRubyがNameErrorエラーを返すのです(Gist)。
ActiveSupport::Deprecation.singleton_methods(false) # => [:debug, :initialize, ...]
ActiveSupport::Deprecation.singleton_method(:debug) # => NameError (undefined singleton method `debug' for `ActiveSupport::Deprecation')
最初はActiveSupportがまた何かやらかしたのではないかと思いました。そしてActiveSupportのコードベースをあさっていると、このバグがActiveSupportがなくても再現できることをこちらの回答(@michaelj)で知りました。適当なモジュールでシングルトンクラスをpropendするだけで再現します。
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')
どうやらModule#prependでおかしなことが起きています。
まずはRubyバグトラッカーでModule#prependのバグを探してみましたが、このバグに関連するものといえば#8044のObject#methodsとModule#prependしか見当たりませんでした。そこで、そのバグがどのように修正されたかをチェックしてKernel#singleton_methodでも同じような修正をやってみる必要が生じました。
免責事項: 私はRubyの内部について詳しくないので、本記事の以下の記述に誤りが含まれている可能性があります。
Object#methodsについてわかったのは、99126a4の修正がRCLASS_ORIGINマクロであった点です。このマクロは、渡されたクラスやモジュールの元のクラスを取得するのに使われます。もうひとつ発見したのは、Module#prependは対象となるクラスを内部でコピーしていることです。つまり元のクラスにアクセスする必要がある場合はこのマクロが使えるということです。シングルトンメソッドにアクセスできていない理由はどうにもわかりませんが、ここからヒントを得られました。
修正前のrb_obj_singleton_methodは次のようになっていました(Gist)。
rb_obj_singleton_method(VALUE obj, VALUE vid)
{
...
if (!id) {
if (!NIL_P(klass = rb_singleton_class_get(obj))
...
ご覧のとおり、klassの取り出し元はrb_singleton_class_get関数になっています。この関数は、何らかのモジュールが既にprependされていた場合はクラスのコピーを返します。つまりそのクラスにRCLASS_ORIGINを適用すればよいことになります。そして実際にやってみました。#14658で完全なパッチをご覧いただけます。
訳注: 週刊Railsウォッチ(20180406)でも本記事と#14658を取り上げました。
Rubyと共にあらんことを。