0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TracePointの不可能を可能にする裏技

Posted at

TracePointの弱点

Ruby組み込みのクラスTracePointは非常に便利ですが、
Cで定義されたメソッドはフックできないという弱点があります。

target = ::Class.instance_method(:attr_accessor)

TracePoint.new(:call) do |tp|
  p tp
end.enable(target: target) do
  class Foo
    attr_accessor :foo
  end
  class Bar
    attr_accessor :bar
  end
end
#=> <internal:trace_point>:212:in `enable': specified target is not supported (ArgumentError)

これはドキュメントにも記載があります。

target should be a code object for which RubyVM::InstructionSequence.of will return an instruction sequence.

対応できないメソッドは、正確には"RubyVM::InstructionSequence.ofnilが返るメソッド"ということですね。

target = ::Class.instance_method(:attr_accessor)
p RubyVM::InstructionSequence.of(target)
#=> nil

Cで定義されたメソッドはTracePointでフックすることは不可能なのでしょうか?
今回、この不可能を可能にする方法を発見したので紹介します。

薄いmoduleをprependで挟む

基本となるアイデアは「薄いmoduleをprependで挟む」これだけです。

薄いmoduleとは、superを呼び出すだけのメソッドを定義したmoduleです。

module Hook
  def attr_accessor(*name)
    super
  end
end

これをClassclassにprependします。

::Class.prepend(Hook)

これまでのことをまとめて1つのコードにしてみます。

module Hook
  def attr_accessor(*name)
    super
  end
end
::Class.prepend(Hook)

target = ::Class.instance_method(:attr_accessor)
p RubyVM::InstructionSequence.of(target)
#=> <RubyVM::InstructionSequence:attr_accessor@t.rb:4>

TracePoint.new(:call) do |tp|
  p [tp, tp.self, tp.method_id, tp.binding.local_variable_get(:name)]
  #=> [#<TracePoint:call `attr_accessor' t.rb:4>, Foo, :attr_accessor, [:foo]]
  #=> [#<TracePoint:call `attr_accessor' t.rb:4>, Bar, :attr_accessor, [:bar]]
end.enable(target: target) do
  class Foo
    attr_accessor :foo
  end
  class Bar
    attr_accessor :bar
  end
end

エラーが発生していたコードが動くようになりました!
どのclassからattr_accessorメソッドが何の引数と共に呼び出されたことが完璧に分かります。

仕組みとしては、元のCで定義されたメソッドを呼び出す前に、Rubyで定義されたメソッドが呼ばれてフックが効くようですね。

この方法の注意点

  • どこから呼ばれたかわからない
    • TracePoint#linenoはRubyで書いた薄いmoduleのメソッド定義のある行を取るので、「どこから呼ばれたのか」には使えません……。
    • callerとかを使えばもしかしたらできるかも。
  • コードをちょっと破壊している
    • 薄いmoduleをprependしているので完全に非破壊とはいっていません。。。
  • いちいち指定がいる
    • 「全部のメソッドに一括で対応!」とはいきません。。。

まとめ

TracePointは便利。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?