2022-06-10:
Ruby: Binding Of Caller Solutions

The Problem | The Gem Solution | Pure Ruby (but no args) | Pure Ruby (but slow) | C Extension (cheat) | C Extension (clean) | Bugs


The Problem

I love ruby. Unfortunately it doesn't include easy reflection into it's own stack, as with Python and sys._getframe().

At least not unless this bug has finally been closed: https://bugs.ruby-lang.org/issues/15778

And admittedly, you usually don't want to do this.

You probably shouldn't be messing around with anything outside your own scope. Why you messin' with other people's variables?

Well - it turns out there are a small set of legitimate reasons, but most of them are constrained to debug, which leaves us with lots of slow, hacky solutions.

There are, however, some legitimate reason. As a simple example, imagine you want to implement your own version of "eval" for whatever reason. Maybe you want to log every eval to a file, or do some sort of checking on the string. But this doesn't work:

def myEval(str)
	puts "Eval: #{str}"
	eval(str)
end

def someOtherScope
	localVar = 42
	ans = myEval("localVar * 2")
end
The problem is that myEval has no access to it's caller binding. This is some magic that Kernel::eval is able to perform but ruby is not.

So I came up with some solutions, with a variety of tradeoffs.


The Gem Solution

There is a gem with this functionality that will probably work for most purposes, it works under MRI ruby (>=v2), Rubinius and Jruby, though it does give this reasonable warning, "Do not use in production apps."

Meh. Not with that bad attitude, maybe...

More info: https://github.com/banister/binding_of_caller


The Pure Ruby (no args) Solution

There is a pure ruby solution which is fast and elegant though complex, but it unfortunately doesn't allow for any args into your method, and it looks like voodoo magic.

Instead of writing like this:

class Magic
	def myMethod()
		bndg = bindingOfCaller
		puts "Local vars: #{bndg.local_variables}"
	end
end
You can do:
# Idea stolen from: https://bugs.ruby-lang.org/issues/18487
class Magic
	define_singleton_method :myMethod, Object::method(:binding) >> ->(bndg) {
		puts "Local vars: #{bndg.local_variables}"
		...
	}
end
The problem is that, because we are sneaking in the call to Kernel::binding by taking it's method and making it a composition with our proc, but if we pass args in then they have to go through Kernel::binding as well. I've tried some experiments with either coming up with a new type of composition for Proc or even updating Kernel::binding to be able to take and pass on args (as well as the binding) but have not succeeded yet.

If you can figure this out, please let me know!

Here's some notes on what is happening here:

First uses Object::method to look up the Kernel::binding method (not actually call it).

The Proc#>> method is composition: Given f and g are procs (that can be called) "f >> g" creates a new proc that takes args that does "g(f(args))"

So we basically turn the Kernel::binding method into a proc which we compose with our lambda so we call lambda(Kernel::binding), but somehow we have to add our arguments to that.


The Pure Ruby SLOW Solution

This solution is in pure ruby, and it's ridiculously slow. It uses the TracePoint API which cleans up access to Kernel#set_trace_func.

This API is mainly used for debugging - but you can use it to track the binding calls. The problem is that it means creating and saving off the binding for every new scope, and this will massively slow down your code. My experience was that my code would run a literal 5 to 10 times slower with this turned on. Bummer. It allows you to go up to 'n' steps back in the stack, which is nice. It's possible you could get a small speedup if you only held the last 3 items (see comments) or so, but the majority of speed lost is in just creating the binding each time, so it doesn't help much.

Here's the code. You can enable it and disable it so as to only use it when you must, since, really, it's darn tootin' slow. This version is Fiber aware, since I was using Fibers. You'd have to do something similar for Threads if you are using those.

  #########################
  # Binding stack support
  #########################

  # Hash of template fiber (nil for main) to array of bindings
  # Fiber aware (keeps a separate stack for each Fiber)
  @@bindingStackTracer = nil

  # Turn on callerBinding tracing for a block of code.
  # This allows us to get the binding() for whomever
  # called us, which is needed by the 'compile()' functions.
  # Unfortunately, this is massively slow - so it's been replaced
  # with the mostly functional and faster callerBinding extension
  #
  # This allows us to enable tracing the bindings for every callee.
  # If given a block, then will execute the block and then disable after.
  #
  # This code is shockingly slow - we were seeing a 5x slowdown in
  # general runs just by enabling this across our fiber code.
  def enableBindingTracing(&block)
    unless @@bindingStackTracer
      @@bindingStack = {}
      @@bindingStackTracer = TracePoint.new(:call, :return, :b_call, :b_return) { |tp|
        tmpl = Fiber::current
        if tmpl.is_a? Template
          event = tp.event
          @@bindingStack[tmpl] ||= []
          @@bindingStack[tmpl].push(tp.binding) if event==:call || event==:b_call
          @@bindingStack[tmpl].pop if event==:return || event==:b_return
        end
      }

      # We need to clear @@bindingStack (at least the temlates) on reset
      PhotonTest::onReset { @@bindingStack = Hash.new }
    end
    Verif::assert(!@@bindingStackTracer.enabled?) { "Tried to disable already disabled bindingTracer?" }
    @@bindingStackTracer.enable
    return unless block
    yield
    @@bindingStackTracer.disable
  end

  def disableBindingTracing
    Verif::assert(@@bindingStackTracer.enabled?) { "Tried to disable already disabled bindingTracer?" }
    @@bindingStackTracer.disable
  end

  # Find out the binding of a caller, only works in sections
  # we have used enableBindingTracing.  This is massively
  # slow and is generally replaced by the callerBinding extension
  def bindingOfCaller(back = -1)
    Verif::assert(@@bindingStackTracer) { "Trying to get caller bindings when caller binding tracing is not enabled" }
    # Now look through the @@bindingStack for this template
    tmpl = Template::curr

    return nil unless @@bindingStack[tmpl]
    # -1 is the last push which is now
    # -2 is the caller of *this*
    # Usually they want *their* caller, which is -3
    @@bindingStack[tmpl][-2 + back]
  end


C Extension Cheat

It turns out you can call eval() as a ruby function inside of a C extension and it uses the scope of the ruby code that called your C extension. So you can actually call eval("binding") and get the binding back from a C extension. There are two major issues:
  1. There's a bug in how the rb_eval_string works that incorrectly hardcodes the receiver to 'Main' (and here's the bug I filed, local)
  2. They've decided to kill the ability to call 'binding' inside of a C extension as of ruby 3.2.0, thereby killing every C extension that has eval() clone behavior:
This C extension does a neat trick - we need to call the C extension first to get the binding, we can't just call it from the method that wants the binding, because then we'd just get the binding of that method. So instead you tell the extension to put a wrapper around any method that you want to get an additional argument of the callers binding, like this:

    def myEval(args..., callerBinding)
      ..
    end
		# This tells our C extension to put a wrapper around myEval that feeds it the binding of the caller
    Caller::provideBinding(self, :myEval)
Feel free to download the C code You can make it as an extension with a simple extconf.rb of:
require 'mkmf'
create_makefile('callerBinding')
Then you do: "ruby extconf.rb ; make" and then you need to "require 'callerBinding'" in your ruby code.


C Extension (Clean)

I'm taking the important bits out of the debug_inspector gem that is used by the binding_of_caller gem mentioned above and renamed it. So now you can do:
  def bindingOfCaller
    # frame 0 is the C ext, frame 1 is this code, frame 2 is our caller, frame 3 is the one we want
    RubyVM::CallerBinding.open { |dc| return dc.frame_binding(3) }
  end
And then use the following extension. (Sorry about the mixing of snake_case and camelCase, I prefer to just use camelCase for all of my code even though the ruby standard is to switch back and forth - and this is an edit of someone else's code...)


Bugs/Errata

I'm not responsible for the binding_of_caller gem, but the rest of it is my fault. And there are no bugs that I know of other than what's specified in each solution.

If you find any, please feel free to contact me


Back to Solutions.

DaveSource.com - Dave's geek site GetDave.com - all the current Dave Pointers. MarginalHacks - I have an elegant script for that, but it's too small to fit in the margin.