binding_of_caller (v0.8.0)のコードリーディングをしました。Rubyのバージョンは2系以上のものを扱っていきます。
まずlib/binding_of_caller.rbの定義は以下のようになっており、Rubyのバージョンなどで読み込むファイルが変わります。Rubyの2系の場合はbinding_of_caller/mri2がrequireされます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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が生えます。
1 2 3 |
class ::Binding include BindingOfCaller::BindingExtensions end |
BindingOfCaller#of_callerは以下のように定義されています。#callersを呼び出して、配列の要素を取り出しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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のインスタンス)を取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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のインスタンス)から情報を取得して、フレームタイプやラベルを返却しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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を使って各フレームの情報をセットしています。
1 2 3 4 5 6 7 8 |
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 |
コメントを残す