Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get Method Arguments using Ruby's TracePoint

I'm able to get access to a Ruby method's arguments using the TracePoint API:

def foo(foo_arg)
end

trace = TracePoint.trace(:call, :c_call) do |tp|
  tp.disable
  case tp.method_id
  when :foo, :sub
    method = eval("method(:#{tp.method_id})", tp.binding)
    method.parameters.each do |p|
      puts "#{p.last}: #{tp.binding.local_variable_get(p.last)}"
    end
  end
  tp.enable
end

trace.enable

foo(10)
# => foo_arg: 10

However when I try this with a c method call, I get an error.

"foo".sub(/(f)/) { $1.upcase }
script.rb:20:in `method': undefined method `sub' for class `Object' (NameError)
    from script.rb:20:in `<main>'
    from script.rb:8:in `eval'
    from script.rb:8:in `block in <main>'
    from script.rb:20:in `<main>'

This looks like it happens because of a discrepancy between the binding returned when using a C method call and regular Ruby method call.

In the Ruby case tp.self is equal to tp.binding.eval("self") is main however in the C case tp.self is "foo" and tp.binding.eval("self") is main. Is there a way to get the arguments passed into a method using TracePoint for both Ruby and C defined methods?

like image 427
Schneems Avatar asked Jun 01 '15 22:06

Schneems


1 Answers

As you point in your question and as it documented in ruby documentation, tp.self returns a traced object, which have a method method you are looking for. I think you should use

method = tp.self.method(tp.method_id)

instead of

method = eval("method(:#{tp.method_id})", tp.binding)

UPDATE. Some explanation regarding your last paragraph in question. tp.self in first case (when you call foo) is point to main, because you define foo method in main context and it points to String object in second case because sub is defined there. But tp.binding.eval("self") returns main in both cases because it returns a calling context (not a 'define' context as you expect) and in both cases it is main.

UPDATE (in reply to comment) I think that the only way to do this is to monkey patch sub and all other methods that you are interesting for. Code example:

class String
  alias_method :old_sub, :sub
  def sub(*args, &block)
    old_sub(*args, &block)
  end
end

trace = TracePoint.trace(:call, :c_call) do |tp|
  tp.disable
  case tp.method_id
  when :sub
    method = tp.self.method(tp.method_id)
    puts method.parameters.inspect
  end
  tp.enable
end

trace.enable

"foo".sub(/(f)/) { |s| s.upcase }

One big drawback is that you can't use $1, $2, ... vars in your original blocks. As pointed here where is no way to make it works. However you can still use block parameters (s in my example).

like image 56
Алексей Овчинников Avatar answered Nov 02 '22 21:11

Алексей Овчинников