This consists of selected sections from a much longer paper, `TaskMaster'. The longer paper discusses a number of environmental features, including exception handling and lightweight multi-tasking, that can be supported in any C-basedlanguage with a single uniform language extension called `Action Expressions'.

This shorter document discusses exception handling as a topic unto itself. Its final section introduces action expression syntax and semantics as an extension to the Objective-C programming environment.

Exception handling

Consider a subroutine, main(), which calls foo(), which calls bar(). The runtime stack that underlies the C runtime environment extends to record that main has called foo and that foo has called bar. Then it retracts as bar returns to foo and as foo returns to main. The same call/return path is used unconditionally, regardless of whether the subroutines ran successfully or failed because of an exception.

The absence of a way of handling exceptions explicitly, independently from normal processing, is a severe obstacle to software quality and reusability. Since subroutines routines return the same way, regardless of whether they succeed or fail, a method that should return a handle to a new object might instead return an error code to indicate that it could not, perhaps because of insufficient memory. Since any such call might fail, the caller must check every return value, reporting any failures to higher levels with similar means. This is sufficiently tedious that it is neglected, allowing unhandled exceptions to crash the application.

An `exception' is a situation where a computation does not proceed as planned, perhaps because of I/O errors, inability to allocate a new object because of insufficient memory, or defects in the software itself. `Exception handling' is a language/environmental feature that reserves the normal subroutine/message return channel exclusively for normal processing. Routines that return normally can be assumed to have succeeded, since those that fail will return via an independent channel reserved for exceptions. Low-level routines never fail by returning an error code for the caller to decipher, but by `raising an exception'. Higher level routines establish code fragments, `exception handlers', that will receive control if the exception they are to handle is raised by a lower-level routine.

The following shows how exception handling might be done in C, in order to show the limitations of this solution and how these limitations are addressed in TaskMaster. TRY() is a C macro that uses setjmp() to record the machine's register settings before entering the computation that might fail; the foo subroutine.

main() {
        TRY() {
                int v = foo();
                ...normal processing...
        } HANDLE(OUTOFMEMORYEXCEPTION) {
                ...handle low memory exceptions...
        } HANDLE(IOEXCEPTION) {
                ...handle IO failure exceptions...
        } OTHERWISE() {
                ...handle any other exceptions...
        }
}
foo() {
        TRY() {
                int v = bar();
                ...normal processing...
        } HANDLE(OUTOFMEMORYEXCEPTION);
}
bar() {
        id newObject = [SomeClass new];
        ...normal processing...
}
If the foo() subroutine, or any subroutine that it calls, fails because of some exception, it does not return normally. Instead, it `raises an exception', which returns directly to the higher-level routine, by using longjmp() to return directly, bypassing the intervening subroutines altogether. The TRY() macro detects the abnormal return and transfers to the appropriate handler, which handles the exception in some case-specific fashion. By having the TRY() macro manage saved register settings on LIFO stack, exception handlers can be nested so that exceptions are presented to their handlers in deepest-first sequence. The low-level exception handlers may choose to pass control to the next higher-level handler by popping the higher-level registers from the exception handler's stack and calling longjmp() again.

The problems with this scheme will be familiar to anyone who has used it extensively. Since the exception handlers unconditionally execute in the context of the calling routine, as opposed to as a subroutine of the routine that detected the exception, information is unconditionally lost as to what sequence of calls triggered the exception. For example, foo's OUTOFMEMORYEXCEPTION handler can determine that bar, or one of its subroutines, failed because of insufficient memory. But it cannot tell which one, because the stack has been rewound before the handler has been invoked. This makes ituseless for exception handlers to invoke process inspectors (debuggers), sincethe debugging information has been unconditionally discarded.

TaskMaster takes a different approach. It exports the handler to the exception,rather than the other way around. By invoking the exception handler as asubroutine of the exception, the stack is never erased until the user's codehas had a chance to use it. One way to think about the difference is thatTaskMaster exception handlers are first class C subroutines, not compoundstatements as in the preceding example. They are passed as function pointers[22] in the exception handler stack and invoked as subroutinesby the logic for raising an exception.

Action expressions are a compiler-supported enhancement to deal with the factthat code that uses function pointers extensively can be hard to write andnearly impossible to understand later. This extension, which is planned for afuture compiler release and is not yet available, would let this example becoded like this:

main() {
        [{ int v = foo();
                ...normal processing...
        } ifException:{
                if (OUTOFMEMORYEXCEPTION) {
                ...handle low memory exceptions...
                } else if (IOEXCEPTION) {
                ...handle IO failure exceptions...
                } else {
                ...handle any other exceptions...
                }
        }];
foo() {
        [{ int v = bar();
                ...normal processing...
        } ifException: {
                ...handle exceptions...
        }];
}
bar() {
        id newObject = [SomeClass new];
        ...normal processing...
}
The statements that look like C compound statements used as expressions, in{bold braces}[23], are action expressions, which will be described in the next section. Theirrelevance to exception handling is that

* The code to be protected from exceptions, and the code for providing thatprotection, can be written alongside each other in a familiar readable fashion,without the syntactic clutter of working with function pointers directly.

* The compiler will generate the necessary boiler plate automatically,transforming the action expressions into function definitions that theunderlying C compiler knows how to handle. Although actions look syntacticallylike expressions, they are semantically analogous to function pointers that theexception handler can invoke as a subroutine of the exception, as opposed towithin the scope of the calling site.

* TaskMaster's run-time support for this style of exception handling is inplace today, and can be used by following the code generation strategy that theextended compiler will use; coding action instantiation statements by hand.

A final TaskMaster feature is that exception handling is closely integratedwith lightweight multi-tasking. Each task maintains an independent stack ofexception handlers that inherits any exception handlers of its parent task. Forexample, a parent task can provide supply handlers of last resort forexceptions in its subtasks. TaskMaster automatically initializes the root taskwith a default handler for all possible exceptions.

This handler of last resort features an interactive task inspector withinteractive debugging facilities. A programmer can use the inspector to examinethe call history that led to the exception and specify whether to exit theapplication, produce a file suitable for detailed debugging, or to continue inspite of the exception.

The inspector is based on a library of routines that interpret C call historiesin terms that a programmer can easily understand. For example, call historiesare not presented as hexadecimal numbers but in symbolic terms. Subroutine andselector names are spelled out with all arguments decoded. Object referencesare presented in terms of the object's class name and its address, stringpointers are presented in terms of the string's contents, and so forth. Thesedecoding facilities are based on a library of platform-dependent routines inthe TaskMaster PDL. These routines support such platform-dependent operationsas loading the application's symbol table. They also provide the heuristicrules that determine which format should be used in presenting the otherwiseuntyped numbers in a C call stack[24].

Apart from such convenience features, the exception handling is notparticularly dependent on the target platform. Exception handling is based on apair of routines, setjmp() and longjmp(), in the standard[25] C run time library. Nor is it particularly dependent onmulti-tasking, except that bundling exception handling with multi-tasking makesit possible for subtasks to manage exceptions independently from other tasks.Nor is exception handling specific to Objective-C. Its benefits are equallygermane to ordinary C programs. This is another reason why TaskMaster'sexception handling facilities are brought forth through two API's, amessage-based API for users who want to treat exceptions as objects and afunction-based API for those who want to treat them as functions.

Action expressions

Action expressions in Objective-C are related to block expressions in Smalltalk, with differences that adapt to the constraints of stack-based languages like C. Action expressions are a way to express actions, or deferred computations; computations to be written at one place but invoked from another.

The classical examples of a deferred computation is a computation that a collection is to perform on each of its members or that a menu is to perform when selected. As the following examples show, action expressions are generally useful, especially for defining menus, operating on collection members, handling exceptions, and defining lightweight tasks:

[ aMenu str:"Open" action:{ code to do a menu operation}];
[ { code that might fail } ifException:{ code to handle the exception } ];
[ anyCollection do:{ code to be applied to each member} ]; 
[ { code to run as a separate task } forkWith:2,arg1, arg2];
Here is an a simpler example of how action expressions help to express adeferred computation. In this example, a printf() statement is to be passed toa subroutine, bar(), which will execute it at its convenience. Notice that thedeferred computation is written right inside the foo subroutine, just like anyC expression. But it is not to be executed there. The action is passed as anargument to the bar routine, so that bar can decide whether, and when, toinvoke it later.

extern int aGlobal;
foo(aFormal)
        int aLocal;

        ...long, involved computation...

        /* 
         * Pass this action to bar, which may run it later 
         */
        bar({ int formalArg; | printf("...", aGlobal,aFormal, aLocal, formalArg);});

        ...long, involved computation...
}
An action expression can be thought of as any legal C compound statement, usedin a context where an expression is otherwise expected[26]:

* Action expressions are written in-line, exactly like an ordinary C orObjective-C expression. In this case, the action expression is an argument tothe foo subroutine call.

* Action expressions can freely reference any and all data within theexpression's enclosing scope, such as the aGlobal, aFormal, and aLocalvariables in this example.

* Evaluation of an action expression produces an object; aninstance of class Action. For example, the foo subroutine will receive aninstance of class Action as its formal argument, anAction.

* Invocation of an action occurs separately, just as invocation of afunction via a function pointer is separate from the act of defining thefunction that it points to. The bar routine might invoke the printf statementwith the message expression, [anAction value], in which formalArg will be nil.If it invokes it as [anAction value:aValue], formalArg will contain aValue.

* Action expressions can receive formal arguments from the message that invokesthem, such as formalArg in this example. These are analogous to subroutinearguments. If formal arguments are to be used, they are declared in the blockexpression as in this example, where the | is a place-holder foran as-yet-undecided syntactic delimiter.

Action expressions' usefulness becomes most obvious in contrast with how thisexample would be coded in ordinary C, using function pointers:

extern aGlobal;
static dummyFv, dummyLv;
static dummyFn(formalArg) 
{
        printf("...", aGlobal, dummyFv, dummyLv, formalArg);
}
foo(aFormal) {
        int aLocal;

        ...long, involved computation...

        /* 
         * Pass dummyFn to bar as a function pointer 
         * so that bar can run it later 
         */
        dummyFv = aFormal;
        dummyLv = aLocal;
        bar(dummyFn);

        ...long involved computation...
}
This is less desirable than the original code for reasons referred to earlieras syntactic clutter.

* The computation is no longer in-line, inside the foo routine. The reader mustbe extremely careful to see that this code is packaging a printf() statementfor execution by bar().

* The enclosing scope (foo) cannot pass data to the computation in a clean,readable fashion. It must use extraneous global variables, the dummyFv anddummyLv symbols in this example, to export data from its internal scope to theexternal function, dummyFn. These global variables are an obstacle to recursionand a rich opportunity for hard to debug race conditions should foo be used bymultiple tasks.

Objective-C actions have a key restriction that is not present with Smalltalkblocks. The code inside a action accesses copies of the variables thatit references in the enclosing scope. These copies are made when the action isinstantiated, not when it is invoked, as is the case withSmalltalk Blocks. The copies are stored as indexed instance variables insidethe Action object and not in the enclosing scope (foo's stack frame).

When the underlying C function is invoked, these variables are exported to thefunction via a structure pointer, as can be seen in the following example. Forexample, the aGlobal, aFormal, and aLocal variables in this example are copiedwhen the action is created (i.e. just before bar is called), not when thedeferred computation is invoked (inside bar). Objective-C action expressions donot share memory with their enclosing scope, they cannot export changes to theenclosing scope as in Smalltalk. Changes to action variables is ineffective,since the change affects a copy, not the original.

This may have made the simple seem unduly complicated. What is going on herecan seen by examining the C code that the compiler would emit for this example:


static void fn(struct { int aGlobal, aFormal, aLocal; } *c, int formalArg) 
{
        printf("...", c->aGlobal, c->aFormal, c->aLocal, formalArg);
}
foo(aFormal) {
        extern int aGlobal;
        int aLocal;

        ...long, involved computation...

        bar(createAction(fn, 3, aGlobal, aFormal, aLocal));

        ...long, involved computation...

}
Of course, this example was intentionally simplified by choosing an example inwhich everything is of type int. This does not mean that actions will only beusable for integers; just that the adjustments that the compiler would emit forother types seem obvious. Until the compiler has been enhanced to emit thiscode automatically, this example shows what a present-day Objective-C userwould write to use TaskMaster's action-based library facilities today.


[22] Actually, they are passed in the handler stack aspointers to instances of Action, which are objects that hold a function pointerinternally, along with copies of data that the action expression referencesfrom its enclosing scope.

[23] Bold braces are purely a typography convention usedwithin this document to suggest an as-yet-undetermined set of block-delimitertokens. The ideal choice would be ordinary braces, as this would suggest that ablock is a compound statement used as an expression. However it is not yetclear that this could be unambiguously distinguished by a parser.

[24] These routines are highly platform dependent, and are byfar the least portable aspect of TaskMaster's PDL For example, it may prove tobe impractical to provide them, and the task inspector that they support, onat least some RISC machines.

[25] I do not know if these routines are specified by anyformal standard. I only know that they have been present in every C environmentthat I've ever used, and that they can be easily provided in assembly languageif an environment fails to provide them. For example, the contextSwitch()subroutine described in the previous section is nothing more than a setjmp tosave the current task's SP (and other registers) and by a longjmp to load thenew task's SP. In fact, contextSwitch() was originally implemented in preciselythis way. This scheme was replaced with the present only because of nagginguncertainty as to the detailed behavior of setjmp and longjmp on all platforms(does this platform's setjmp save all registers - or only the SP?).

[26] Bold braces are used throughout this document as a standin for an as-yet-undetermined set of block-delimiter tokens. The ideal choicewould be ordinary braces, to suggest that a block is conceptually a compoundstatement used as an expression. The specific tokens have not be decidedbecause it is not yet clear that ordinary braces could be unambiguouslydistinguished by the parser.