In general, the area which holds the local variables of
a particular method or block is
called a stack frame.
In Smalltalk, these are accessible to the
program as objects (of course) and are called contexts.
(In the browser, have a look at the Context
and BlockContext
classes.)
To the programmer, the currently active context (i.e. the one of the
currently executing method or block) is accessible through the pseudovariable
Every context keeps some state for its
method or block (in general: codeObject).
Among others, the interesting things
found in a context are:
Let us see a concrete example;
the following code shows the senders selector when executed:
Of course, mostly debugging utilities within ST/X
use the context chain information; for more details, have a look into
the
To be able to do this, a block keeps a reference to the context of its
defining codeObject - in this case, the context of the anonymous doItmethod.
Knowing this, we now understand, how a block can access variables declared in
its enclosing method: it uses the homeContext reference, to
fetch and store these outer variables.
The above example is even more interesting:
This is one of the most fundamental differences between
Smalltalk and most other programming languages:
If you tried something like the above example in other programming languages
(say C or C++), you'd get a core dump in the best case - or silently invalid
references in the typical case.
This mechanism allows you to pass an exception block down to a called method
and to return from the outer method -
even from within an arbitrary deeply nested calling hierarchy.
The above may seem artificial,
but you use this feature daily in situations like:
The above is called a stack unwind or context unwind.
It is not allowed to return from a method which has already returned
i.e. the following code is illegal (and raises an exception):
You can also perform unwinds manually, via messages sent to a context.
There are situations, where naive unwind operations are critical and
leave the system in a bad mood: consider the case, where your program
wants to perform some dataBase update and has locked a record for doing so:
In general, the above problem arises whenever some state is changed
temporarily and has to be restored later.
To support this, ST/X, ST-80 and others
provide mechanisms to allow a cleanup
action to be defined.
This cleanup action is executed in case of any stack unwinds.
If you browse for senders of these messages, you will find many concrete
examples of unwind-protection in the system.
(browse senders of
All unwind actions are also performed when an exception handler
aborts some computation (i.e. returns) or if
a process is terminates.
Since there is is a non-zero chance of a never ending recursion here (in case of
errors in the unwind actions which are to be unwound again),
the
As a general rule, be careful when coding these unwind actions, and try to
avoid errors there, IFF there is critical state involved (such as dataBase locks).
Notes:
(*2) indirection
(*3) context copying
Cheap blocks are the most CPU friendly during program execution:
they are created at compile time, and there is no overhead
at all during execution.
Copying blocks are created during program execution,
but require no special home context handling.
The CPU overhead is somewhere in-between cheap blocks and full blocks.
Finally, the ``most expensive'' ones are the full blocks,
which force the compiler to create additional check code into the
context-return.
Copyright © 1995 Claus Gittinger, all rights reserved
thisContext.
Via sender
this is the context of the codeObject which is responsible for the execution
of the current codeObject. This context either performed a message send
or a block evaluation which lead into the current codeObject
(and thereby created the current context).
receiver
(methods only)
this is the receiver of the current message.
Within the codeObject, this is usually referred to via the
self
pseudovariable. For blocks, this referres to
the receiver of the method in which the block was defined.
(blockContexts do not refer to the receiver directly;
these access the receiver via their homeContext.)
arguments
(if any)
these are the arguments passed to the codeObject. I.e. either method
or block arguments.
these are the local variables of the codeObject; i.e. either method locals
or block locals.
selector
the selector by which the codeObject was found. For blocks, the selector will
one of the #value...
selectors.
homeContext
(blocks only)
the context of the block- or methodCodeObject in which the block-codeObject
was defined. For nested blocks, the homeContext may refer to another blockContext.
Blocks access outer locals and arguments via the homeContext
chain.
thisContext
, the program can gain quite interesting
things about the current state of execution.
For example, it is possible to find out who was the sender of a message,
the receiver of a message etc.
The debugger uses this to extract the information shown in the walkback list.
The context chain
Whenever a method or block is called for execution, the system automatically
creates a new context object which holds the above state during the
execution.
The individual contexts are linked via the sender
fields
and unlinked when the codeObject returns.
This allows a program to traverse the calling chain - back to the first context
which is where the whole show started.
Of course, every smalltalk process has its own context chain.
We call the last context of inactive processes the suspendedContext
of a process.
or, looking at the sender:
Transcript show:'I am the: '.
Transcript show:thisContext selector.
Transcript show:' method of '.
Transcript show:thisContext receiver class name.
Transcript cr.
a nice application of this is a trace printing facility;
you can define a method in the
Transcript show:'I was invoked by: '.
Transcript show:thisContext sender selector.
Transcript show:' method of '.
Transcript show:thisContext sender receiver class name.
Transcript cr.
Object
class, which outputs
its senders selector and receiver:
and add trace printing to your methods with:
tracePrint
Transcript show:thisContext sender receiver printString.
Transcript show:' '.
Transcript showCR:thisContext sender selector printString
a concrete application of this is found in the
someMethod
self tracePrint.
...
#obsoleteMethodWarning:
method, found in the Object
class.
DebugView
and the ProcessMonitor
classes.
Blocks homecontexts
Consider the case where you define a block in some method, and pass this
as actionBlock to a button:
At first sight, there seems to be nothing special with the above code ...
|myButton aString|
aString := 'hello there'.
myButton := Button label:'foo'.
myButton action:[Transcript showCR:aString].
myButton open
... however, thinking about the above, you will find that the blocks code
references a variable which was declared in an outer scope: the method in which
the block was defined (*1).
This context is called the blocks homeContext
(or sometimes shortly the blocks home).
Since blocks may be declared within other blocks, a blocks homeContext
can be either a methods context or another blocks context.
For blocks within other blocks, we can walk the homeContext chain back to
the context of the method in which
all those blocks are defined;
this is called the blocks methodHome.
I.e. it accesses those variables
indirectly, through the homeContext reference
(*2).
you may have already noticed, that the
doIt method returns after the "myButton open"
.
However, the block is still living (since kept in the button)
and can still reference this dead methods local variable !
in Smalltalk, a context behaves just like any other object, in that it
is not destroyed, IFF there are still
references from other objects to it. (typically, these are blocks)
Technically, the context is moved from the stack area
(which has first-in/last-out behavior) to the normal
object memory, which is garbage collected
(*3).
Some Smalltalk implementations allocate contexts in the object memory right away;
other (older) systems only allow references to outer variables
as long as the outer context is still on the active context chain
(i.e. the corresponing method has not yet returned)
and report an error for the above example.
Long return from a block
Blocks may contain a return statement (^ someValue
).
This return will end the execution of its defining method and force
a return to the sender of this method.
Notice the word: method being boldified; it is NOT
only the block from which we return, but also the defining method.
For example:
in the above, the evaluation of exceptionBlock in
method1
self method2:[^ false].
^ true
!
method2:exceptionBlock
...
self method3:exceptionBlock
...
method3:exceptionBlock
...
someErrorOccurred ifTrue:[
exceptionBlock value
].
...
method3:
will return immediately from method1
.
here, a block gets passed down to the collections
...
aCollection do:[:element |
someConditionWithElement ifTrue:[
^ true
]
].
...
#do:
method, which evaluates it. The return statement found there will
return from your method.
(Now you understand, why a return statements semantic should not be changed
to return from the block instead ...)
Technically, it is similar to a longjmp()
in the C language
or (roughly) to a catch&throw in other programming languages
(well, not exactly; read ``exception handling''
for the real catch&throw mechanism).
In contrast to longjmp()
in C/C++,
all situations concerning pending references
to intermediate contexts (as described above) are handled correctly.
Also, the garbage collector takes care of any objects which were created
in the meanwhile and are no longer in use.
the above block as returned by
...
getABlock
^ [ ^ self]
!
method2
|aBlock|
aBlock := self getABlock.
aBlock value
!
#getABlock
tries to return
from its home context, when evaluated.
In the above example, that home context has already returned.
Stack unwinding
The above unwind operation is performed by code which was compiler
generated; every ^
(return) statement produces this code.
(the debuggers return and abort functions do this).
now, consider the case of an unwind occurring while the database is locked;
noone would ever unlock
...
aDataBase lock.
...
record := aDataBase readRecord.
...
update the information
...
aDataBase writeRecord:record.
...
aDataBase unlock
...
(it makes no difference what the exact reason for the unwind was;
it could be a return statement in a block,
a programmed unwind, or an abort
from a debugger which opened due to an error somewhere)
Other examples where this may happen are:
cursor change to an hourGlass shape during
doIt evaluation, block/unblock interrupts,
changing drawing colors in a view, unlocking a semaphore etc.)
In your program, you have to wrap your statements into a block, and send it
the message #ifCurtailed:
, passing the cleanup
actions as a block argument.
i.e.
...
[
...
do something
...
] ifCurtailed:[
...
cleanup in case of unwinding
...
].
...
With unwind protection, the database example becomes:
Since the cleanup actions are often the same as the last actions
in the block (here: unlocking the dataBase),
there is another more convenient method called
...
[
aDataBase lock.
...
record := aDataBase readRecord.
...
update the information
...
aDataBase writeRecord:record.
...
aDataBase unlock
] ifCurtailed:[
aDataBase unlock
]
...
ensure:
, which evaluates the block
argument in any case (i.e. whether the computation was unwound or
not) and avoids you having to write these actions twice:
These messages used to be called
...
[
aDataBase lock.
...
record := aDataBase readRecord.
...
update the information
...
aDataBase writeRecord:record.
...
] ensure:[
aDataBase unlock
]
...
#valueOnUnwindDo:
and
#valueNowOrOnUnwindDo:
;
they have been renamed in the process of ANSI standardization.
The old messages are still supported, but should be avoided in new code.
#valueOnUnwindDo:
#ifCurtailed:
or:
#valueNowOrOnUnwindDo:
or:
#ensure:
)
Before doing a hard terminate (i.e. really destroying itself and freeing
its resources), every process unwinds its stack and thereby evaluates all
unwind blocks found. This means that the above cleanup is done even if
the process gets terminated (which is very convenient for the programmer ;-).
(see ``exception handling''
and ``working with processes'').
Process
class provides two variations for termination:
The hard terminate can be executed from within a debugger or
an error handler, in case things got messed up very badly.
terminate
)
which does evaluate all unwind actions
terminateNoSignal
)
which does not evaluate them.
However, the state as modified before will not be restored correctly
in this case, and you may have to manually remove locks and/or restore variables;
also, no unwind actions are performed.
(*1) doIt code
This is also true for doIt execution;
here, the compiler
creates a temporary (anonymous) method, and invokes it directly
(without a message send).
Thus, the context-behavior is the same for
regular methods, blocks and doIt evaluations.
this indirection is the reason for block local variables being
slightly faster accessed than outer block or method variables.
not all blocks require the homeContext. ST/X
(and ST-80) distinguish between those that do access
outer context locals or
return through their homeContext (so called full blocks),
those that only access the receiver via self
(so called
copying blocks) and finally those that do not require any
homeContext access at all (so called cheap blocks).
The checking and context moving is only required and performed for
full blocks. Which is the reason that creation and use of fullBlocks
requires slightly more CPU time (and space) overhead than the others.
Doc $Revision: 1.20 $ $Date: 2021/03/13 18:24:51 $