ruby-trunk-changes r36229 - r36240

今日は Module#prepend という include に似てるけどメソッド探索の優先順位が違うメソッドが追加されています。しかしまだ少し動作の不安定なところがありそうです。

nobu:r36229 2012-06-27 09:15:51 +0900

IO.popen で引数に String を受け取りつつ環境変数の指定や exec のオプションを受け付けるようにしています。その実装のために Hash オブジェクトから exec/spawn のオプションを取得する rb_execarg_extract_options() という内部向け API を追加しています。 [ruby-dev:45853] [Feature #6651]
また IO.popen("-") とパイプを通じて fork した子プロセスと通信する使いかたが fork(2) を提供していない環境では NotImplementedError 例外を発生させるようにしています。

svn:r36230 2012-06-27 09:15:55 +0900

version.h の日付更新。

nobu:r36231 2012-06-27 13:10:12 +0900

NOEX_NOREDEF というメソッドエントリのビットフラグ定数で、多分再定義を禁止するものがあるのですが、現在は 0 に定義されていて無効になっています。それに関するソースコードが #if 0 でコメントアウトされているのを #if NOEX_NOREDEF で分岐するように変更しています。また rb_raise() で文字列を埋め込むのに VALUE 型を直接受け取れる指示子を利用することで C の文字列化を不要にしています。

nobu:r36232 2012-06-27 14:37:15 +0900

関数の定義のスタイルの整形(引数の型と関数名の間に改行を入れる)をしているだけです。

nobu:r36233 2012-06-27 16:46:54 +0900

r36231 で rb_raise の引数に渡す文字列を VALUE 型のままにしたところで、Class のオブジェクト(つまりクラス)をそのまま渡していた部分を rb_class_name() でクラス名を表す String のオブジェクトにしています。

nobu:r36234 2012-06-27 16:48:50 +0900

Module#prepend が追加されています。 [ruby-core:21822] [Feature #1102]
Module#prepend は Module#include の亜種みたいなもので、include はそのクラスの継承関係で親クラスとの間にモジュール(実装的には正確にはモジュールへの参照を持つ IClass)を挟みこむことでそのモジュールの実装(メソッド/定数)を利用できるようにしますが、これだとそのクラス自身が同じ名前のメソッドを持っていたらそちらが優先されます。

module M
  def m1
    "M#m1"
  end
end

class C
  include M   # メソッドの優先順位は C -> M -> Object ...
  def m1
    "C#m1"
  end
end

C.new.m1  # => "C#m1"

Module#prepend はモジュールのほうを優先順位を高くして上記の例だと M のメソッド定義で C のメソッド定義を「上書きする」ような挙動をさせます。

module M
  def m1
    "M#m1"
  end
end

class C
  prepend M  # メソッド探索の優先順位は M -> C -> Object ...
  def m1
    "C#m1"
  end
end

C.new.m1 # => "M#m1"

また prepend した時に引数の Module に対して呼ばれるコールバックの prepended なども追加しています。
実装としては T_CLASS 型のオブジェクトの拡張データに origin というメンバを追加して、prepend が実行されたらそこに元のクラスが持っていたメソッドテーブルを持った IClass を作って、クラスそのものとメソッドテーブルを持つ IClass を分離し、メソッドテーブルを持たなくなったクラスにモジュールを include することで実現しています。

class C
  # C -> Object
  prepend M
  # 1. C(メソッドテーブル空) -> <C の IClass(メソッドテーブルはこっちに移植されている)>
  # 2. C(メソッドテーブル空) -> <M の IClass> -> <C の IClass(メソッドテーブルはこっちに移植されている)>
  #  C は <C の IClass> への参照を origin で持つ。
end

C へのメソッドの定義や superclass を辿る処理は origin が指している "C の IClass" を起点にするようになっています。
メソッドだけで定数の探索は include と同じみたいです

nobu:r36235 2012-06-27 20:36:15 +0900

Module#prepend の追加により T_CLASS 型のオブジェクトで m_tbl が NULL の場合がありえるようになったため、拡張ライブラリ objspace で ObjectSpace に追加される memsize_of で SEGV が発生していたので NULL チェックを追加しています。

nobu:r36236 2012-06-27 21:01:39 +0900

さらに Module#instance_methods の実装でも T_CLASS 型オブジェクトの m_tbl にチェックなしでアクセスしているところがあって SEGV していたのを NULL チェックを追加して修正しています。 [ruby-core:45915] [Bug #6655]
しかし以下のように prepend を使うと instance_methods(false) がクラスのインスタンスメソッドを返さなくなってしまっています。 [Bug #6660]

module M
end
class C
  prepend M
  def m
  end
end
C.instance_methods(false) # => []

nobu:r36237 2012-06-27 21:31:17 +0900

Module#prepend で別のモジュール M1 を include しているモジュール M2 を Mix-in(と言うのでしょうか?)した時に M2 のメソッドだけ取り込まれて M1 のメソッドが取り込まれない、つまり継承関係のリストに M1(の IClass) が入らないので、prepend と同時に対象のモジュールの継承関係の親も辿って全て include するように(Module#include と同様に)処理するようにしています。[ruby-core:45914] [Bug #6654]
ここの修正の影響かどうか未確認ですが、Module#prepend すると Module#ancestors になぜか Class が入り込むようです。 include だと入らないので意図的ではないような気がします。また ancestors で入るだけで Object#is_a? の判定やメソッドの呼び出しができてしまったりはしないようです。 [Bug #6658]

module M; end
class C; prepend M; end
C.ancestors # => [C, M, Class, Object, Kernel, BasicObject]
C.new.is_a?(Class) # => false

nobu:r36238 2012-06-27 22:40:34 +0900

Object#singleton_methods でも m_tbl を NULL チェックせずアクセスして SEGV していたところがあったので Module#prepend の追加に伴い NULL チェックを追加する修正をしています。

nobu:r36239 2012-06-27 22:40:37 +0900

Module#remove_method でメソッドの定義を取り除く時にも origin (つまりメソッドテーブルを移植した IClass) を処理の対処にするように修正しています。
ちなみに undef_method は「未定義」というマークを積極的にメソッドテーブルに追加するため include したモジュールや superclass で定義されていても NoMethodError になるのに対して remove_method はメソッドテーブルからそのメソッドの定義を取り去るだけなので ancestors にそのメソッドの定義があればそれが呼ばれるようになります。
で、prepend しているクラスで undef_method した場合は prepend したモジュールのメソッドは NoMethodError になるべきなんでしょうかねぇ。上書きしているので呼べる、でもいいような気はします。

kazu:r36240 2012-06-27 23:18:26 +0900

irb に NotImplementError という定数名での参照があったので NotImplementedError に改名しています。昔は NotImplementError という名前だったみたいですね。