Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyの`Kernel#singleton_method`バグを修正した話(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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のバグを探してみましたが、このバグに関連するものといえば#8044Object#methodsModule#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と共にあらんことを。

関連記事

異色のRubyメソッド: `Module.class_exec`(翻訳)

[Ruby] Kernelのモジュール関数について


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。