CRubyも出来る子!という話
keita_yamaguchiさんの、「Rubiniusは出来る子!という話」(http://d.hatena.ne.jp/keita_yamaguchi/20080914/1221328049)
を読んで、Rubiniusいいーなー、Cygwinじゃあ動かないんだよなーって悔しがっていたのですが、CRubyでも禁断の秘儀 Binding.of_callerを使えば出来るのではと思い試してみました。
ところが、いろいろ調べてみるとRubyの新しいバージョンでは、Binding.of_callerは動かなくなったそうです。原因がyharaさんの
「Binding.of_callerが1.8.5から動かなくなったのは」(http://mono.kmc.gr.jp/~yhara/d/?date=20070811#p01)にありました。
なるほど、set_trace_funcでreturnイベントが出るタイミングが変わって、callerのbindingが取れなくなったようです。returnのタイミングがだめならlineのタイミングで取ればいいじゃないということで、少し変えてみました。
こんな感じになりました。動いてみるみたいです。でも、1.9.0では動きません。1.8.6で確認しています。もちろん、Cygwin上のRubyです。
サンプルプログラムです
class Foo Map = { 'key1' => lambda { |bar| puts bar Binding.of_caller {|b| p eval("self", b) } } } def baz(key, bar) Map[key].call(bar) end end foo = Foo.new foo.baz('key1', (foo.object_id * 2).to_s(16))
結果はこうなります
1002fbb0 #<Foo:0x1002fbb0>
Ruby 1.8.6対応のBinding.of_callerです。上のサンプルプログラムのうち、Map[key].call(bar)で呼び出したProcオブジェクトが帰ってくるときなぜかreturnイベントが発生しないため、これに対応するため書き換えています。普通にメソッドのBinding.of_callerを取るときはコメントのように書き直してください。
追記 2008/9/16
これまで、bindingを得て継続を使って巻き戻す処理を、set_trace_funcのline, returnイベントで行っていましたが、countが必要な値になったら、イベントの種類に関係なくbindingを得て巻き戻しちゃっていいことに気づいたので、それに対応しました。あと、Ruby 1.9.0で動くか調べるためThread.criticalの設定の処理を省いていましたが、1.9.0では動かないので復活しました。
このプログラムはRuby on RailsまたはRuby breakpointのbinding_of_caller.rbを元にしています、というかほとんど同じです。
def Continuation.create(*args, &block) # :nodoc: cc = nil; result = callcc {|c| cc = c; block.call(cc) if block and args.empty?} result ||= args return *[cc, *result] end def Binding.of_caller(&block) old_critical = Thread.critical Thread.critical = true count = 0 cc, result, error, extra_data = Continuation.create(nil, nil) error.call if error tracer = lambda do |*args| type, context, extra_data = args[0], args[4], args if count == 1 # 実際には2にしてください # It would be nice if we could restore the trace_func # that was set before we swapped in our own one, but # this is impossible without overloading set_trace_func # in current Ruby. set_trace_func(nil) cc.call(eval("binding", context), nil, extra_data) end if type == "return" count += 1 # First this method and then calling one will return -- # the trace event of the second event gets the context # of the method which called the method that called this # method. elsif type == "line" then nil elsif type == "c-return" and extra_data[3] == :set_trace_func then nil else set_trace_func(nil) error_msg = "Binding.of_caller used in non-method context or " + "trailing statements of method using it aren't in the block." cc.call(nil, lambda { raise(ArgumentError, error_msg) }, nil) end end unless result set_trace_func(tracer) return nil else Thread.critical = old_critical case block.arity when 1 then yield(result) else yield(result, extra_data) end end end