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
.
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 }
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
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.
3.145.103.154