rick@tetrauk.UUCP (Rick Jones) (11/19/90)
(This is a re-post of an article I tried to send last week. I don't think it made it to anywhere on the net; if anyone has already received it, I apologise for the duplication.) ------------------------------------------------------------ On one of my favourite subjects (as readers of this newsgroup may have noticed), I have been trying to devise ways to extend the effectiveness of Eiffel's exception handling. As a result, I recently hit on a mechanism for passing arbitrary objects from an exception occurrence to a rescue clause. The idea was remotely (only remotely :-) inspired by the proposed exception handling system for C++. After considering that this could be suitable as publicly contributed software to Eiffel-shelf, etc. the implementation turned out to be so simple that I thought I might as well just post it to the net. What follows is largely my notes on the system (written originally for internal consumption, excuse the verbosity); the source of the two key classes is appended. I would of course like to hear any comments on this from interested parties (before or after trying it). ------------------------------------------------------------ Enhanced programmed exceptions for Eiffel ========================================= Rick Jones version: 1.1 Tetra Ltd. date: 90/11/12 These classes provide an enhanced exception mechanism for programmed exceptions. It provides an extension to the "raise" facility, allowing arbitrary objects to be passed between the raise and trap points of an exception. Exception propagation is also enhanced. The standard "raise" mechanism is used internally, the system is written entirely in conventional Eiffel classes. Classes Only two fundamental classes are involved, these are EXCEP and EXTRAP. EXCEP is a class which provides a basic exception object. The creation of an EXCEP object implicitly raises an exception (i.e. calling such a Create will NOT return to the existing line of code). A class which wishes to trap exceptions resulting from creation of EXCEP objects must inherit from EXTRAP. EXTRAP objects should NOT be instantiated. Note that a class which creates EXCEP objects does not need to inherit from any other class in order to use the facility. Facilities EXTRAP provides a facility for descendants in the form of the feature "last_excep". This should be used by code within a rescue clause, and returns an object reference of type EXCEP. If the exception which causes the rescue clause to be invoked is the result of creating an exception of class EXCEP, "last_excep" will return the EXCEP object which was created at that time. Otherwise it will return a Void. It will also return a Void if called outside a rescue clause. Examples The following code shows examples of using EXCEP and EXTRAP: ------------------------------------------------------------------------------ class CATCH -- test special exception catching inherit STD_FILES ; EXTRAP ; feature Create is local excep: EXCEP ; do -- excep will only be non-void if it was assigned a value -- in the rescue clause, indicating the "retry" condition if excep.Void then -- normal condition try ; else -- we are being re-tried putstring ("Trapped an exception\n") ; -- will exit program end ; rescue -- get the last exception - will only be non-void -- if the exception was of class EXCEP excep := last_excep ; -- retry only if we found such an exception if not excep.Void then retry end ; putstring ("No retry - abort\n") ; end ; try is -- a dummy routine to force an exception, -- of course this need NOT be in the same class -- as the routine which rescues the exception local excep: EXCEP ; do -- create an exception excep.Create ; -- there is no return from the Create above! end ; end ; ------------------------------------------------------------------------------ It should be noted that since creation of an EXCEP object always raises an exception, an EXCEP entity will in general only be non-void if assigned a value (directly or indirectly) via the use of "last_excep". Since this itself only yields a non-void result when used in a rescue clause, EXCEP objects are only ever accessible after they have been trapped following an exception. If EXCEP references are only ever used as local entities, it is impossible to erroneously have an accessible EXCEP object when an exception has not occurred. Inheriting from EXCEP Since this system passes back exception identifiers as objects, it is possible to define more complex exception objects by inheritance. A programmer may define an exception class which inherits from EXCEP, and allow it to contain more complex information regarding the exception. As this is a general requirement, a class STD_EXCEP is provided which contains contain two simple attributes, "code" and "text", whose initial values are supplied in the call to Create. These are arbitrary values to allow the programmer to identify different exceptions. The following is the code of the STD_EXCEP class: ------------------------------------------------------------------------------ class STD_EXCEP -- extended exceptions, standard class -- this class provides an exception object with two attributes, -- an integer and a string; this should suffice for many cases -- where some information needs to be attached to exceptions export code, text, print_exception -- standard features for coding exceptions inherit EXCEP -- general exception class feature code: INTEGER ; -- arbitrary code for exception text: STRING ; -- arbitrary text for exception Create (c_code: INTEGER, c_text: STRING) is -- general create, defining a code & text do code := c_code ; text := c_text ; raisex (Current) ; ensure code = c_code ; text = c_text ; -- an exception is raised, there is no return from Create end ; print_exception( msg: STRING ) is -- Print error code and text on standard error -- with msg appended external fprintf: INTEGER language "C"; local fmt: STRING; res: INTEGER; do if not msg.Void then fmt := "%d: %s: %s\n"; res := fprintf( io.error.file_pointer, fmt.to_c, code, text.to_c, msg.to_c ); else fmt := "%d: %s\n"; res := fprintf( io.error.file_pointer, fmt.to_c, code, text.to_c ); end; end; -- print_exception end ------------------------------------------------------------------------------ It is important that the Create routine of the new class either invokes the inherited (renamed) Create, or calls the inherited feature "raisex", passing "Current" as the parameter. This will ensure that the exception is correctly raised. It must also be the last thing which the Create routine does, as it will not return. When an exception is trapped, reverse assignment (the ?= operator) may be used to establish if the trapped exception is a specific descendant of EXCEP rather than treat it as the general case. This may be done either in the rescue clause, or in the "retry" section of the routine body. A modification of the above example, where STD_EXCEP is raised, is shown below: ------------------------------------------------------------------------------ class CATCH -- test special exception catching inherit STD_FILES ; EXTRAP ; feature Create is local excep: EXCEP ; std_excep: STD_EXCEP ; do -- excep will only be non-void if it was assigned a value -- in the rescue clause, indicating the "retry" condition if excep.Void then -- normal condition try ; else -- we are being re-tried, -- first treat as a general exception putstring ("Trapped an exception\n") ; -- is it a "standard" exception? std_excep ?= excep ; if not std_excep.Void then std_excep.print_exception ; end ; -- will exit program end ; rescue -- get the last exception - will only be non-void -- if the exception was of class EXCEP excep := last_excep ; -- retry only if we found such an exception if not excep.Void then retry end ; putstring ("No retry - abort\n") ; end ; try is -- a dummy routine to force an exception, -- here the exception is a descendant of EXCEP local excep: STD_EXCEP ; do -- create an exception excep.Create (1, "Exception 1") ; -- there is no return from the Create above! end ; end ; ------------------------------------------------------------------------------ Application-specific exception classes may inherit either directly from EXCEP, or from STD_EXCEP, according to the requirements of the design. Propagating an exception It is often a requirement to trap an exception, but rather than retry, propagate the exception back, maybe after doing some cleanup work in the rescue clause. It may also be desirable to change the nature of the exception being propagated, but this should be done without actually raising a new exception within a rescue clause. Exception substitution is required. The EXCEP class handles this completely transparently, allowing a new EXCEP object to be created while in a rescue clause. If the exception being rescued is already of an EXCEP class, then the new exception object is substituted for the previous one and no new exception is raised. This prevents the current rescue clause being re-entered, so avoiding indefinite loops. It should be noted that in this situation the Create routine WILL return, allowing the exception to fall-back in the normal way. The following further adaptation of the previous examples shows this in cascading rescue clauses: ------------------------------------------------------------------------------ class CATCH -- test special exception catching inherit STD_FILES ; EXTRAP ; feature Create is local excep: STD_EXCEP ; -- only handles standard exceptions do -- excep will only be non-void if it was assigned a value -- in the rescue clause - -- this indicates the "retry" condition if excep.Void then -- normal condition try ; else -- we are being re-tried, putstring ("Trapped an exception\n") ; putstring ("code = ") ; putint (excep.code) ; putstring (", text = ") ; putstring (excep.text) ; new_line ; -- will exit program end ; rescue -- get the last exception - will only be non-void -- if the exception conforms to class STD_EXCEP excep ?= last_excep ; -- retry only if we found such an exception if not excep.Void then retry end ; putstring ("No retry - abort\n") ; end ; try is -- a dummy routine to propagate an exception, -- descendants of EXCEP which do not conform to -- STD_EXCEP will be propagated back as the latter local other: SOME_CLASS ; std_excep: STD_EXCEP ; do -- we presume that this call to other results in -- an exception being raised other.Create ; rescue -- see if the exception is of class STD_EXCEP, if not, -- but of class EXCEP , propagate a standard exception -- (an example of reverse assignment in the rescue clause) std_excep ?= last_excep ; if std_excep.Void and not last_excep.Void then -- we propagate a general exception std_excep.Create (0, "Undefined exception") ; end ; -- no retry, but the exception may have been changed end ; end ; ------------------------------------------------------------------------------ Conclusions This mechanism provides a clean and simple way of passing exception objects to rescue clauses, using the standard "raise" mechanism of the language. It should be noted that as well as using "raise" as the exception mechanism, it depends on the current behaviour of the "is_programmer_exception" feature of class EXCEPTIONS. This will never return True outside a rescue clause, since one of the effects of the "retry" statement is to clear the name of the raised exception (the string returned by the "programmer_exception_name" feature). There is a case for not clearing this name on a retry when the simple "raise" exception mechanism is used, but for the purposes of this system the present method is preferable. There is also an interesting issue of the semantics of Create under exception conditions. It is generally agreed that when an exception occurs during object creation the object in question should be inaccessible. This is on the basis that the object can be presumed to be in an invalid state. The current implementation of Eiffel does not in fact behave this way, but it is anticipated that it will in future. However, in this mechanism the exception object being created raises an exception but must continue to exist so that it can be accessed by the rescuing routine. There need not be a real conflict if the different conditions are examined. In the normal case of Create, a reference to the new object is assigned to the entity associated with Create, e.g. the statements: a: A_CLASS a.Create can be considered equivalent to the notion of: a := new (A_CLASS) (a style of syntax used in some other OOPLs). If the creation of the object fails, the important point is that the entity "a" should remain void. The fact that the object has been created and partially initialised will in the normal case result in it being collected by the garbage collector since it is unreachable. If, however, the Create routine of A_CLASS deliberately chooses to store a reference to itself somewhere else then this need not be prevented, even under exception conditions. This is the basis of the EXCEP class, since a reference to the new exception object is stored in a Once CELL, accessible to classes inheriting from EXTRAP, immediately before the exception is raised. In short, the fact that a Create routine never returns a reference for assignment to the invoking entity need not prevent the object existing and being referred to via other routes. ------------------------------------------------------------ Source of EXTRAP: indexing version: "1.2" ; date: "90/11/12" ; class EXTRAP -- an extended exception mechanism -- this class supports trapping of extended exceptions -- and must be inherited from by any class which contains a -- rescue clause to deal with these exceptions inherit EXCEPTIONS -- library exception class feature -- feature {NONE} stored_excep: CELL[EXCEP] is -- a common reference to the last exception once Result.Create ; end ; -- feature {} -- used only by EXCEP descendant classes magic_text: STRING is "EXCEPTIONAL" ; our_rescue: BOOLEAN is -- True if in a rescue clause and -- the trapped exception is one of ours do Result := is_programmer_exception (magic_text) ; end ; -- feature {} -- used only by application descendant classes last_excep: EXCEP is do if our_rescue then Result := stored_excep.item ; end ; end ; end ------------------------------------------------------------ Source of EXCEP: indexing version: "1.2" ; date: "90/11/12" ; class EXCEP -- an extended exception mechanism -- this class provides objects which represent actual exceptions, -- the exception is raised when such an object is created; -- it may be inherited from to yield exception objects with -- special properties according to actual requirements inherit EXTRAP -- exception trap class feature Create is do raisex (Current) ; ensure -- an exception is raised, there is no return from Create end ; -- feature {} -- may be called by descendants raisex (excep: like Current) is -- store and raise the given exception - -- if already rescuing, no raise is called do stored_excep.put (excep) ; if not our_rescue then raise (magic_text) ; end ; end ; end ------------------------------------------------------------ -- Rick Jones Tetra Ltd. Maidenhead, Berks, UK rick@tetrauk.uucp Absence of .signature is not .signature of absence