Alias Chaining

As we’ve seen, metaprogramming in Ruby often involves the dynamic definition of methods. Just as common is the dynamic modification of methods. Methods are modified with a technique we’ll call alias chaining.[6] It works like this:

  • First, create an alias for the method to be modified. This alias provides a name for the unmodified version of the method.

  • Next, define a new version of the method. This new version should call the unmodified version through the alias, but it can add whatever functionality is needed before and after it does that.

Note that these steps can be applied repeatedly (as long as a different alias is used each time), creating a chain of methods and aliases.

This section includes three alias chaining examples. The first performs the alias chaining statically; i.e., using regular alias and def statements. The second and third examples are more dynamic; they alias chain arbitrarily named methods using alias_method, define_method, and class_eval.

Tracing Files Loaded and Classes Defined

Example 8-8 is code that keeps track of all files loaded and all classes defined in a program. When the program exits, it prints a report. You can use this code to “instrument” an existing program so that you better understand what it is doing. One way to use this code is to insert this line at the beginning of the program:

require 'classtrace'

An easier solution, however, is to use the -r option to your Ruby interpreter:

ruby -rclasstrace my_program.rb  --traceout /tmp/trace

The -r option loads the specified library before it starts running the program. See Invoking the Ruby Interpreter for more on the Ruby interpreter’s command-line arguments.

Example 8-8 uses static alias chaining to trace all invocations of the Kernel.require and Kernel.load methods. It defines an Object.inherited hook to track definitions of new classes. And it uses Kernel.at_exit to execute a block of code when the program terminates. (The END statement described in BEGIN and END would work here as well.) Besides alias chaining require and load and defining Object.inherited, the only modification to the global namespace made by this code is the definition of a module named ClassTrace. All state required for tracing is stored in constants within this module, so that we don’t pollute the namespace with global variables.

Example 8-8. Tracing files loaded and classes defined

# We define this module to hold the global state we require, so that
# we don't alter the global namespace any more than necessary.
module ClassTrace
  # This array holds our list of files loaded and classes defined.
  # Each element is a subarray holding the class defined or the
  # file loaded and the stack frame where it was defined or loaded.
  T = []  # Array to hold the files loaded

  # Now define the constant OUT to specify where tracing output goes.
  # This defaults to STDERR, but can also come from command-line arguments
  if x = ARGV.index("--traceout")    # If argument exists
    OUT = File.open(ARGV[x+1], "w")  # Open the specified file
    ARGV[x,2] = nil                  # And remove the arguments
  else
    OUT = STDERR                     # Otherwise default to STDERR
  end
end

# Alias chaining step 1: define aliases for the original methods
alias original_require require
alias original_load load

# Alias chaining step 2: define new versions of the methods 
def require(file)
  ClassTrace::T << [file,caller[0]]     # Remember what was loaded where
  original_require(file)                # Invoke the original method
end
def load(*args)
  ClassTrace::T << [args[0],caller[0]]  # Remember what was loaded where
  original_load(*args)                  # Invoke the original method
end

# This hook method is invoked each time a new class is defined
def Object.inherited(c)
  ClassTrace::T << [c,caller[0]]        # Remember what was defined where
end

# Kernel.at_exit registers a block to be run when the program exits
# We use it to report the file and class data we collected
at_exit {
  o = ClassTrace::OUT
  o.puts "="*60
  o.puts "Files Loaded and Classes Defined:"
  o.puts "="*60
  ClassTrace::T.each do |what,where|
    if what.is_a? Class  # Report class (with hierarchy) defined
      o.puts "Defined: #{what.ancestors.join('<-')} at #{where}"
    else                 # Report file loaded
      o.puts "Loaded: #{what} at #{where}"
    end
  end
}

Chaining Methods for Thread Safety

Two earlier examples in this chapter have involved thread safety. Example 8-2 defined a synchronized method (based on an Object.mutex method) that executed a block under the protection of a Mutex object. Then, Example 8-5 redefined the synchronized method so that when it was invoked without a block, it would return a SynchronizedObject wrapper around an object, protecting access to any methods invoked through that wrapper object. Now, in Example 8-9, we augment the synchronized method again so that when it is invoked within a class or module definition, it alias chains the named methods to add synchronization.

The alias chaining is done by our method Module.synchronize_method, which in turn uses a helper method Module.create_alias to define an appropriate alias for any given method (including operator methods like +).

After defining these new Module methods, Example 8-9 redefines the synchronized method again. When the method is invoked within a class or a module, it calls synchronize_method on each of the symbols it is passed. Interestingly, however, it can also be called with no arguments; when used this way, it adds synchronization to whatever instance method is defined next. (It uses the method_added hook to receive notifications when a new method is added.) Note that the code in this example depends on the Object.mutex method of Example 8-2 and the SynchronizedObject class of Example 8-5.

Example 8-9. Alias chaining for thread safety

# Define a Module.synchronize_method that alias chains instance methods
# so they synchronize on the instance before running.
class Module
  # This is a helper function for alias chaining.
  # Given a method name (as a string or symbol) and a prefix, create
  # a unique alias for the method, and return the name of the alias
  # as a symbol. Any punctuation characters in the original method name
  # will be converted to numbers so that operators can be aliased.
  def create_alias(original, prefix="alias")
    # Stick the prefix on the original name and convert punctuation
    aka = "#{prefix}_#{original}"
    aka.gsub!(/([=|&+-*/^!?~\%<>[]])/) {
      num = $1[0]                       # Ruby 1.8 character -> ordinal
      num = num.ord if num.is_a? String # Ruby 1.9 character -> ordinal
      '_' + num.to_s
    }
    
    # Keep appending underscores until we get a name that is not in use
    aka += "_" while method_defined? aka or private_method_defined? aka

    aka = aka.to_sym            # Convert the alias name to a symbol
    alias_method aka, original  # Actually create the alias
    aka                         # Return the alias name
  end

  # Alias chain the named method to add synchronization
  def synchronize_method(m)
    # First, make an alias for the unsynchronized version of the method.
    aka = create_alias(m, "unsync") 
    # Now redefine the original to invoke the alias in a synchronized block.
    # We want the defined  method to be able to accept blocks, so we
    # can't use define_method, and must instead evaluate a string with 
    # class_eval. Note that everything between %Q{ and the matching } 
    # is a double-quoted string, not a block. 
    class_eval %Q{
      def #{m}(*args, &block)
        synchronized(self) { #{aka}(*args, &block) }
      end
    }
  end
end

# This global synchronized method can now be used in three different ways.
def synchronized(*args)
  # Case 1: with one argument and a block, synchronize on the object
  # and execute the block
  if args.size == 1 && block_given?
    args[0].mutex.synchronize { yield }

  # Case two: with one argument that is not a symbol and no block
  # return a SynchronizedObject wrapper
  elsif args.size == 1 and not args[0].is_a? Symbol and not block_given?
    SynchronizedObject.new(args[0])

  # Case three: when invoked on a module with no block, alias chain the
  # named methods to add synchronization. Or, if there are no arguments, 
  # then alias chain the next method defined.
  elsif self.is_a? Module and not block_given?
    if (args.size > 0) # Synchronize the named methods
      args.each {|m| self.synchronize_method(m) }
    else
      # If no methods are specified synchronize the next method defined
      eigenclass = class<<self; self; end 
      eigenclass.class_eval do # Use eigenclass to define class methods
        # Define method_added for notification when next method is defined
        define_method :method_added do |name|
          # First remove this hook method
          eigenclass.class_eval { remove_method :method_added }
          # Next, synchronize the method that was just added
          self.synchronize_method name
        end
      end
    end

  # Case 4: any other invocation is an error
  else
    raise ArgumentError, "Invalid arguments to synchronize()"
  end
end

Chaining Methods for Tracing

Example 8-10 is a variant on Example 8-4 that supports tracing of named methods of an object. Example 8-4 used delegation and method_missing to define an Object.trace method that would return a traced wrapper object. This version uses chaining to alter methods of an object in place. It defines trace! and untrace! to chain and unchain named methods of an object.

The interesting thing about this example is that it does its chaining in a different way from Example 8-9; it simply defines singleton methods on the object and uses super within the singleton to chain to the original instance method definition. No method aliases are created.

Example 8-10. Chaining with singleton methods for tracing

# Define trace! and untrace! instance methods for all objects.
# trace! "chains" the named methods by defining singleton methods
# that add tracing functionality and then use super to call the original.
# untrace! deletes the singleton methods to remove tracing.
class Object
  # Trace the specified methods, sending output to STDERR.
  def trace!(*methods)
    @_traced = @_traced || []    # Remember the set of traced methods

    # If no methods were specified, use all public methods defined 
    # directly (not inherited) by the class of this object 
    methods = public_methods(false) if methods.size == 0

    methods.map! {|m| m.to_sym } # Convert any strings to symbols
    methods -= @_traced          # Remove methods that are already traced
    return if methods.empty?     # Return early if there is nothing to do
    @_traced |= methods          # Add methods to set of traced methods

    # Trace the fact that we're starting to trace these methods
    STDERR << "Tracing #{methods.join(', ')} on #{object_id}
"

    # Singleton methods are defined in the eigenclass
    eigenclass = class << self; self; end

    methods.each do |m|         # For each method m
      # Define a traced singleton version of the method m.
      # Output tracing information and use super to invoke the
      # instance method that it is tracing.
      # We want the defined  methods to be able to accept blocks, so we
      # can't use define_method, and must instead evaluate a string.
      # Note that everything between %Q{ and the matching } is a 
      # double-quoted string, not a block. Also note that there are 
      # two levels of string interpolations here. #{} is interpolated
      # when the singleton method is defined. And #{} is interpolated 
      # when the singleton method is invoked.
      eigenclass.class_eval %Q{
        def #{m}(*args, &block)
          begin
            STDERR << "Entering: #{m}(#{args.join(', ')})
"
            result = super
            STDERR << "Exiting: #{m} with #{result}
"
            result
          rescue
            STDERR << "Aborting: #{m}: #{$!.class}: #{$!.message}"
            raise
          end
        end
      }
    end
  end

  # Untrace the specified methods or all traced methods
  def untrace!(*methods)
    if methods.size == 0    # If no methods specified untrace
      methods = @_traced    # all currently traced methods
      STDERR << "Untracing all methods on #{object_id}
"
    else                    # Otherwise, untrace
      methods.map! {|m| m.to_sym }  # Convert string to symbols
      methods &= @_traced   # all specified methods that are traced
      STDERR << "Untracing #{methods.join(', ')} on #{object_id}
"
    end

    @_traced -= methods     # Remove them from our set of traced methods

    # Remove the traced singleton methods from the eigenclass
    # Note that we class_eval a block here, not a string
    (class << self; self; end).class_eval do
      methods.each do |m|
        remove_method m     # undef_method would not work correctly
      end
    end

    # If no methods are traced anymore, remove our instance var 
    if @_traced.empty?
      remove_instance_variable :@_traced
    end
  end
end


[6] It has also been called monkey patching, but since that term was originally used with derision, we avoid it here. The term duck punching is sometimes used as a humorous alternative.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.186.83