views:

644

answers:

9

I'm trying to get a callback when any method on a particular class is called. Overriding "send" doesn't work. It seems send doesn't get called in normal Ruby method invocation. Take the following example.

class Test
  def self.items
   @items ||= []
  end
end

If we override send on Test, and then call Test.items, send doesn't get called.

Is what I'm trying to do possible?

I'd rather not use set_trace_func, since it'll probably slow down things considerably.

+4  A: 

Use alias or alias_method:

# the current implementation of Test, defined by someone else
# and for that reason we might not be able to change it directly
class Test
  def self.items
    @items ||= []
  end
end

# we open the class again, probably in a completely different
# file from the definition above
class Test
  # open up the metaclass, methods defined within this block become
  # class methods, just as if we had defined them with "def self.my_method"
  class << self
    # alias the old method as "old_items"
    alias_method :old_items, :items
    # redeclare the method -- this replaces the old items method,
    # but that's ok since it is still available under it's alias "old_items"
    def items
      # do whatever you want
      puts "items was called!"
      # then call the old implementation (make sure to call it last if you rely
      # on its return value)
      old_items
    end
  end
end

I rewrote your code using the class << self syntax to open up the metaclass, because I'm not sure how to use alias_method on class methods otherwise.

Theo
No, I want a callback for *any* method invocation. Items is just an example.
Alex MacCaw
So do it with all your methods.
Chuck
Combined with `:method_added` it should do what you need.
Theo
+1  A: 

This does what you want, RCapture: http://cheind.wordpress.com/2010/01/07/introducing-rcapture/

banister
Unfortunately that only works with specific methods that you specify. It works by just overwriting them.
Alex MacCaw
A: 

I don't have a complete answer, but I'm thinking method_added might be helpful here.

Ken
+2  A: 

You can see how this is done via the ExtLib hook functionality. ExtLib::Hook basically allows you to invoke arbitrary callbacks before or after a method is completed. See the code on GitHub here for how its done (it overrides :method_added to automagically rewrite methods as they're added to the class).

Michael Bleigh
+1  A: 

Something like this: works with instance methods and class methods, it will not only intercept the current methods defined in the class but any that are added later though reopening the class etc.

(there is also rcapture http://code.google.com/p/rcapture/):

module Interceptor
  def intercept_callback(&block)
    @callback = block
    @old_methods = {}
  end
  def method_added(my_method)
    redefine self, self, my_method, instance_method(my_method)
  end
  def singleton_method_added(my_method)
    meta = class << self; self; end
    redefine self, meta, my_method, method(my_method)
  end
  def redefine(klass, me, method_name, my_method)
    return unless @old_methods and not @old_methods.include? method_name
    @old_methods[method_name] = my_method
    me.send :define_method, method_name do |*args|
      callback = klass.instance_variable_get :@callback
      orig_method = klass.instance_variable_get(:@old_methods)[method_name]
      callback.call *args if callback
      orig_method = orig_method.bind self if orig_method.is_a? UnboundMethod
      orig_method.call *args
    end
  end
end

class Test
  extend Interceptor
  intercept_callback do |*args|
    puts 'was called'
  end
  def self.items
    puts "items"
  end
  def apple
    puts "apples"
  end
end

class Test
  def rock
    puts "rock"
  end
end

Test.items
Test.new.apple
Test.new.rock
nightshade427
A: 

You can do something like this, you can even put conditions on the method being called or not (I don't think that's to useful, but still you have it just in case).

module MethodInterceptor

  def self.included(base)
    base.extend(ClassMethods)
    base.send(:include, InstanceMethods)
    base.class_eval do 
      # we declare the method_list on the class env
      @_instance_method_list = base.instance_methods.inject(Hash.new) do |methods, method_name|
        # we undef all methods
        if !%w(__send__ __id__ method_missing class).include?(method_name)
          methods[method_name.to_sym] = base.instance_method(method_name)
          base.send(:undef_method, method_name)
        end
        methods
      end
    end
  end

  module ClassMethods

    def _instance_method_list
      @_instance_method_list
    end

    def method_added(name)
      return if [:before_method, :method_missing].include?(name)
      _instance_method_list[name] = self.instance_method(name)
      self.send(:undef_method,  name)
      nil
    end

  end

  module InstanceMethods

    def before_method(method_name, *args)
      # by defaults it always will be called
      true
    end

    def method_missing(name, *args)
      if self.class._instance_method_list.key?(name)
        if before_method(name, *args) 
          self.class._instance_method_list[name].bind(self).call(*args)
        else
          super
        end
      else
        super
      end
    end
  end

end

class Say
  include MethodInterceptor

  def before_method(method_name, *args)
    # you cannot say hello world!
    return !(method_name == :say && args[0] == 'hello world')
  end

  def say(msg)
    puts msg
  end

end

Hope this works.

Roman Gonzalez
A: 

I've got it working using a Proxy class - and then setting a constant using the real class's name. I'm not sure how to get it working with instances though. Is there a way of changing which object variables are pointing too?

Basically, I want to do this:

t = Test.new
Persist.new(t)

t.foo # invokes callback

Here's the code I used to get it working with classes:

class Persist
  class Proxy
    instance_methods.each { |m| 
      undef_method m unless m =~ /(^__|^send$|^object_id$)/ 
    }

    def initialize(object)
      @_persist = object
    end

    protected
      def method_missing(sym, *args)
        puts "Called #{sym}"
        @_persist.send(sym, *args)
      end
  end


  attr_reader :object, :proxy

  def initialize(object)
    @object = object
    @proxy  = Proxy.new(@object)
    if object.respond_to?(:name)
      silence_warnings do
        Object.const_set(@object.name, @proxy)
      end
    end
  end
end
Alex MacCaw
Actually, on second thoughts method redefining might be the way to go - it would work with instances too:https://gist.github.com/5226decf57adc11baf46
Alex MacCaw
+1  A: 

are you trying to hook an instance method of a class? Then the following snippet might help. It uses RCapture which can be installed via

gem install rcapture

An introductionary article can be found at here

require 'rcapture'

class Test 
  include RCapture::Interceptable
end

Test.capture_post :class_methods => :items do
  puts "items!"
end

Test.items 
#=> items!
Christoph Heindl
from the famous developer of RCapture himself!
martinus
A: 

Try proxy_machine gem, it does exactly this

http://github.com/tulios/proxy_machine

Túlio