2018-03-22

binding_of_callerコードリーディング

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
このエントリーをはてなブックマークに追加