[prev] [up] [next]

Contexts, Stacks and Unwinding

Contents

Introduction

In Smalltalk/X (like in some other Smalltalk implementations and most other programming languages), variables which are declared locally in a method or block, are created in a memory area called the stack.
Since most machines provide hardware support (i.e. machine instructions) to support efficient manipulation of the stack, this makes allocation and deallocation of memory in this stack highly efficient.

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.)

Contexts

To the programmer, the currently active context (i.e. the one of the currently executing method or block) is accessible through the pseudovariable thisContext.

Every context keeps some state for its method or block (in general: codeObject). Among others, the interesting things found in a context are:

Via 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.

Let us see a concrete example; the following code shows the senders selector when executed:

    Transcript show:'I am the: '.
    Transcript show:thisContext selector.
    Transcript show:' method of '.
    Transcript show:thisContext receiver class name.
    Transcript cr.
or, looking at the sender:
    Transcript show:'I was invoked by: '.
    Transcript show:thisContext sender selector.
    Transcript show:' method of '.
    Transcript show:thisContext sender receiver class name.
    Transcript cr.
a nice application of this is a trace printing facility; you can define a method in the Object class, which outputs its senders selector and receiver:
    tracePrint
	Transcript show:thisContext sender receiver printString.
	Transcript show:' '.
	Transcript showCR:thisContext sender selector printString
and add trace printing to your methods with:
    someMethod
	self tracePrint.
	...
a concrete application of this is found in the #obsoleteMethodWarning: method, found in the Object class.

Of course, mostly debugging utilities within ST/X use the context chain information; for more details, have a look into the 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:
    |myButton aString|

    aString := 'hello there'.

    myButton := Button label:'foo'.
    myButton action:[Transcript showCR:aString].
    myButton open
At first sight, there seems to be nothing special with the above code ...
... 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).

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.
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.

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.
I.e. it accesses those variables indirectly, through the homeContext reference (*2).

The above example is even more interesting:
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 !

This is one of the most fundamental differences between Smalltalk and most other programming languages:

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.

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.

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.

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.
For example:

    method1
	self method2:[^ false].
	^ true
    !

    method2:exceptionBlock
	...
	self method3:exceptionBlock
	...

    method3:exceptionBlock
	...
	someErrorOccurred ifTrue:[
	    exceptionBlock value
	].
	...
in the above, the evaluation of exceptionBlock in method3: will return immediately from method1.

The above may seem artificial, but you use this feature daily in situations like:

    ...
    aCollection do:[:element |
	someConditionWithElement ifTrue:[
	    ^ true
	]
    ].
    ...
here, a block gets passed down to the collections #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 ...)

The above is called a stack unwind or context unwind.
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.

It is not allowed to return from a method which has already returned i.e. the following code is illegal (and raises an exception):

    ...

    getABlock
	^ [ ^ self]
    !

    method2
	|aBlock|

	aBlock := self getABlock.
	aBlock value
    !
the above block as returned by #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.

You can also perform unwinds manually, via messages sent to a context.
(the debuggers return and abort functions do this).

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:

    ...
    aDataBase lock.
    ...
    record := aDataBase readRecord.
    ...
    update the information
    ...
    aDataBase writeRecord:record.
    ...
    aDataBase unlock
    ...
now, consider the case of an unwind occurring while the database is locked; noone would ever 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)

In general, the above problem arises whenever some state is changed temporarily and has to be restored later.
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.)

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.
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:
    ...
    [
	aDataBase lock.
	...
	record := aDataBase readRecord.
	...
	update the information
	...
	aDataBase writeRecord:record.
	...
	aDataBase unlock
    ] ifCurtailed:[
	aDataBase unlock
    ]
    ...
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 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:
    ...
    [
	aDataBase lock.
	...
	record := aDataBase readRecord.
	...
	update the information
	...
	aDataBase writeRecord:record.
	...
    ] ensure:[
	aDataBase unlock
    ]
    ...
These messages used to be called #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.

If you browse for senders of these messages, you will find many concrete examples of unwind-protection in the system. (browse senders of #valueOnUnwindDo: #ifCurtailed: or: #valueNowOrOnUnwindDo: or: #ensure: )

All unwind actions are also performed when an exception handler aborts some computation (i.e. returns) or if a process is terminates.
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'').

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 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.
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.

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:
(*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.

(*2) indirection
this indirection is the reason for block local variables being slightly faster accessed than outer block or method variables.

(*3) context copying
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.

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

<cg at exept.de>

Doc $Revision: 1.20 $ $Date: 2021/03/13 18:24:51 $