Watch method calls in Pharo
This week I learned a quite cool way to watch whether a method is called during the execution of some code in Pharo. Since I found no documentation about such possibility (tell me if you know something I missed), I decided to write a small blogpost about it.
Introduction
To achieve this task, we first need to have an overview of what happens when a message is sent to an object in Pharo. Let’s say we have a Dog class implementing the method #bark. We have the following source code:
dog := Dog new.
dog bark.
The next figure shows a simplified view of the process happening when dog bark
is executed.
When the dog bar
, the virtual machine looks in the class of the message receiver (dog). If such method is defined in Dog class or its superclass, the corresponding CompiledMethod is executed.
In fact, the reality is a little more complicated than that. I will not give too much details because it is not required to understand the rest of this blogpost. The only thing I will precise is that the CompiledMethod is in fact executed in a certain context modeled by an instance of Context. This context holds the information about the current state of the execution and executes the CompiledMethod in Context>>#send:to:with:super:. I’ll come back on this method later.
To watch when a method is called, we create a CompiledMethodProxy object which will, as its name suggests, serve as a proxy that we’ll put between a CompiledMethod and the class holding it.
From Dog class’ point of view, there is no difference because the proxy will implement the interface required to be executed. The interface to be implemented by the proxy to be able to act like a CompiledMethod at execution can be deducted from the source code of Context>>#send:to:with:super:.
Context>>#send: selector to: aReceiver with: arguments super: superFlag
"Simulate the action of sending a message with selector, selector, and arguments, args, to receiver. The argument, superFlag, tells whether the receiver of the message was specified with 'super' in the source method." | class aMethod value context |
class := superFlag
ifTrue: [(self method literalAt: self method numLiterals) value superclass]
ifFalse: [self objectClass: aReceiver].
aMethod := class lookupSelector: selector.
aMethod == nil
ifTrue: [ ^ self
send: #doesNotUnderstand:
to: aReceiver
with: (Array with: (Message selector: selector arguments: arguments))
super: superFlag ].
aMethod isCompiledMethod
ifFalse: [ ^ self
send: #run:with:in:
to: aMethod
with: (Array with: selector with: arguments with: aReceiver)
super: superFlag ].
value := self tryPrimitiveFor: aMethod receiver: aReceiver args: arguments."primitive runs without failure?"
(self isFailToken: value) ifFalse: [^ value].
(selector == #doesNotUnderstand: and: [ (class canUnderstand: #doesNotUnderstand: ) not ])
ifTrue: [
^self error: 'Simulated message ', (arguments at: 1) selector, ' not understood'].
"failure.. lets activate the method"
context := self activateMethod: aMethod withArgs: arguments receiver: aReceiver class: class.
"check if activated method handles the error code (a first bytecode will be store into temp)"
"long store temp"
(context method at: context pc ) = 129
ifTrue: [ context at: context stackPtr put: value last ].
^ context
Notably, the following piece of code:
[...]
aMethod isCompiledMethod
ifFalse: [ ^ self
send: #run:with:in:
to: aMethod
with: (Array with: selector with: arguments with: aReceiver)
super: superFlag ].
[...]
Tells us that if the object in the temporary variable aMethod is not an instance of CompiledMethod, the message #run:with:in: will be sent to it. Thus, our proxy must at least implement this method. If you’re still a bit confused about how it will work, don’t panic. The next section presents the implementation and will make things clear.
Implementation
Let’s implement the CompiledMethodProxy class. It inherits from ProtoObject because we want CompiledMethodProxy to understand as little messages as possible so we can forward them to the CompiledMethod instance. ProtoObject is lightweight version of Object that is convenient to implement proxy as said in its class comment.
ProtoObject establishes minimal behavior required of any object in Pharo, even objects that should balk at normal object behavior.
Generally these are proxy objects designed to read themselves in from the disk, or to perform some wrapper behavior, before responding to a message.
ProtoObject has no instance variables, nor should any be added.
The implementation of the class is the following:
ProtoObject subclass: #CompiledMethodProxy
slots: { #hasRun. #method }
classVariables: { }
category: 'CompiledMethodProxy'
#hasRun instance variable will hold a Boolean indicating whether the method was run or not and #method instance variable will hold the instance of CompiledMethod watched.
Now it’s time implement methods of the proxy. First, let’s create an initialization method:
CompiledMethodProxy>>#initializeOn: aCompiledMethod
hasRun := false.
method := aCompiledMethod
Second, we create accessors for #hasRun and #method:
CompiledMethodProxy>>#hasRun
^ hasRunCompiledMethodProxy>>#method
^ method
Third, we create methods for marking and unmarking the fact that the method watched was executed:
CompiledMethodProxy>>#mark
"Mark the fact that the method was run.
This method should only be used internally.
"
hasRun := trueCompiledMethodProxy>>#unmark
"Forget the fact that the method was run."
hasRun := false
Fourth, things start to get interesting, we want Contexts to be able to “execute” the proxy when it replaces a CompiledMethod. Thus, we need to implement #run:with:in:. The implementation is quite simple. The only thing we need to do before executing the CompiledMethod in our #method instance variable is to #mark the execution of the method:
CompiledMethodProxy>>#run: aSelector with: anArray in: aReceiver
"Log the fact that the method was called and execute it."
self mark.
^ aReceiver withArgs: anArray executeMethod: method
Fifth, we want that objects accessing the CompiledMethod do not get confused by the fact we replaced it by a CompiledMethodProxy. Thus, we will do a bit of meta-programming. Don’t be afraid, although the word sounds really cool, it is not complicated. In our case, we want to forward messages our proxy does not understand to the watched CompiledMethod. This can be done quite easily by overriding #doesNotUnderstand: method. This method is called by Pharo when a message is sent to an object and the corresponding method is not found in the object’s class nor its super classes during the method lookup.
The transfer of messages not understood to the CompiledMethod is implemented as:
CompiledMethodProxy>>#doesNotUnderstand: aMessage
"Messages not understood by myself are forwarded to the CompiledMethod I hold."
^ method perform: aMessage selector withArguments: aMessage arguments
Finally, we implement methods allowing to install/uninstall the proxy instead of the CompiledMethod in the class holding it:
CompiledMethodProxy>>#install
"Install myself instead of the CompiledMethod in the class holding it.
This way, when sending a message to the CompiledMethod (to execute it for example)
I am notified and I can remember that the method was run.
"
method methodClass methodDict
at: method selector
put: selfCompiledMethodProxy>>#uninstall
"Put the CompiledMethod I replaced back to its place.
After this method has been executed, I do not receive CompiledMethod's
messages before forwarding them to it anymore.
"
method methodClass methodDict
at: method selector
put: method
The implementation is complete. However, to make instantiation of proxies easy, we add the following class-side method:
CompiledMethodProxy>>#on: aCompiledMethod
"Initialize the method tracer on aCompiledMethod and returns the method tracer.
Do not install the tracer. You need to explicitely call #install on the instance returned to install it.
"
^ self basicNew initializeOn: aCompiledMethod
Voilà!
Playing with the proxy
It’s now possible to use a CompiledMethodProxy instance to watch calls to a specific method in a class. Let’s say we want to watch if the method #assert: of the TestCase class is called. In this case, we would write the following:
proxy := CompiledMethodProxy on: TestCase>>#assert:.
proxy install.
You can then run a test of the image and check whether TestCase>>#assert was called or not:
proxy hasRun.
If you want, you can unmark the proxy to watch the next method call:
proxy unmark.
When you do not want to watch method calls anymore, you can uninstall the proxy:
proxy uninstall.
Conclusion
Implementing a mechanism to watch method calls is quite easy in Pharo. This is due to the fact that: Pharo is a pure object oriented system. It means that everything is an object. Thus, using polymorphism, it is possible to replace any object by another implementing the required interface.
The implementation of CompiledMethodProxy could be extended to be more generic. It is possible to make the CompiledMethodProxy hold a BlockClosure allowing to execute any arbitrary action. Instead of calling #mark in #run:with:in:, the BlockClosure held by the instance of CompiledMethodProxy would be executed. This modification is left to curious readers of this blogpost.
Acknowledgments
The implementation presented in this blogpost is inspired from HudsonBuildTools20 package (HDTestCoverage class) available in Pharo images (6.1 at the time this post is written). Thanks to Pavel Krivanek for pointing me this package and thanks to the author(s) of this package for the inspiration to write this blogpost. Thanks to Stéphane Ducasse for his comments and advices.