Method wrappers
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.
Normally code to be executed by the Pharo virtual machine is compiled as an instance of CompiledMethod
and saved in the MethodDictionary
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
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.
MinimalMethodWrapper
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
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:
NB:
Duck-typing also requires a method wrapper to implement several other methods understood by CompiledMethod
, so we forward any messages not understood by the wrapper to the method by implementing: MinimalMethodWrapper>>#doesNotUnderstand:
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.
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:
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.
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.