In this post, we describe how we implement a breakpoint that affects only one specific object, and how we implement it using the Reflectivity framework.
What is an object-centric breakpoint?
Let’s take a simple example: imagine two
p1 := 0@2. p2 := 1@3.
Each of these points has two instance variables,
y, that we can change by calling the
setX:setY: method. Imagine that we have a bug related to point
p1, and that we want to halt the execution when this object executes the
We definitely do not want to put a breakpoint directly in the
setX:setY: of class
Point method. Points are used all over the system: putting a breakpoint in class
Point will halt whenever any point calls that method and the image will freeze.
What we really need is a breakpoint that halts the execution only when
setX:setY: is called on
The halt-on-call breakpoint
This breakpoint is an operator called halt-on-call, defined by Jorge Ressia in his Object-Centric debugger. It halts whenever one specific object receives a given message. This is what we need! Ideally, we would like to use an API like this:
p1 haltOnCall: #setX:setY
This installs a breakpoint that halts the method
setX:setY: for the point
p1 exclusively. By extension, all objects should benefit from that API, so that we can install object-centric breakpoints on any kind of object. Now let’s implement it.
Implementing the halt-on-call breakpoint API
Let’s define an interface for this operator in the
Object class, to make it available for all objects. Let’s call it
haltOnCall:. It takes a method selector as parameter, which defines the selector of the method to halt. We delegate the definition and the installation of the breakpoint instrumentation to another object named
Object >> haltOnCall: methodSelector ^ ObjectCentricInstrumenter new halt: methodSelector for: self
This interface installs a halt-on-call breakpoint on its receiver, and returns the object modeling that instrumentation. It is very important to keep a reference to that instrumenter object if we want to uninstall our breakpoint later. This would typically be handled by a higher level tool such as a real debugger.
This method is now our top-level debugging API, available for all objects in the system. Using this API, we can now ask any object to halt when it receives a particular message. Now, we have to create the
ObjectCentricInstrumenter class and implement the
halt:for: method that is used to instrument the receiver in the code above.
Implementing an object-centric instrumenter using Reflectivity
We use the Reflectivity framework as a support to implement object-centric breakpoints.
What is Reflectivity?
Reflectivity is a reflective framework shipped in the base Pharo distribution. It features annotation objects named
Metalink that apply reflective operations at the sub-method level (i.e., at the level of sub-expressions of a method).
A metalink is an annotation of a method AST. It is an object that defines a message selector, a receiver named meta-object, and an optional list of arguments. At run time, when the code corresponding to the annotated AST is reached, the metalink is executed. The message corresponding to the selector is sent to the meta-object, with the previously computed argument list: the corresponding method is executed in the meta-object.
For example, adding logging to
Point>>setX:setY:with Reflectivity goes as follows:
- Instantiate a metalink
- Define a meta-object, for example, a block:
[Transcript show: 'Hello World']
- Define a message selector that will be sent to the block at run time, for example:
- Attach the metalink to the ast node of a method, for example the method
At run time, each time a point will execute
setX:setY:, the metalink will first execute and send
value to the block object
[Transcript show: 'Hello World'], resulting in the execution of the block. Then the execution of
setX:setY: will continue.
Now, back to our original problem, Reflectivity supports object-centric metalinks. This means we can actually scope a metalink to a single, specific object. We will use this feature to implement our object-centric instrumenter and define our object-centric breakpoint.
More details about Reflectivity are available in the latest Reflectivity paper.
Implementing the object-centric instrumenter
Now, we have to create the
ObjectCentricInstrumenter class and implement the
halt:for: method. This class has three instance variables:
targetObject: the target object affected by instrumentation
metalink: the instrumentation per se, that is, a metalink
methodNode: the AST node of the method we instrument
Object subclass: #ObjectCentricInstrumenter instanceVariableNames: 'targetObject metalink methodNode' classVariableNames: '' package: 'Your-Pharo-Package'
In this class, we have to define how we install the halt-on-call breakpoint on our object. This is done through the
halt:for: method. This method takes two parameters: the message selector of the method that will halt and the target object that the breakpoint will affect.
ObjectCentricInstrumenter >> halt: methodSelector for: anObject targetObject := anObject. metalink := MetaLink new metaObject: #object; selector: #halt. targetObject link: metalink toMethodNamed: methodSelector
First, we store the target object (line 2). We need to keep a reference to that object to uninstall the breakpoint later. Then, we configure a metalink to send the
#halt message to
#object (lines 3-5). At run time,
#object represents the receiver of the current method that is executing. This instrumentation is equivalent to insert a
self halt instruction at the beginning of the instrumented method. Finally, we use ourReflectivity’s object-centric API (line 6) to install the breakpoint metalink on the target method, but only for the target object.
When that is done,
targetObject will halt whenever it receives the message
methodSelector. All other objects from the system remain unaffected by that new breakpoint.
Using our object-centric breakpoint
Now that we have implemented our object-centric breakpoint, we can use it. We instrument our point
p1 with an object-centric breakpoint on the
setX:setY: method (line 3). We store the instrumenter object in the
instrumenter variable so that we can reuse it later. Calling
p1 will now halt the system, while calling it on
p2 or any other point will not halt.
p1 := 0@2. p2 := 1@3. instrumenter := p1 haltOnCall: #setX:setY:. p1 setX: 4 setY: 2. "<- halt!" p2 setX: 5 setY: 3. "<- no halt"
After debugging, we will probably need to uninstall our breakpoint. As we kept a reference to the instrumenter object, we can use it to change or to remove the instrumentation it defines.
Let’s first define an uninstall method in the
ObjectCentricInstrumenter class. This method just calls the uninstall behavior of the metalink, removing all instrumentation from the target object.
ObjectCentricInstrumenter >> uninstall metalink uninstall
Our little example script now becomes:
point p1 := 0@2. point p2 := 1@3. instrumenter := p1 haltOnCall: #setX:setY:. p1 setX: 4 setY: 2. "<- halt!" p2 setX: 5 setY: 3. "<- no halt" instrumenter uninstall. p1 setX: 4 setY: 2. "<- no halt" p2 setX: 5 setY: 3. "<- no halt"
More object-centric debugging tools!
We showed how to implement a breakpoint that halts when one single, specific object receives a particular message. And it was simple!
But why stopping there? The Pharo reflective tools provide us with much more power! In our next blog posts, we’ll show how we can implement a breakpoint that halts when the state of one specific object is accessed.