Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining "method_called".. How do I make a hook method which gets called every time any function of a class gets called?

I want to make a hook method which gets called everytime any function of a class gets called. I have tried method_added, but it executes only once at the time of class definition,

class Base

  def self.method_added(name)
    p "#{name.to_s.capitalize} Method's been called!!"
  end
  def a
    p "a called."
  end
  def b
    p "b called."
  end
end
t1 = Base.new
t1.a
t1.b
t1.a
t1.b

Output:

"A Method's been called!!"
"B Method's been called!!"
"a called."
"b called."
"a called."
"b called."

but my requirement is that any function of a class that gets called anywhere in the program triggers the "method_called", hook method.

Expected Output:
"A Method's been called!!"
"a called."
"B Method's been called!!"
"b called."
"A Method's been called!!"
"a called."
"B Method's been called!!"
"b called."

If there is any defined existing hook method that works just the same, then please tell about it.

Thanks in advance..

like image 778
Apoorv Saxena Avatar asked Jul 13 '10 10:07

Apoorv Saxena


3 Answers

Take a look at Kernel#set_trace_func. It lets you specify a proc which is invoked whenever an event (such as a method call) occurs. Here's an example:

class Base
  def a
    puts "in method a"
  end

  def b
    puts "in method b"
  end
end

set_trace_func proc { |event, file, line, id, binding, classname|
  # only interested in events of type 'call' (Ruby method calls)
  # see the docs for set_trace_func for other supported event types
  puts "#{classname} #{id} called" if event == 'call'
}

b = Base.new
b.a
b.b

Outputs:

Base a called
in method a
Base b called
in method b
like image 68
mikej Avatar answered Nov 14 '22 12:11

mikej


method_added is there to run code when a new method has been added to the class; it doesn't report when a method has been called. (As you discovered.)

If you don't want to follow mikej's answer, here is a class that implements your specification:

#!/usr/bin/ruby

class Base
  def self.method_added(name)
    if /hook/.match(name.to_s) or method_defined?("#{name}_without_hook")
      return
    end
    hook = "def #{name}_hook\n p 'Method #{name} has been called'\n #{name}_without_hook\nend"
    self.class_eval(hook)

    a1 = "alias #{name}_without_hook #{name}"
    self.class_eval(a1)

    a2 = "alias #{name} #{name}_hook"
    self.class_eval(a2)
  end
  def a
    p "a called."
  end
  def b
    p "b called."
  end
end
t1 = Base.new
t1.a
t1.b
t1.a
t1.b

And output:

$ ./meta.rb
"Method a has been called"
"a called."
"Method b has been called"
"b called."
"Method a has been called"
"a called."
"Method b has been called"
"b called."
like image 23
sarnold Avatar answered Nov 14 '22 11:11

sarnold


I recently wrote something that might be useful, though there are some provisos (see below). Here's the class you want to add your hook to:

class Original  
  def regular_old_method msg
    puts msg
  end

private

  def always_called method_called
    puts "'#{method_called.to_s.capitalize}' method's been called!"
  end
end

And here's the code for adding that hook:

class << Original
  def new(*args)
    inner = self.allocate
    outer_name = self.name + 'Wrapper'
    outer_class = Class.new do
      def initialize inner_object
        @inner = inner_object
      end
      def method_missing method_called, *args
        @inner.send method_called, *args
        @inner.send :always_called, method_called
      end
    end
    outer_class_constant = Object.const_set(outer_name, outer_class)
    inner.send :initialize, *args
    outer_class_constant.new inner
  end
end

If you call it like this...

o = Original.new
o.regular_old_method "nothing unusual about this bit"

You get the following output:

nothing unusual about this bit

'Regular_old_method' method's been called!

This approach would be a bad idea if your code checked class names, because even though you've asked for an object of class 'Original', what you got back was an object of class 'OriginalWrapper'.

Plus I imagine there could be other drawbacks to messing with the 'new' method, but my knowledge of Ruby metaprogramming doesn't stretch that far yet.

like image 1
RobJ Avatar answered Nov 14 '22 11:11

RobJ