概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 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と共にあらんことを。