tags:

views:

112

answers:

3

I am trying to find a way to get the binding from the caller within method_missing in Ruby (1.8), but I can't seem to find a way to do it.

Hopefully the following code explains what I would like to do:

class A
  def some_method
    x = 123
    nonexistent_method
  end

  def method_missing(method, *args, &block)
    b = caller_binding # <---- Is this possible?
    eval "puts x", b
  end
end

A.new.some_method
# expected output:
#   123

So... is there a way to obtain the caller's binding, or is this just impossible in Ruby (1.8)?

+3  A: 

If the method is invoked with a block you can get the block's binding (which closes over the caller's binding) by doing block.binding. That doesn't work without a block though.

You can't get the caller's binding directly (well, unless you pass it explicitly of course).

Edit: I should add that there once was a Binding.of_caller method floating around, but that doesn't work with any of the recent ruby versions anymore (where recent includes 1.8.6)

sepp2k
dang, unfortunately I don't have a block in the situation I am in... I hope you are somehow wrong, but I don't think you are :-(
Mike Stone
+2  A: 

This may be a bit messier than you wanted, but here's one way I was able to do it.

#x = 1 # can uncomment out this and comment the other if you like

A = Class.new do
  x = 1
  define_method :some_method do
    x = 123
    nonexistent_method
  end

  define_method :method_missing do |method, *args|
    puts x
  end
end

A.new.some_method

Replacing the class and method definitions with the Class.new and define_method calls is only half the job, though. Unfortunately, the ugly part is that it only works if you already define x beforehand, so you're not really grabbing the caller's binding (instead the callee is modifying the variable at a different scope).

This may be equivalent to just defining all your variables as globals, but this may work for you depending on your situation. And maybe with this you'll be able to find the last piece of the puzzle with this change in hand (if this doesn't work for you).

EDIT: You can get the binding of any of the methods as follows, but even with it, I'm not able to eval successfully (be sure to put this at the top). This will fill up @@binding with the bindings for some_method and method_missing (in that order), so maybe that can help out somehow.

@@binding = []

class Class
  alias real_def define_method
  def define_method(method_name, &block)
    real_def method_name, &block
    @@binding << block.binding
  end
end
Chris Bunch
Appreciate the ideas, unfortunately I don't actually have access to the method I need the binding for... it's defined in a library that I wanted to avoid having a personal patch for, so I can't really shift things around so I have access to the variable. Upvoted for the effort though! :-)
Mike Stone
+6  A: 

Here's a (somewhat fragile) hack:

# caller_binding.rb
TRACE_STACK = []
VERSION_OFFSET = { "1.8.6" => -3, "1.9.1" => -2 }[RUBY_VERSION]
def caller_binding(skip=1)
  TRACE_STACK[ VERSION_OFFSET - skip ][:binding]
end
set_trace_func(lambda do |event, file, line, id, binding, classname|
  item = {:event=>event,:file=>file,:line=>line,:id=>id,:binding=>binding,:classname=>classname}
  #p item
  case(event)
  when 'line'
    TRACE_STACK.push(item) if TRACE_STACK.empty?
  when /\b(?:(?:c-)?call|class)\b/
    TRACE_STACK.push(item)
  when /\b(?:(?:c-)?return|end|raise)\b/
    TRACE_STACK.pop
  end
end)

This works with your example, but I haven't tested it with much else

require 'caller_binding'
class A
  def some_method
    x = 123
    nonexistent_method
  end
  def method_missing( method, *args, &block )
    b = caller_binding
    eval "puts x", b
  end
end

x = 456
A.new.some_method #=> prints 123
A.new.nonexistent_method #=> prints 456

Of course, this won't work if the binding doesn't define the variable you're trying to evaluate, but this is a general issue with bindings. If a variable is not defined, it doesn't know what it is.

require 'caller_binding'
def show_x(b)
  begin
    eval <<-SCRIPT, b
      puts "x = \#{x}"
    SCRIPT
  rescue => e
    puts e
  end
end

def y
  show_x(caller_binding)
end

def ex1
  y #=> prints "undefined local variable or method `x' for main:Object"
  show_x(binding) #=> prints "undefined local variable or method `x' for main:Object"
end

def ex2
  x = 123
  y #+> prints "x = 123"
  show_x(binding) #+> prints "x = 123"
end

ex1
ex2

To get around this, you need to do some error handling within the evaluated string:

require 'caller_binding'
def show_x(b)
  begin
    eval <<-SCRIPT, b
      if defined? x
        puts "x = \#{x}"
      else
        puts "x not defined"
      end
    SCRIPT
  rescue => e
    puts e
  end
end

def y
  show_x(caller_binding)
end

def ex1
  y #=> prints "x not defined"
  show_x(binding) #=> prints "x not defined"
end

def ex2
  x = 123
  y #+> prints "x = 123"
  show_x(binding) #+> prints "x = 123"
end

ex1
ex2
rampion
I tried it out on my box and like my answer, it relies on x being declared in a different scope also (here if x = 456 isn't stated, it doesn't work).
Chris Bunch
If x isn't defined in the caller's binding, it's not going to find it, no more than `eval "puts x", binding()` would work in the caller's context. See my edit for more.
rampion
@Chris: Rampion's `caller_binding` implementation works with no special accommodations. Though it needs `"1.8.7" => -3` added to its offset hash in order to work with Ruby 1.8.7.
Chuck
Not quite the solution I want, but I think this is the best you can do, so I'm accepting.For the record, I wanted access to a variable defined in a Rails template engine to set up a method missing to access that variable and invoke methods on the variable (to turn "var.method" into just "method" so it is more like a DSL). I don't want such a complex solution for this, so I ended up just patching the plugin... which I was trying to avoid in the first place. Oh well! Thanks for the ideas! :-)
Mike Stone