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のモジュール関数について

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ