Saturday, July 30, 2011

Caller binding

One of most useful feature not present in Ruby is to get the binding of the caller of current method, to do something with its local variables.

There is an implementation in the Extensions gem, but it must be the last method call in the method, and we must use the binding within a block.

There is another implementation here, but it depends on tracing along all the code execution, compromising the performance.

However, in this answer in StackOverflow, Taisuke Yamada implemented an version of ppp.rb (what is it?) which inspired me to implement my own version of a caller_binding method. Enjoy!

#!/usr/bin/ruby
#
# (c) Sony Fermino dos Santos, 2011
# License: Public domain
# Implementation: 2011-07-30
#
# Published at:
# http://rubychallenger.blogspot.com/2011/07/caller-binding.html
#
# Inspired on:
# http://stackoverflow.com/questions/1356749/can-you-eval-code-in-the-context-of-a-caller-in-ruby/6109886#6109886
#
# How to use:
# return unless bnd = caller_binding
# After that line, bnd will contain the binding of caller

require 'continuation' if RUBY_VERSION >= '1.9.0'

def caller_binding
  cc = nil     # must be present to work within lambda
  count = 0    # counter of returns

  set_trace_func lambda { |event, file, lineno, id, binding, klass|
    # First return gets to the caller of this method
    # (which already know its own binding).
    # Second return gets to the caller of the caller.
    # That's we want!
    if count == 2
      set_trace_func nil
      # Will return the binding to the callcc below.
      cc.call binding
    elsif event == "return"
      count += 1
    end
  }
  # First time it'll set the cc and return nil to the caller.
  # So it's important to the caller to return again
  # if it gets nil, then we get the second return.
  # Second time it'll return the binding.
  return callcc { |cont| cc = cont }
end

# Example of use:

def var_dump *vars
  return unless bnd = caller_binding
  vars.each do |s|
    value = eval s.to_s, bnd
    puts "#{s} = #{value.inspect}"
  end
end

def test
  a = 1
  s = "hello"
  var_dump :s, :a
end

test

7 comments:

  1. I am bit puzzled with your code.
    In line 20,you have: cc = nil
    And the next time you refer to cc in line 31, you call a method on cc without ever initializing it to non-ni. when I try running your example, i get "undefined method `call' for nil:NilClass "

    ~ raj

    ReplyDelete
    Replies
    1. Hi, thank's for your comment! I've tested on Ruby 1.8.7, exactly as above; I got no errors.

      The flow is very tricky.

      When you call "return unless bnd = caller_binding", cc is set to nil, a trace_func is created, and callcc will create a Continuation object into cc variable and return nil.

      But then, code reaches the "return" of "return callcc { |cc| }" (line 40). That will evoke the trace_func defined before. First time count will be 0 and will be incremented since the event is "return". Then the code flows to out: bnd = caller_bindind, which is the "nil" returned by callcc.

      "unless nil" will be evaluated to true, so it will "return" again (for "return unless bnd = caller_binding", line 46). The trace_func is evoked. Since count == 1 and event is "return", it will increment count again. Then code continues outside the caller of the caller (in the example, out of var_dump, in line 57). At every line, trace_func is invoked; so, in line 57, trace_func is called again, and then count will be 2. In line 29 the trace_func will be disabled. But in line 57 we have the binding which we want. That binding is in "binding" variable of the lambda func (line 23). And finally we will do a cc.call(binding).

      When a Continuation is created, code flow will jump to the end of the block of callcc, when cc.call(arg) is issued. And callcc will return arg to the caller.

      So, when we use "cc.call(binding)", the program will jump to the end of line 40, and the callcc will return that binding, which will be returned to "bnd = caller_binding" (line 46 again). Now, bnd is not nill, and "unless bnd" will evaluate to false; so it won't return again, and program continues on line 47 with the desired external binding on bnd.

      Delete
  2. Sony

    Thanks for a great example.

    I suppose under Ruby 1.9 line 40 should be `callcc {|cont| cc = cont}` because block argument always shadow local variables in Ruby 1.9.

    I tried `callcc {|cc|}` first, but it always returned `nil`, so I had to modify it the way described above.

    ReplyDelete
  3. Sad to inform you all interested parties, that the `callcc` solution has issues under 64-bit (x86_64) rubies resulting in a segfault...

    Tried -p392, -p429, -p448, all the same.

    The sadness gets even worse considering the fact stuff *does not reliably fail* all the time. My code using `callcc` crashes when run from RSpec, but is totally ok when run under regular server or console.

    Presumably it has something to do with stack overflow, but I didn't investigate it thoroughly.

    32-bit Rubies don't seem to be affected, but who can guarantee you're given a 32-bit version these days?

    ReplyDelete
  4. This is really useful code.

    I'm currently getting a warning:

    /opt/rubies/ruby-2.3.1/lib/ruby/2.3.0/x86_64-linux/continuation.so: warning: callcc is obsolete; use Fiber instead

    Do you plan on updating this with with Fiber?

    ReplyDelete