Search code examples
rubymemorygarbage-collectionmetaprogrammingmonkeypatching

How to monkeypatch "GC.start"


I want to do some experiments which involve hooking into invocations of GC.start.

Ruby tells me GC is not a class when I run this:

class GC
  def self.start
    puts "hello"
    super
  end
end

But running this, Ruby tells me GC.start has no superclass, so I think I'm not actually hooking into the original one, but just somehow taking over that name:

module GC
  def self.start
    puts "hello"
    super
  end
end
GC.start

How can I monkeypatch GC.start?


Solution

  • Let's first redefine GC::start so we can see when it is invoked.

    module GC
      def self.start(full_mark: true, immediate_sweep: true)
        puts "old start, full_mark: #{full_mark}, " +
          "immediate_sweep: #{immediate_sweep}"
      end
    end  
    

    Here are two ways to obtain the desired result.

    1. Use Module#prepend from within GC's singleton class

    module X 
      def start(full_mark: true, immediate_sweep: true)
        puts "new start, full_mark: #{full_mark}, " +
          "immediate_sweep: #{immediate_sweep}"
        method(__method__).super_method.call(full_mark: full_mark,
          immediate_sweep: immediate_sweep)  
      end
    end
    
    module GC
      class << self
        prepend X
      end
    end
    

    GC.start(full_mark: 'cat')
    new start, full_mark: cat, immediate_sweep: true
    old start, full_mark: cat, immediate_sweep: true
    

    Note:

    GC.singleton_class.ancestors
      #=> [X, #<Class:GC>, Module, ...] 
    

    Using Module#prepend within GC's singleton class is like GC.extend X except it places X ahead of GC's singleton class among GC's ancestors. See also Method#super_method, Object#method, Kernel#__method__ and Method#call.

    Observe also that:

    GC.singleton_class.public_send(:prepend, X)
    

    can be used in place of:

    module GC
      class << self
        prepend X
      end
    end
    

    2. Use aliasing

    module GC
      class << self
        alias old_start start
      end
    
      def self.start(full_mark: true, immediate_sweep: true)
        puts "new start, full_mark: #{full_mark}, " +
          "immediate_sweep: #{immediate_sweep}"
        old_start(full_mark: full_mark, immediate_sweep: immediate_sweep)
      end
    end
    

    GC.start(full_mark: 'cat')
    new start, full_mark: cat, immediate_sweep: true
    old start, full_mark: cat, immediate_sweep: true
    

    Aliasing was commonly used before Module#prepend made its debut in Ruby v2.0.