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
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.
Illustrating a minimal method wrapper
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.
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:
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.