Querying the runtime
TL;DR
This page shows how we can query the run-time stack of execution contexts using the thisContext
keyword.
What is a context?
A
context
holds all the dynamic state associated with the execution of a method or a block. At run time, this information exists in the virtual machine, not in the image. This means that we need to
reify
a context in order to query and manipulate it. This is achieved using the thisContext
keyword, which reifies the current execution context.
If you inspect the following snippet, you will obtain a reified instance of the execution context which you can explore.
thisContext.
This is an instance of Context
. Each context has compiled code (the getter is method
or compiledCode
).
thisContext method.
Each context also has a sender, which is another context:
thisContext sender.
The chain of senders forms a stack of execution contexts.
thisContext stack.
Contexts are especially useful for accessing information that can only be obtained at runtime.
Accessing the current method
A simple usage of thisContext
is to obtain the compiled code of the currently executing method:
thisMethodFromContext <gtExample> | method | method := thisContext method. self assert: method selector equals: #thisMethodFromContext. ^ method
Accessing temporaries
A context holds the entire execution state of a running method. For example, you can access the values of temporary variables by sending tempNamed:
thisContext "NB: as soon as we return the context, it will be dead, so the values of variables will no longer be visible." <gtExample> | context foo | foo := 42. context := thisContext. self assert: context isDead not. self assert: context method selector equals: #thisContext. self assert: (context tempNamed: #foo) equals: 42. ^ context
Caveat:
If you inspect the example above, the variables will all be nil
. This is because the context will be
dead
after being returned.
thisContextIsDead <gtExample> | context | context := self thisContext. self assert: context isDead. ^ context
If instead we make a copy of the context before returning it, we will be able to still see the variable values.
thisContextCopy "Here we return a copy of the context, which will contain all the temporary variables with their values at the point of return." <gtExample> | context foo | foo := 42. context := thisContext. self assert: context isDead not. self assert: context method selector equals: #thisContextCopy. self assert: (context tempNamed: #foo) equals: 42. ^ context copy
thisContextCopyIsNotDead <gtExample> | context | context := self thisContextCopy. self assert: context isDead not. ^ context
Accessing the stack
You can obtain the full run-time stack by sending stack
to thisContext
, or you can ask for just a stack up to a certain size.
thisStack <gtExample> <noTest> | stack | stack := thisContext stackOfSize: 10. self assert: stack size equals: 10. self assert: stack first method selector equals: #thisStack. ^ stack
Note that at run time, the stack we obtain will be live, but when we inspect the example above, we are just seeing a stack of dead contexts. We can copy all the contexts to get a live stack with their full states.
thisStackCopy "Return a copied stack of live contexts." <gtExample> <noTest> | stack | stack := thisContext stackOfSize: 10. self assert: stack size equals: 10. self assert: stack first method selector equals: #thisStackCopy. ^ stack collect: #copy
Finding senders
We can walk up the stack to find an earlier method of interest, for example, an <gtExample>
that directly or indirectly invokes the current method, such as ContextExamples>>#findExampleInStack
Here's a sample usage:
thisMethodFoundDeeplyInStack <gtExample> | method | method := self searchIndirectly. self assert: method selector equals: #thisMethodFoundDeeplyInStack. ^ method
Conditional breakpoints
Searching for an earlier method in the stack can be interesting for setting conditional breakpoints. Here is an example of a conditional halt: HaltDemo>>#haltIfCalledFrom:
Since foo
is not called from bar
here, it will not halt.
foo "Does not halt" <gtExample> self haltIfCalledFrom: #bar. ^ 'foo'
On the other hand, bar
calls foo
in this example, so here it will halt.
bar "Will halt" <gtExample> <noTest> ^ self foo , 'bar'
Finding defining methods
Here is a practical example of using thisContext
to find defining methods for custom tools.
In GT, you can ALT-click on a View or an Action to see its defining method. This works pretty much as we have seen. When a View is created, it collects its defining method by searching through the stack for a method with the <gtCollector
> pragma.
Have a look at GtPhlowDefiningMethodsCollector>>#collect
to see how it's done.
For a sample usage, see GtPhlowProtoView>>#list
, which invokes the defining method collector when a list view is created.
The <gtCollector>
pragma is used in GtPhlowView>>#on:perform:
, which performs the view methods. This way the collector starts where the factory method is called, and stops where the view method is performed.