binding_of_caller (v0.8.0)のコードリーディングをしました。Rubyのバージョンは2系以上のものを扱っていきます。
まずlib/binding_of_caller.rbの定義は以下のようになっており、Rubyのバージョンなどで読み込むファイルが変わります。Rubyの2系の場合はbinding_of_caller/mri2がrequireされます。
dlext = RbConfig::CONFIG['DLEXT']
mri_2 = defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby" &&
RUBY_VERSION =~ /^2/
if mri_2
require 'binding_of_caller/mri2'
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == "ruby"
require "binding_of_caller.#{dlext}"
elsif defined?(Rubinius)
require 'binding_of_caller/rubinius'
elsif defined?(JRuby)
require 'binding_of_caller/jruby_interpreted'
end
BindingクラスにオープンクラスによってBindingOfCaller::BindingExtensionsがincludeされます。これによりBindingオブジェクトに#of_callerや#callersが生えます。
class ::Binding
include BindingOfCaller::BindingExtensions
end
BindingOfCaller#of_callerは以下のように定義されています。#callersを呼び出して、配列の要素を取り出しています。
require 'debug_inspector'
module BindingOfCaller
module BindingExtensions
# Retrieve the binding of the nth caller of the current frame.
# @return [Binding]
def of_caller(n)
c = callers.drop(1)
if n > (c.size - 1)
raise "No such frame, gone beyond end of stack!"
else
c[n]
end
end
# ...
end
#callersではdebug_inspectorのrubygemのRubyVM::DebugInspectorを使っており、Debug Inspector APIをRubyから簡単に使えるようにするライブラリになります。これによってバックトレースや各フレームのbinding、iseq(RubyVM::InstructionSequenceのインスタンス)を取得できます。
def callers
ary = []
RubyVM::DebugInspector.open do |dc|
locs = dc.backtrace_locations
locs.size.times do |i|
b = dc.frame_binding(i)
if b
b.instance_variable_set(:@iseq, dc.frame_iseq(i))
ary << b
end
end
end
ary.drop(1)
end
drop(1)しているのは#callersの呼び出し分を取り除くためです。#of_callerでもdrop(1)していますが、これも#of_callerの呼び出し分を取り除いています。
各バインディングのインスタンスには#frame_typeと#frame_descriptionというメソッドも生えます。これはcallersで取得した@iseq(RubyVM::InstructionSequenceのインスタンス)から情報を取得して、フレームタイプやラベルを返却しています。
module BindingOfCaller
module BindingExtensions
# The type of the frame.
# @return [Symbol]
def frame_type
return nil if !@iseq
# apparently the 9th element of the iseq array holds the frame type
# ...not sure how reliable this is.
@frame_type ||= @iseq.to_a[9]
end
# The description of the frame.
# @return [String]
def frame_description
return nil if !@iseq
@frame_description ||= @iseq.label
end
ちなみにbetter_errorsのgemでは、binding_of_callerがある場合には、binding_of_callerで取得した各bindingに対してevalやframe_descriptionを使って各フレームの情報をセットしています。
def setup_backtrace_from_bindings
@backtrace = exception.__better_errors_bindings_stack.map { |binding|
file = binding.eval "__FILE__"
line = binding.eval "__LINE__"
name = binding.frame_description
StackFrame.new(file, line, name, binding)
}
end