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