Method wrappers

TL;DR

Sometimes we want to temporarily wrap or replace methods with new behavior. With a method wrapper we replace a compiled method with an object that behaves just like a compiled method, but in fact wraps it with new behavior.

Simulating methods as objects using #run:with:in:

Normally code to be executed by the Pharo virtual machine is compiled as an instance of CompiledMethod CompiledCode variableByteSubclass: #CompiledMethod instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Methods' and saved in the MethodDictionary Dictionary variableSubclass: #MethodDictionary instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Methods' of a class.

Interestingly, however, if the VM finds another kind of object than a compiled method in a method dictionary, it will try to evaluate the method #run:with:in:.

The arguments to #run:with:in are (1) the message selector, (2) the arguments array, and (3) the receiver.

We can use this fact to implement method wrappers . A method wrapper is an object that wraps a compiled method and implements #run:with:in:. This method will typically evaluate the compiled method, and perform some additional before or after code.

MinimalMethodWrapper Object subclass: #MinimalMethodWrapper instanceVariableNames: 'method' classVariableNames: '' package: 'GToolkit-Demo-Reflection-Intercession' is an example of such a method wrapper that in its #run:with:in: method simply performs the compiled method that it wraps.

The (live) diagram below illustrates the situation. A method dictionary can contain either compiled methods or method wrappers, and a method wrapper can contain (wrap) a compiled method.

Illustrating a minimal method wrapper

MinimalMethodWrapper Object subclass: #MinimalMethodWrapper instanceVariableNames: 'method' classVariableNames: '' package: 'GToolkit-Demo-Reflection-Intercession' is a bare bones method wrapper.

It is created by sending #on:, with an existing compiled method as its argument.

wrapper := MinimalMethodWrapper 
				on: Integer >> #factorial.
  

The wrapper is installed by sending it #install. This will replace the compiled method by the wrapper in the dictionary of the method's class: MinimalMethodWrapper>>#install install method methodClass methodDictionary at: method selector put: self

We can verify that an installed wrapper is not a compiled method.

wrapper install.
(Integer >> #factorial) isCompiledMethod not.
  

This minimal wrapper implements #run:with:in: by just running the wrapped method. MinimalMethodWrapper>>#run:with:in: run: aSelector with: anArray in: aReceiver ^ aReceiver withArgs: anArray executeMethod: method

NB: Duck-typing also requires a method wrapper to implement several other methods understood by CompiledMethod CompiledCode variableByteSubclass: #CompiledMethod instanceVariableNames: '' classVariableNames: '' package: 'Kernel-Methods' , so we forward any messages not understood by the wrapper to the method by implementing: MinimalMethodWrapper>>#doesNotUnderstand: doesNotUnderstand: aMessage "Forward all other messages to the compiled method." ^ aMessage sendTo: method

We can verify that the method wrapper works as expected:

5 factorial.
  

Aside: Note that the code bubble above works but shows the wrapped method, notthe wrapper. This is because we have implemented #doesNotUnderstand:. However if we inspect Integer>>#factorial we'll see the wrapper, not the wrapped method.

Integer >> #factorial.
  

To remove the method wrapper, we send #uninstall:

wrapper uninstall.
  

or equivalently:

(Integer >> #factorial) uninstall.
  

Implementing a logging method wrapper

To do something useful, we just change (or wrap) what happens in run:with:in:.

A simple logging wrapper keeps track of an invocation count: LoggingMethodWrapper>>#run:with:in: run: aSelector with: anArray in: aReceiver invocationCount := 1 + invocationCount. ^ super run: aSelector with: anArray in: aReceiver

Here we see it in action, first running the original method, then the wrapper, and finally the original method again.

"Create the logging method wrapper."
logger := LoggingMethodWrapper on: Integer >> #slowFactorial.

"We run the method without the wrapper."
logger invocationCount.
4 slowFactorial.
self assert: logger invocationCount equals: 0.

"We install the wrapper to log the invocations and uninstall it when we are done."
logger install.
[ 5 slowFactorial ] ensure: [ logger uninstall ].
self assert: logger invocationCount equals: 6.

"Again without the wrapper."
logger invocationCount: 0.
10 slowFactorial.
logger invocationCount.
self assert: logger invocationCount equals: 0.

logger.
  

Summary

Method wrappers are class-based . All instances are controlled. You can't instaall them for an individual object.

Only known messages are intercepted. Each method wrapper controls just a single method.

Method wrappers do not require compilation for installation or removal.