binding.pryの仕組み

binding.pryしてますか? 普段何気なくbinding.pryを使っているけど、その中身についてはブラックボックスな認識のまま使っていたので、一部のみですが仕組みをソースコードリーディングしながら追ってみました。

便利な Pry コマンド

whereami (@)

alias は @ です。 このコマンドは pry が現在実行しているコンテキストのソースコードを ruby 上に表示してくれます。

debug していると「今どの部分のコード(コンテキスト)を実行しているんだっけ?」となったときに「@」と入力すれば、その付近のコードを出力してくれます。

pry-byebug と組み合わせてnextコマンドを使ったりすると思いますが、いまどこにいるのかわからなくなったりするときに確認できて便利です。

pry> whereami

From: TestController#index:

     8: def index
     9:   binding.pry
 => 10: end

show-source ($)

alias は $ です。 このコマンドは pry の REPL 上で引数にクラスやメソッドを渡して実行するとその定義場所を教えてくれます。

例えば、あらゆる箇所で呼び出されるcurrent_userメソッドがどこで定義されているのか(show-sourceしたときに呼ばれるcurrent_userメソッドはどこのメソッドが呼ばれているのか)を見つけてみましょう。

pry> show-source current_user

From: /app/controllers/application_controller.rb:188:
Owner: ApplicationController
Visibility: public
Signature: current_user
Number of lines: 7

def current_user
  return @_current_user if @_current_user
  if current_user
    @_current_user = User.find_by(id: user_id)
  end
end

ちゃんと gem の内部のクラスやメソッドも定義場所を教えてくれます。

pry> show-source ActiveModel

From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/activemodel-6.1.6.1/lib/active_model/gem_version.rb:2
Module name: ActiveModel
Number of monkeypatches: 3. Use the `-a` option to display all available monkeypatches
Number of lines: 15

module ActiveModel
  # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
  def self.gem_version
    Gem::Version.new VERSION::STRING
  end

  module VERSION

cat

実は pry の REPL 上で cat コマンドが使えます。 別にエディタ開けば良いじゃんという感じですが、integration や production などのサーバー上で rails ruby を起動している場合にサクッと確認したいファイルの内容などがあるときにたまに使います。

pry> cat User.rb

class User < ApplicationRecord
  has_many ...
end

紹介した 3 つの他にも shell command で使えるものや、便利なコマンドがあったりします。 https://github.com/pry/pry/tree/master/lib/pry/commands あたりを眺めて実行してみてください。

binding.pryを仕込んでから ruby で REPL が立ち上がるまで

本記事での対象の pry gem の version は 0.13.1、Ruby は2.7.6です。

class Binding

Ruby の組み込みクラスに Binding クラスというものがあります。 https://docs.ruby-lang.org/ja/latest/class/Binding.html

ローカル変数のテーブルと self、モジュールのネストなどの情報を保持するオブジェクトのクラスです。

説明の通りこのクラスから生成されるオブジェクトは、Ruby を実行している中で「そのある時点の変数やメソッドのコンテキストを含んだオブジェクト」です。

大体この時点でbinding.pryの仕組みの察しがつきますね。

Kernal.#bindingを実行するとBindingクラスのオブジェクトが返ります。

pry> binding
#<Binding:0x00007f3eed13cb70>
pry> binding.class
Binding < Object

Kernelモジュールは Ruby のすべてのクラスから参照することができるメソッドを定義しているモジュールであり、Object クラスはこのモジュールを include しています。つまり、bindingオブジェクトは Ruby 上のほぼすべての箇所から参照可能です。 https://docs.ruby-lang.org/ja/2.4.0/class/Kernel.html

binding.pryで止まった箇所の時点でのローカル変数やスコープが参照できるのはこのBindingクラスのおかげのようです。

binding.source_location

bindingオブジェクトのメソッドにsource_locationというものがあります。 これはbinding.source_locationを実行した箇所におけるselfのソースファイル名と行番号を返してくれるメソッドです。 https://docs.ruby-lang.org/ja/latest/class/Binding.html#I_SOURCE_LOCATION

irb(#<ApplicationController:0x00007fc1f4c51e40>):001:0> binding.source_location
=> ["/app/controllers/application_controller.rb", 1]

まさにソースコードの定義場所を割り当てるのにちょうど良さそうなメソッドですね。 前述したshow-sourceメソッドなんかはこのbinding.source_locationを拡張していたりするのでしょうか?

show-source binding.pryをしてみる

メソッドなどの定義場所を探し出してくれるshow-sourceメソッドですが、binding.pryの仕組みを追いたいのであればshow-sourceしてみればいいじゃないかということで実行してみました。

pry> show-source binding.pry

From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/core_extensions.rb:25:
Owner: Object
Visibility: public
Signature: pry(object=?, hash=?)
Number of lines: 7

def pry(object = nil, hash = {})
  if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
    Pry.start(self, object || {})
  else
    Pry.start(object, hash)
  end
end

実行してみた結果、binding.pryメソッドの実体は pry gem の中のlib/pry/core_extensions.rbにあるようです。

定義箇所のクラスを見てみると、Objectクラスのメソッドとして定義されていますね。 Ruby においてObjectクラスはすべてのクラスの継承元になりますから、このpryメソッドはすべてのクラスで実行することが可能になります。

つまり、Objectクラスを継承したBindingクラスのオブジェクトであるbindingオブジェクトもpryメソッドを実行することができるため、binding.pryという記述が可能になります。 (bindingだけでなくても1.pry'hoge'.pryなども実行できるということになりますね。)

# lib/pry/core_extensions.rb

class Object
  # Start a Pry REPL on self.
  #
  # If `self` is a Binding then that will be used to evaluate expressions;
  # otherwise a new binding will be created.
  #
  # @param [Object] object  the object or binding to pry
  #                         (__deprecated__, use `object.pry`)
  # @param [Hash] hash  the options hash
  # @example With a binding
  #    binding.pry
  # @example On any object
  #   "dummy".pry
  # @example With options
  #   def my_method
  #     binding.pry :quiet => true
  #   end
  #   my_method()
  # @see Pry.start
  def pry(object = nil, hash = {})
    if object.nil? || Hash === object # rubocop:disable Style/CaseEquality
      Pry.start(self, object || {})
    else
      Pry.start(object, hash)
    end
  end

pryメソッドは内部でPry.startというクラスメソッドを実行しており、これによってlib/pry/pry_class.rb の self.start メソッドが呼び出されます。

# lib/pry/pry_class.rb

# Start a Pry REPL.
# This method also loads `pryrc` as necessary the first time it is invoked.
# @param [Object, Binding] target The receiver of the Pry session
# @param [Hash] options
# @option options (see Pry#initialize)
# @example
#   Pry.start(Object.new, :input => MyInput.new)
def self.start(target = nil, options = {})
  return if Pry::Env['DISABLE_PRY']
  if Pry::Env['FAIL_PRY']
    raise 'You have FAIL_PRY set to true, which results in Pry calls failing'
  end

  options = options.to_hash

  if in_critical_section?
    output.puts "ERROR: Pry started inside Pry."
    output.puts "This can happen if you have a binding.pry inside a #to_s " \
                "or #inspect function."
    return
  end

  unless mutex_available?
    output.puts "ERROR: Unable to obtain mutex lock."
    output.puts "This can happen if binding.pry is called from a signal handler"
    return
  end

  options[:target] = Pry.binding_for(target || toplevel_binding)
  initial_session_setup
  final_session_setup

  # Unless we were given a backtrace, save the current one
  if options[:backtrace].nil?
    options[:backtrace] = caller

    # If Pry was started via `binding.pry`, elide that from the backtrace
    if options[:backtrace].first =~ /pry.*core_extensions.*pry/
      options[:backtrace].shift
    end
  end

  driver = options[:driver] || Pry::REPL

  # Enter the matrix
  driver.start(options)
rescue Pry::TooSafeException
  puts "ERROR: Pry cannot work with $SAFE > 0"
  raise
end

最後のdriver.start(options)で、また何かを start していますが、driver にはPry::REPLクラスが入ってくるので、最終的には pry の REPL が起動され、いつも見ているbinding.pryの画面になります。

# pryのREPL
pry>

REPL の start メソッドはlib/pry/repl.rbにあります。 ここで、prologueメソッドが実行されているのですが、その中身を見てみるとpry.exec_hookメソッドが実行されているのが分かります。

# lib/pry/repl.rb

# Start the read-eval-print loop.
# @return [Object?] If the session throws `:breakout`, return the value
#   thrown with it.
# @raise [Exception] If the session throws `:raise_up`, raise the exception
#   thrown with it.
def start
  prologue
  Pry::InputLock.for(:all).with_ownership { repl }
ensure
  epilogue
end

private

# Set up the repl session.
# @return [void]
def prologue
  pry.exec_hook :before_session, pry.output, pry.current_binding, pry

  return unless pry.config.correct_indent

  # Clear the line before starting Pry. This fixes issue #566.
  output.print(Helpers::Platform.windows_ansi? ? "\e[0F" : "\e[0G")
end

引数にpry.current_bindingという値が渡されていて、どうやらここらへんで現在の binding オブジェクトが渡されているようですね。

pry.exec_hookメソッドはlib/pry/hooks.rbに実装があります。 少々難しいですが、前述のprologueメソッドで:before_sessionという Symbol が渡されているので、@hooks[:before_session.to_s]が map されます。

# lib/pry/hooks.rb

# Execute the list of hooks for the `event_name` event.
# @param [Symbol] event_name The name of the event.
# @param [Array] args The arguments to pass to each hook function.
# @return [Object] The return value of the last executed hook.
def exec_hook(event_name, *args, &block)
  @hooks[event_name.to_s].map do |_hook_name, callable|
    begin
      callable.call(*args, &block)
    rescue RescuableException => e
      errors << e
      e
    end
  end.last
end

この@hooksインスタンス変数にどんな値が入っているのかというと、pry が initialize されるときにPry::Hooks.defaultというメソッドがすでに実行されており、add_hook メソッドを通じて@hooksインスタンス変数に対して:before_sessionという key とその value(配列)が格納されています。

irb(#<Pry::Hooks:0x0000565023aaddd0>):001:0> @hooks
{
  "before_session" => [
    [
      :default,
      #<Proc:0x0000565023aadc18 /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/hooks.rb:18>
    ]
  ]
}

exec_hookメソッドではevent_name:before_sessionで渡ってきますから、callable.call(*args, &block)lib/pry/hooks.rb:18の Proc が実行されます。

# lib/pry/hooks.rb

def self.default
  hooks = new
  hooks.add_hook(:before_session, :default) do |_out, _target, pry_instance|
    next if pry_instance.quiet?

    pry_instance.run_command('whereami --quiet')
  end
  hooks
end

ここで、pry_instance.run_command('whereami --quiet')が実行されている通り、前述したwhereamiメソッドが実行され、binding.pryを仕込んだ箇所のソースコードが表示されるのです。 そして REPL の input が表示されます。

# binding.pryを仕込んだ箇所で止まったときの表示

From: /app/controllers/test_controller.rb:10 TestController#index:

From: TestController#index:

     8: def index
     9:   binding.pry
 => 10: end

pry(#<TestController>)[2.7.6]>

ここまでで、binding.pryを仕込んだ箇所の Binding クラスのオブジェクトに対して pry の REPL 環境を立ち上げているため、ローカル変数などその時点でのコンテキストを参照できるというのがbinding.pryメソッドの仕組みであることがわかりました。

疑問

binding.pry を仕込んだ箇所で REPL を起動させるロジックの流れを読んでみましたがshow-souceメソッドを実行したとき、巨大なコードベースなアプリケーションでもそこまで load 時間は長くありません。 つまり、すべてのファイルを事前に load しておいて、すべての定義箇所を記憶するなどという重たい処理はやっていないと考えられます。

前述で、「pry におけるshow-sourceメソッドは、Binding クラスのオブジェクトが持っているbinding.source_locationを拡張しているのではないか」という推測をしましたが、show-source current_companyなどを実行してもcurrent_companyメソッドに対してbindingメソッドを仕込んでいるわけではないため、binding.source_locationが使えないはずで、拡張しているわけではなさそうです。(「binding.source_locationを使っている」 == 「current_companyメソッド内部にbinding.source_locationが実行されていないと見つけられない」)

Q. show-source メソッドは巨大なコードベースであっても実行するとすぐに該当定義箇所を見つけて返してくれますが、どのように実現しているのでしょうか?

binding.pryの仕組みがわかったので、次はshow-sourceメソッドの仕組みを追ってみましょう。

show-sourceメソッドの実装を追う

show-source show-sourceを実行してみましょう。(なんかメタいですね) ちゃんと結果が返ってきます。

※ pry-doc が Gemfile にある状態で「show-source show-source」を実行すると、今回の意図とは外れる pry-doc 内部のshow-sourceメソッドが参照されます

pry> show-source show-source

From: /.asdf/installs/ruby/2.7.6/lib/ruby/gems/2.7.0/gems/pry-0.13.1/lib/pry/commands/show_source.rb
Number of lines: 107

class ShowSource < Command::ShowInfo
  include Pry::Helpers::DocumentationHelpers

  match 'show-source'
  group 'Introspection'
  description 'Show the source for a method or class.'

  banner <<-'BANNER'
    Usage:   show-source [OPTIONS] [METH|CLASS]
    Aliases: $, show-method

    Show the source for a method or class. Tries instance methods first and then
    methods by default.

これでshow-sourceメソッドがは pry の中の lib/pry/commands/show_source.rb に実装があることがわかりました。

113 行目あたりを見てみると、show-sourceという文字列が pry のコマンドとして add されているのがわかりますね。

# lib/pry/commands/show_source.rb

Pry::Commands.add_command(Pry::Command::ShowSource)
Pry::Commands.alias_command 'show-method', 'show-source'
Pry::Commands.alias_command '$', 'show-source'
Pry::Commands.alias_command '?', 'show-source -d'

ShowSourceの中身はoptionsメソッドやprocessメソッドが定義されていますが、特別な処理は特にしていないのと、processメソッド内部でsuperが実行されているので、親クラスを見に行くと何かがわかるようです。

# lib/pry/commands/show_source.rb

def options(opt)
  opt.on :e, :eval, "evaluate the command's argument as a ruby " \
                    "expression and show the class its return value"
  opt.on :d, :doc, 'include documentation in the output'
  super(opt)
end

def process
  if opts.present?(:e)
    obj = target.eval(args.first)
    self.args = Array.new(1) { obj.is_a?(Module) ? obj.name : obj.class.name }
  end

  super
end

ShowSourceというクラスは継承関係を追っていくと ShowSource < ShowInfo < ClassCommand < Command のようになっています。 1 つ親のShowInfoクラスのprocessメソッドを見てみましょう。

# lib/pry/commands/show_info.rb

def process
  code_object = Pry::CodeObject.lookup(obj_name, pry_instance, super: opts[:super])
  raise CommandError, no_definition_message unless code_object

  @original_code_object = code_object

  if !obj_name && code_object.c_module? && !opts[:all]
    result = "You're inside an object, whose class is defined by means of " \
             "the C Ruby API.\nPry cannot display the information for this " \
             "class."
    if code_object.candidates.any?
      result += "\nHowever, you can view monkey-patches applied to this " \
                "class.\n.Just execute the same command with the '--all' " \
                "switch."
    end
  elsif show_all_modules?(code_object)
    # show all monkey patches for a module

    result = content_and_headers_for_all_module_candidates(code_object)
  else
    # show a specific code object
    co = code_object_with_accessible_source(code_object)
    result = content_and_header_for_code_object(co)
  end

  set_file_and_dir_locals(code_object.source_file)
  pry_instance.pager.page result
end

Pry::CodeObject.lookupあたりのメソッドがなにやらそれっぽいですね。 code から参照したいメソッドの定義場所を lookup してくれているように見えます。CodeObject.lookupの中身を見てみましょう。

https://github.com/pry/pry/lib/pry/code_object.rb#L69-L74

# lib/pry/code_object.rb

class CodeObject
   ....
    class << self
      def lookup(str, pry_instance, options = {})
        co = new(str, pry_instance, options)

        co.default_lookup || co.method_or_class_lookup ||
          co.command_lookup || co.empty_lookup
      end
    end
  end
end

co.method_or_class_lookupがそれっぽいメソッドな予感がします。さらに見てみましょう。 https://github.com/pry/pry/lib/pry/code_object.rb#L136-L148

# lib/pry/code_object.rb

def method_or_class_lookup
  obj =
    case str
    when /\S+\(\)\z/
      Pry::Method.from_str(str.sub(/\(\)\z/, ''), target) ||
      Pry::WrappedModule.from_str(str, target)
    else
      Pry::WrappedModule.from_str(str, target) ||
      Pry::Method.from_str(str, target)
    end

  lookup_super(obj, super_level)
end

Pry::Method.from_strメソッドを実行しており、さらにその中身を見てみるとfrom_objメソッド内でPry::Method#lookup_method_via_bindingメソッドを実行しています。どうやらここらへんがshow-sourceメソッドにおけるコアロジックのようです。

https://github.com/pry/pry/lib/pry/method.rb#L107-L126

# lib/pry/method.rb

# In order to support 2.0 Refinements we need to look up methods
# inside the relevant Binding.
# @param [Object] obj The owner/receiver of the method.
# @param [Symbol] method_name The name of the method.
# @param [Symbol] method_type The type of method: :method or :instance_method
# @param [Binding] target The binding where the method is looked up.
# @return [Method, UnboundMethod] The 'refined' method object.
def lookup_method_via_binding(
  obj, method_name, method_type, target = TOPLEVEL_BINDING
)
  Pry.current[:obj] = obj
  Pry.current[:name] = method_name
  receiver = obj.is_a?(Module) ? "Module" : "Kernel"
  target.eval(
    "::#{receiver}.instance_method(:#{method_type})" \
    ".bind(Pry.current[:obj]).call(Pry.current[:name])"
  )
ensure
  Pry.current[:obj] = Pry.current[:name] = nil
end

パッと見何をやっているのかわからないですね。 例えばとある Rails controller に仕込んだbinding.pry上でshow-source current_companyを実行したとき、Pry.currentには以下のような値が入ってきます。

> Pry.current
{
  obj: #<Api::Private::HogeController:0x0000001ae6630>,
  name: "current_company",
}

objbinding.pryを実行しているselfが、name にはshow-sourceの引数(定義場所を探したいメソッド名)が入ってきます。 receiverにはKernelが、method_typeには:methodという Symbol が入ってくるため::#{receiver}.instance_method(:#{method_type}).bind(Pry.current[:obj]).call(Pry.current[:name])は以下のようになります。

# lib/pry/method.rb

> ::Kernel.instance_method(:method).bind(self).call("current_company")
#current_company(?)
> ::Kernel.instance_method(:method).bind(self).call("current_company").class
Method < Object

KernelObject)の instance_method であるmethodメソッドをselfで bind し、引数として”current_company”という文字列を渡して実行しています。 https://docs.ruby-lang.org/ja/latest/method/Object/i/method.html

最終的には Ruby の組み込みクラスであるMethodクラスのインスタンスが生成されます。 https://docs.ruby-lang.org/ja/latest/class/Method.html

そして、そのMethodクラスのインスタンスをPry::Methodクラスの引数として渡し、Pry::Methodクラスのインスタンスを生成しています。 https://github.com/pry/pry/lib/pry/method.rb#L152

# lib/pry/method.rb

def from_obj(obj, name, target = TOPLEVEL_BINDING)
  new(lookup_method_via_binding(obj, name, :method, target))
rescue StandardError
  nil
end

Ruby の組み込みクラスであるMethodクラスにはsource_locationというメソッドが存在しており、ソースコードのファイル名と行番号を配列で返します。 https://docs.ruby-lang.org/ja/latest/class/Method.html#I_SOURCE_LOCATION

試しに pry 上で Method クラスに対してsource_locationを実行してみましょう。

pry(#<ApplicationController>)[2.7.6]> method('current_user).source_location
[
  [0] "/app/controllers/application_controller.rb",
  [1] 188
]

ここまでくればもうおわかりですね。 pry のshow-sourceメソッドは、Ruby の組み込みクラスMethodクラスのインスタンスメソッドであるMethod#source_locationメソッドを利用してメソッドの定義場所を割り出しているのです。

上述したShowSourceクラスのprocessメソッドに戻ると、内部でcode_object.source_fileを実行しておりその内部でMethod.source_loationを呼び出しています。

# lib/pry/method.rb

# @return [String, nil] The name of the file the method is defined in, or
#   `nil` if the filename is unavailable.
def source_file
  if source_location.nil?
    if source_type == :c
      info = pry_doc_info
      info.file if info
    end
  else
    source_location.first
  end
end

あとは、割り出した定義箇所の情報を用いて file から該当コード周辺を取り出し、整形して ruby に出力しているという流れになります。

まとめ

  • binding.prybindingは Ruby の組み込みBindingクラスのインスタンスのこと
    • pry はObjectクラスを拡張しているのでBindingクラスのオブジェクトであるbindingオブジェクトに対して pry メソッドが使える
  • show-sourceメソッドは Ruby の Method.source_locationを活用したもので、Method#source_location + Bindingインスタンスの組み合わせで実現されている。

何も知らずに binding.pry を使っていたときは pry gem の中でゴリゴリの独自実装がされていると思っていましたが、ソースコードを読んでみると思った以上に Ruby の力を最大限に使っていました。

Ruby は奥が深いですね。

また、binding.irbという pry を Ruby 本体に逆輸入する動きもあるようで、今後はgem install pryをしなくても pry 相当の環境がビルトインで手に入る未来がすぐそこまで来ています。(ruby 2.7 などではbinding.irbがすでに使えます。) https://k0kubun.hatenablog.com/entry/2021/04/02/211455

使ってみると補完の UI がリッチですね。 まだ挙動が安定しない箇所がありそうでしたが、今後に期待できそうです。