An exception is an event that may require special processing by a user
program (or by the underlying implementation). Exceptions may be raised by
the computer's error-detection mechanisms, explicit activations (e.g.,
HALT
and ASSERT
), failed runtime checks, or by actions
external to the program. The pair of modules `Exception' and
`Signal' give the programmer control over the handling of these
exceptions. `Exception' provides the basic exception handling
mechanism for user programs, whereas `Signal' provides the means to
connect signal handlers to that mechanism.
A signal is an externally generated software interrupt delivered to a process (or program). These are generally produced by the underlying operating system as a means of reporting exceptional situations to executing processes. However, it is also possible for one process to signal another process.
The primary use of this module is to provide error handling and to allow
programmer control over both system and language (HALT
and
ASSERT
) exception handling. The programmer may define handlers,
which can be used in place of those defined by the implementation.
There are two states of program execution: normal and exceptional. A
program remains in the normal execution state until an exception is raised
by a call to RAISE
, HALT
, or ASSERT
, or after a failed
run-time check. After that, the program remains in the exceptional
execution state until ACKNOWLEDGE
or RETRY
is called.
An exception affects the control flow of a program; raising an exception implies transfer of control to some sort of handler, even across procedure invocations. An exception handler is a statement sequence that is executed when an exception occurs. In OOC, an exception handler is set up as part of an execution context. Both normal and exceptional execution blocks can be set up within the same procedure, or exceptions can be allowed to propogate up the call stack and handled by a calling procedure. A execution context, and exception handler, is typically set up like
Exception.PUSHCONTEXT (e); IF (e = NIL) THEN (* normal execution *) ELSE (* an exception was raised *) (* handle the exception raised during normal execution *) END; Exception.POPCONTEXT;
Please note: Wherever the word "thread" appears below, it should be read as "program" for the moment. Multi-threading isn't supported yet.
The facilities provided by module `Exception' allow the user to raise
exceptions and query the current execution state (normal or exceptional).
Exceptions are identified uniquely by a pair (Source
, Number
).
The programmer is responsible for managing the stack of exception handlers;
this is because Oberon-2 does not provide direct language support for
exceptions, and therefore exception handling in OOC is done through a
library module. The stack is manipulated primarily through the procedures
PUSHCONTEXT
and POPCONTEXT
. The only other action that
changes the stack is raising an exception while the program is an
exceptional execution state; this pops the topmost context (that was
responsible for the exception) from the stack before moving control to the
then topmost execution context. Raising an exception in the state of normal
execution does not change the stack.
Source
is defined and allocated to establish a particular
set of exceptions.
(VAR source: Source)
NIL
; if the context is later
reactivated by raising an exception, source is set to the exception's
source (see section Exception Examples). At most one context can be pushed per
procedure at a time. During a single procedure evaluation, two successive
calls to PUSHCONTEXT
without a POPCONTEXT
in between are not
allowed and result in undefined program behaviour.
Please note: When the context is activated again (by raising an
exception), the value of non-global variables of enclosing procedures that
were modified after the initial call to PUSHCONTEXT
are undefined.
POPCONTEXT
is called,
the exception is raised again, thereby passing it along to the next higher
exception handler. During the execution of a procedure, the dynamic number
of calls to POPCONTEXT
has to balance the ones to PUSHCONTEXT
.
PUSHCONTEXT
returns again, with the parameter
source set to NIL
. This allows the "normal" part to be
re-executed. Be very careful when using this because all local variables of
the enclosing procedure(s) that were modified after the initial call to
PUSHCONTEXT
are undefined when activating the context again.
If the current thread is in the normal execution state, calling RETRY
raises an exception.
If the current thread is in the normal execution state, calling
ACKNOWLEDGE
raises an exception.
(VAR newSource: Source)
Source
. If an unique
value cannot be allocated, an exception is raised.
(source: Source; number: Number; message: ARRAY OF CHAR)
RAISE
, the context on top of the stack is
activated; if it was already in the exceptional execution state, the stack
is popped before activating the context. Activating the execution context
looks as if the corresponding call to PUSHCONTEXT
returns a second
time, except this time returning with source (of PUSHCONTEXT
)
set to source (of RAISE
) (see section Exception Examples).
Using a value of NIL
for source raises an exception.
The message should have the format "[<module>] <description>
";
it may be truncated by RAISE
to an implementation-defined length.
(source: Source): Number
(VAR text: ARRAY OF CHAR)
(): BOOLEAN
TRUE
; otherwise, it
returns FALSE
.
There are a number of important restrictions on the use of
PUSHCONTEXT
:
PUSHCONTEXT
is undefined if the context is activated again by raising
an exception or calling RETRY
. The reason is that, while the
compiler ensures that the evaluation of a piece of code delivers the correct
results in the end, it does not ensure that the state of an interrupted
computation is correctly reflected by the memory contents at the time of the
interruption.
RAISE
(i.e., failed run-time checks and external signals), the place where the
exception was raised is undefined. That is, the programmer cannot be
certain of the exact set of intructions that were completed before the
exception was raised. The reason is that a sequence of instructions as
specified in the source code may be evaluated in a different order or in an
overlapped fashion in the emitted machine code.
PUSHCONTEXT
must have exactly one matching call to
POPCONTEXT
within the same procedure, assuming that the program parts
in between are completed without raising an exception.
If a stack underflow occurs when POPCONTEXT
is called, an exception
is raised. If an execution context is left on the stack that doesn't
correspond to a valid procedure (i.e., a procedure doing a
PUSHCONTEXT
returns without doing a matching POPCONTEXT
),
activating the context by raising an exception transfers the program into a
completely undefined state. Most likely, the program abort due to a
segmentation violation or a comparable error, or the stack of execution
contexts is rolled back until a valid context is reached. There is no way
to check for such a situation. Any programmer should be aware that an
invalid context stack can cause considerable grief.
Several exception sources are predefined in module `Exception'. These are available for handling exceptions generated through Oberon-2 language constructs and other run-time exceptions.
Source
Source
HALT
and ASSERT
; HALT(n)
is equivalent to
RAISE (halt, n, "")
, and ASSERT(FALSE, n)
to RAISE
(assert, n, "")
.
Source
The source runtime
is used to report failed run-time checks, and the
following exception numbers are associated with it. These numbers signify
the corresponding failed run-time checks, which are described fully in
section Illegal Operations.
NIL
or type test on NIL
.
NEW
was called with a negative length for an open array pointer type.
NEW
could not allocate the requested memory.
RETURN
statement.
CASE
construct, and there is no ELSE
part.
WITH
failed, and there is no ELSE
part.
Typically, one exception source is defined per module. Exception numbers
are then used to distinguish between the actual exceptions raised against
that source. Those exceptions can then be handled either within that module
itself, as is generally the case in OOC Library modules that use
`Exception', or the source and related constants can be exported and
then handled externally. Because exception sources assert
,
halt
, and runtime
are defined within `Exception', failed
assertions, and so forth, can be handled just like any other exception.
The following example is meant to show how to define and use an exception source. Two instances are given where exceptions are raised against that source; note that the exception is handled in only one of these.
MODULE SimpleException; IMPORT Exception, Err; CONST genericException = 1; VAR src: Exception.Source; PROCEDURE RaiseIt; BEGIN Exception.RAISE (src, genericException, "[SimpleException] An exception is raised") END RaiseIt; PROCEDURE HandleIt; VAR e: Exception.Source; BEGIN Exception.PUSHCONTEXT (e); IF (e = NIL) THEN (* normal execution *) RaiseIt ELSE (* an exception was raised *) Err.String ("Caught the exception."); Err.Ln; Exception.ACKNOWLEDGE END; Exception.POPCONTEXT; END HandleIt; PROCEDURE LetItGo; BEGIN RaiseIt END LetItGo; BEGIN Exception.AllocateSource (src); HandleIt; LetItGo; END SimpleException.
The exception source src
is allocated (and initialized) by the call
to AllocateSource
in the body of the module. Procedure
RaiseIt
raises an exception against that source.
In procedure HandleIt
, an exception context is established, and then
any exceptions that are raised in the scope of that context are handled.
Note the use of ACKNOWLEDGE
to indicate the exception was handled,
and POPCONTEXT
to end the context and clean up after it.
In procedure LetItGo
, the raised exception is not handled, so the
exception propagates up the call stack, and finding no enclosing context
handler, finally terminates the program. The output of this program should
look something like
Caught the exception. ## ## Unhandled exception (#1): ## [SimpleException] An exception is raised ##
To identify different exceptions, and provide different handling depending
on the exception raised, both the exception Source
and Number
need to be considered. The pair (Source
, Number
) uniquely
identify the exception that has been raised. For example,
MODULE MultiExcept; IMPORT Exception, Out; CONST genericException = 1; zeroException = 2; negativeException = 3; VAR src: Exception.Source; PROCEDURE RaiseIt; BEGIN Exception.RAISE (src, genericException, "[MultiExcept] An exception is raised") END RaiseIt; PROCEDURE Test (c: INTEGER); BEGIN Out.String ("Testing value="); Out.Int (c, 0); Out.Ln; IF (c = 0) THEN Exception.RAISE (src, zeroException, "[MultiExcept] Value is zero") ELSIF (c < 0) THEN Exception.RAISE (src, negativeException, "[MultiExcept] Value less than zero") ELSE RaiseIt END; END Test; PROCEDURE p (i: INTEGER); VAR e: Exception.Source; str: ARRAY 256 OF CHAR; BEGIN Exception.PUSHCONTEXT (e); IF (e = NIL) THEN Test(i); ELSE IF (e = src) THEN (* identify the exception source *) IF (Exception.CurrentNumber(e) = zeroException) THEN Exception.GetMessage(str); Out.String ("Caught exception: "); Out.String(str); Out.Ln; Exception.ACKNOWLEDGE ELSIF (Exception.CurrentNumber(e) = negativeException) THEN Exception.GetMessage(str); Out.String ("Caught exception: "); Out.String(str); Out.Ln; Exception.ACKNOWLEDGE END; END; (* Note: No ELSE part; *) END; (* all other exceptions are re-raised. *) Exception.POPCONTEXT; END p; BEGIN Exception.AllocateSource (src); p(-4); p(0); p(3); END MultiExcept.
Exception numbers genericException
, zeroException
, and
negativeException
are defined for src
. In procedure p
,
two of these exceptions are handled, and all other exceptions, including
genericException
, are simply re-raised. The output of this program
looks like
Testing value=-4 Caught exception: [MultiExcept] Value less than zero Testing value=0 Caught exception: [MultiExcept] Value is zero Testing value=3 ## ## Unhandled exception (#1): ## [MultiExcept] An exception is raised ##
The previous two examples are somewhat contrived; you probably wouldn't use exceptions quite that way. Those examples were meant to show how the exception mechanisms work, not necessarily how you would use them in a real situation. So for this next set of examples, let us look at a more practical problem. Consider the following module, which performs a typical programming task: reading from one file, processing the information, and writing the result out to another file. Note that, in this version, no error checking is done.
MODULE FileFilter; IMPORT Files, TextRider; PROCEDURE Process(inFileName: ARRAY OF CHAR; outFileName: ARRAY OF CHAR); VAR r: TextRider.Reader; w: TextRider.Writer; fin, fout: Files.File; res: INTEGER; BEGIN fin := Files.Old(inFileName, {Files.read}, res); r := TextRider.ConnectReader(fin); fout := Files.New(outFileName, {Files.write}, res); w := TextRider.ConnectWriter(fout); (* Process the files... *) fin.Close; fout.Close; END Process; BEGIN Process("in.txt", "out.txt"); END FileFilter.
There are a number of places where things might go wrong. For instance, suppose `in.txt' does not exist; running the program would result in the following output:
## ## Unhandled exception (#1) in module TextRider at pos 45930: ## Dereference of NIL ##
Please note: The exception is only raised if `TextRider' was compiled with run-time checks enabled; they are disabled by default. In general, it is not a good idea to assume that library modules raise "proper" exceptions when they are fed illegal values. For instance, nstead of a deref-of-nil exception, they might cause the OS to signal a
SIGSEGV
(or something similar). Some modules (everything implemented in C) cannot be forced to handle run-time checks gracefully at all.
This exception occurs because Files.Old
failed and returned a value
of NIL
, and that value was passed to ConnectReader
. This
situation should be checked for; Oberon-2 provides a predefined procedure
ASSERT
that could be used in this situation. The following version
adds error checking to the program:
PROCEDURE Process(inFileName: ARRAY OF CHAR; outFileName: ARRAY OF CHAR); VAR r: TextRider.Reader; w: TextRider.Writer; fin, fout: Files.File; res: INTEGER; BEGIN fin := Files.Old(inFileName, {Files.read}, res); ASSERT(res = Files.done); r := TextRider.ConnectReader(fin); ASSERT(r # NIL); fout := Files.New(outFileName, {Files.write}, res); ASSERT(res = Files.done); w := TextRider.ConnectWriter(fout); ASSERT(w # NIL); (* Process the files... *) IF fin # NIL THEN fin.Close END; IF fout # NIL THEN fout.Close END; END Process;
Running this program under the same conditions (i.e., `in.txt' does not exist) produces the following result:
## ## Unhandled exception (#1) in module FileFilter2 at pos 299: ## Assertion failed ##
This is slightly better than the first version; at least the unhandled exception message now shows the relative location of the exception in the source text. But, it would be even better, especially if this kind of file processing were done from an interactive program, if there were a way to recover from this situation. The next version shows how failed assertions can be caught:
PROCEDURE Process(inFileName: ARRAY OF CHAR; outFileName: ARRAY OF CHAR); CONST finError = 1; rError = 2; foutError = 3; wError = 4; VAR r: TextRider.Reader; w: TextRider.Writer; fin, fout: Files.File; res: INTEGER; e: Exception.Source; BEGIN fin := NIL; fout := NIL; Exception.PUSHCONTEXT (e); IF (e = NIL) THEN fin := Files.Old(inFileName, {Files.read}, res); ASSERT(res = Files.done, finError); r := TextRider.ConnectReader(fin); ASSERT(r # NIL, rError); fout := Files.New(outFileName, {Files.write}, res); ASSERT(res = Files.done, foutError); w := TextRider.ConnectWriter(fout); ASSERT(w # NIL, wError); (* Process the files... *) ELSE IF e = Exception.assert THEN CASE Exception.CurrentNumber(e) OF finError: (* ... *) Exception.ACKNOWLEDGE | rError: (* ... *) Exception.ACKNOWLEDGE | foutError: (* ... *) Exception.ACKNOWLEDGE | wError: (* ... *) Exception.ACKNOWLEDGE ELSE (* exception is not acknowledged otherwise. *) END; END; (* all other exceptions are re-raised. *) END; Exception.POPCONTEXT; IF fin # NIL THEN fin.Close END; IF fout # NIL THEN fout.Close END; END Process;
When an exception occurs (indicated by a failed assertion) special
processing can be done based on the exception number: finError
,
rError
, foutError
, or wError
. Note that the calls to
Close
occur outside of the exception context, so that the files can
still be closed when an exception occurs (as long as they are not
NIL
). An else clause is included as part of the CASE
to
prevent a misleading noMatchingLabel
exception.
This example shows how the exception mechanism can be used in conjunction
with ASSERT
. If more fine-grained control is required, an exception
source can be defined and calls to RAISE
used in place of
ASSERT
.
The module `Signal' provides the means to connect signals to the exception handling mechanism defined by module `Exception'. A signal reports the occurrence of an exceptional event to an executing program; that is, a signal is an externally generated software interrupt. The following are examples of events that can generate a signal: Program or operation errors, or external events such as alarms or job control events; one process can also send a signal to another process.
Full coverage of the use of signals is beyond the scope of this manual. To learn more about signals, most books on the Unix operating system have sections describing signals. Otherwise, The GNU C Library Reference Manual (available to download in various formats--say, as "info" files--or in print with ISBN 1-882114-53-1) is an excellent source of information on the use of signals.
Signals can also be set up to be handled independently of exceptions. The
procedure SetHandler
is used to install a handler procedure for when
a specific signal occurs. The procedure Raise
can be used to raise a
particular signal, which is then handled in the same way as system generated
signals (i.e., either an exception is raised or the signal's action is
activated).
A signal's action can be set to handlerException
, which means that an
occurance of the given signal raises an exception. Unless specified
otherwise, signals trigger their respective default actions.
A generic handler, which could be used to handle different kinds of signals, might look something like this:
PROCEDURE GenericHandler(sigNum: Signal.SigNumber); VAR dummy: Signal.SigHandler; BEGIN Err.String("Handling signal="); Err.LongInt(sigNum, 0); Err.Ln; dummy := Signal.SetHandler(sigNum, GenericHandler); (* sigNum's action might be reset by the system to * `handlerDefault', so we explicitly reset our own * signal action here. See note below. *) IF sigNum = Signal.Map(Signal.sigint) THEN (* Actions applicable to `sigint'. *) ELSIF sigNum = Signal.Map(Signal.sigsegv) THEN (* Actions applicable to `sigsegv'. * HALT() would probably be good here. *) ELSIF sigNum = ... ... ELSE (* For other signals that have this procedure as their action, * but are not handled by an ELSIF branch, reset the action * as default, and raise it again. *) dummy := Signal.SetHandler(sigNum, Signal.handlerDefault); Signal.Raise(sigNum) END; END GenericHandler;
Please note: Resetting the signal's action to the current handler is only necessary for System V systems. For BSD or POSIX, the current signal handler is kept. Also note that with System V there is a race condition: There is no guarantee that the signal isn't raised again after the handler is cleared by the system, but before the called handler has reinstalled itself.
This handler would be installed for various signals (probably in a module's BEGIN block) as follows:
oldHandler := Signal.SetHandler(Signal.Map(Signal.sigint), GenericHandler); oldHandler := Signal.SetHandler(Signal.Map(Signal.sigsegv), GenericHandler); ...
The following constants define symbolic names for signals. Because signal
numbers vary from system to system, the numbers below cannot be passed
directly to a system call; a number has to be mapped to the system's
numbering scheme first by the function Map
. Multiple names can be
mapped to a single signal number; for example on most systems, the signals
sigiot
and sigabrt
are aliases. Not all signals are available
on all systems. If a signal is not defined for the current system,
Map
will return the value unknownSignal
.
Program error signals:
Termination signals:
Alarm signals:
Job control signals:
Operation error signals:
Miscellaneous signals:
Other:
Map
for invalid signal names.
The following types are declared in module `Signal':
PROCEDURE (signum: SigNumber)
SetHandler
. A procedure variable of
this type is activated upon the arrival of the signal, and the system
dependent signal number is passed to the signum parameter.
The following variables are defined for use with facilities provided in module `Signal':
sigkill
and sigstop
cannot
be ignored.
SetHandler
to
indicate an error.
signum
, the handler
will install itself again as action for the given signal number, and then
activate Exception.RAISE
with Signal.exception
as source, the
message string `[Signal] Caught signal number <signum>', and the system
dependent value of signum
as exception number.
If the exception isn't handled by the user, the default exception handler
will print the usual message to stderr
, reset the signal's handler to
the default action, and raise the signal again. If the latter doesn't
terminate the program, the default handler will terminate the program like a
failed run-time check.
handlerException
.
The following procedures are provided for setting signal handlers and raising signals:
(signum: SigNumber): SigNumber
unknownSignal
is returned. More than one signal may be
mapped onto the same number.
(signum: SigNumber; action: SigHandler): SigHandler
Map
first. The behaviour of this procedure is undefined if
the given number does not correspond to a legal signal.
If the signal can be handled, the next occurence of the given signal will
activate the procedure in action, passing the system specific signal
number via the procedure's signum parameter. Calling this procedure
with action = NIL
is equivalent to calling it with
action = handlerDefault
. The system might, as in the case of
System V systems, reset the signal handler to the default action before
calling action. On other systems, notably POSIX and BSD, the current
action is kept. So, it is generally a good idea to explicitly set the
signal handler again as part of action.
On success, the SetHandler
function returns the action that was
previously in effect for the specified signum. This value can be
saved and later restored by calling SetHandler
again.
On failure, the value handlerError
is returned. Possible errors are an
invalid signum, or an attempt to ignore or provide a handler for the
signals sigkill
or sigstop
.
Please note: In
oo2c
, this function is just a wrapper around the C functionsignal
. For more details, check the specification of this function (e.g., its man page or the relevant chapter of libc info).
(signum: SigNumber)
SetHandler
for the restrictions regarding the values of signum.
Initial actions for all signals within a program are usually either
handlerDefault
or handlerIgnore
. A check should be done when
establishing new signal handlers to be sure that the original action was not
handlerIgnore
.
Example:
MODULE SigTest; IMPORT Signal, ...; VAR oldHandler: Signal.SigHandler; PROCEDURE CleanUp(sigNum: Signal.SigNumber); (* Set the handler back to default, clean up (e.g., close * files), and then resend the signal. *) BEGIN oldHandler := Signal.SetHandler(sigNum, Signal.handlerDefault); (* Do the clean up stuff. *) Signal.Raise(sigNum); END CleanUp; BEGIN oldHandler := Signal.SetHandler(Signal.Map(Signal.sigint), CleanUp); (* Check to make sure the signal was not set to be ignored. *) IF oldHandler = Signal.handlerIgnore THEN oldHandler := Signal.SetHandler(Signal.Map(Signal.sigint), Signal.handlerIgnore); END; ... (* Other program termination signals, like sighup and * sigterm, might also be set to do the CleanUp action. *) END SigTest.
Certain signals might not occur when normal run-time checks are enabled. For example, index checks are normally done when accessing array elements, so a segmentation violation should never occur because of accessing out-of-bounds array elements. However, if these run-time checks are disabled, appropriate signal handlers can be set up to capture error conditions.
Example:
<* IndexCheck := FALSE *> ... PROCEDURE PrintIt(sigNum: Signal.SigNumber); BEGIN oldHandler := Signal.SetHandler(sigNum, PrintIt); Err.String("Resetting program and exiting..."); Err.Ln; (* Cleanup stuff *) HALT(1); END PrintIt; ... oldHandler := Signal.SetHandler(Signal.Map(Signal.sigsegv), PrintIt);
It is often very difficult to recover from serious events that trigger signals. This is why the exception handling module `Exception' has been tied into `Signal'; a program can be set up to handle the error via an exception handler.
Example:
MODULE SigExcept; <* IndexCheck := FALSE *> IMPORT Signal, Exception, ...; VAR oldHandler: Signal.SigHandler; PROCEDURE RunIt; VAR ... e: Exception.Source; BEGIN Exception.PUSHCONTEXT (e); IF (e = NIL) THEN ... (* Normal excecution part *) ELSE IF e = Signal.exception THEN IF Exception.CurrentNumber(e) = Signal.Map(Signal.sigsegv) THEN ... Exception.ACKNOWLEDGE ELSE END END END Exception.POPCONTEXT; END RunIt; BEGIN oldHandler := Signal.SetHandler(Signal.Map(Signal.sigsegv), Signal.handlerException); ... RunIt END SigExcept.
Go to the first, previous, next, last section, table of contents.