Intercession without source changes

By: on August 31, 2012

Methods in a Smalltalk object live in the method dictionary of its class. A method dictionary maps Symbols to CompiledMethods. From the virtual machine’s perspective, anything that understands #run:with:in is compatible with a CompiledMethod, in the sense that the VM sends this message to things that it will execute.

As a result, it’s easy enough to put arbitrary objects (that understand #run:with:in) in a class’s method dictionary. In fact, there’s a library for it.

With this hammer in hand, it becomes trivial to perform all manner of intercessions on code, without instrumenting code through a rewrite+compile cycle: wrap the CompiledMethod in an ObjectAsMethodWrapper (or several) and away you go. What might you do? Pre- and post-condition checking, flagging which methods execute for coverage analysis, profiling, and so on. Today we’re going to turn a method into a memoised one.

ObjectAsMethodWrapper subclass: #MemoizingWrapper
    instanceVariableNames: 'usedArgs'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ObjectAsMethodWrapper-Extra'

ObjectAsMethodWrapper >> initialize
    super initialize.
    usedArgs := Dictionary new.

ObjectAsMethodWrapper >> run: aSelector with: arguments in: aReceiver
    | key |
    key := {aReceiver}, aSelector, arguments.
    usedArgs at: key ifPresent: [:value | ^ value].
    ^ usedArgs
        at: key
        put: (super run: aSelector with: arguments in: aReceiver)

With that in hand, let’s see how it behaves:

TestCase subclass: #MemoizingWrapperTest
    instanceVariableNames: 'transcript'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'ObjectAsMethodWrapper-Extra-Tests'.

MemoizingWrapperTest >> setUp
    transcript := OrderedCollection new.

MemoizingWrapperTest >> recordingMethod: anObject
    transcript add: ('Executed #recordingMethod: with {1}' format: {anObject}).
    ^ anObject.

MemoizingWrapperTest >> testWrapperMemoizesCalls
    | w |
    "Wrap a single method."
    w := MemoizingWrapper installOn: self class selector: #recordingMethod:.
    ["Sanity check"
    self assert: transcript isEmpty.
    "Wrapper leaves method result unchanged"
    self assert: 1 equals: (self recordingMethod: 1).
    "First execution, so of course the method's executed."
    self assert: 1 equals: transcript size.
    self recordingMethod: 1.
    "Second execution with same argument returns memoized value
     rather than executing the method again."

    self assert: 1 equals: transcript size.
    "New execution's result also unchanged."
    self assert: 2 equals: (self recordingMethod: 2).
    "And no further executions of the method have been logged."
    self assert: 2 equals: transcript size.] ensure: [w uninstall]

This technique permits one to install arbitrary before/after/around method changing things around methods that you might not otherwise be able to change. It does, however, have a few drawbacks:

  • Changes in behaviour are not reflected in changes in source.
  • Like annotations, you can’t look at a method’s source and understand exactly how it will behave.
  • Unlike annotations, without some kind of inspector you have no idea that a method is wrapped, or what it might do. At least it’s easy to find wrapped methods, if you thought to check for their presence: ObjectAsMethodWrapper allInstances will let you iterate over all the wrappers.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>