keith@cecil.UUCP (keith gorlen) (02/25/86)
C++ is a terrific language, but I was a bit disappointed initially that it included no facilities for exception handling -- but wait -- maybe "you can program that yourself"! My favorite exception handling mechanism is the SIGNAL-ENABLE construct of BLISS-11. Its reasonably efficient and flexible, and it seems to have inspired Ada's RAISE-EXCEPTION construct. A similar facility for C++ works as follows: BEGINX // start of exception block statements that might cause an exception to be raised EXCEPTION exception handlers; format same as body of switch statement ENDX // end of exception block As a trivial example: #include <stream.h> #include "exception.h" const int exception1 = 1; void f() { RAISE(exception1); } main() { BEGINX f(); cerr << "This will never happen!\n"; EXCEPTION case exception1: cerr << "exception1 handled\n"; ENDX cerr << "End of program\n"; } will output: exception1 handled End of program Of course, if no exception is raised in the first part of the block, the handlers following the EXCEPTION keyword are not executed. When an exception is raised, the case with the same exception code (if any) is executed. Unless the flow of control is altered by a return, break, continue, etc., execution continues with the first statement after the ENDX whether or not an exception occurred, and whether or not a matching case was found. BEGINX...ENDX blocks may be nested within either part of a BEGINX...ENDX block; i.e., the exception handlers may contain exception blocks. An exception is raised by executing the statement RAISE(code), causing control to pass to the most recently executed exception block. An exception handler may reference the current exception code by the name EXCEPTION_CODE. If no case matches the current exception code, it can be propagated up to the next most recently executed exception block by including the statement: default:RAISE(EXCEPTION_CODE); in the exception block. The shar file attached to the end of this article contains my implementation of this and a test program. I would like to hear people's comments on the following points: 1. Will this implementation work in general, or have I forgotten something? Are there some dire consequences of using setjmp/longjmp in this fashion? Note that destructors will not be called when a block is exited by doing a RAISE. 2. In Ada, if an EXCEPTION block has no handler for the raised exception, the exception is automatically propagated up to the next level. In my implementation, control goes to the statement after the ENDX. I think Ada's way is better. Can anyone think of a way to do this while retaining the capability of having a default error handling case? The problem is that there can only be one default label in a switch statement. 2. In Ada, the EXCEPTION block is within the scope of variables declared in the BEGIN...EXCEPTION part. This seems like a good idea, because the exception handlers may want to examine these variables to decide how to handle the exception. It doesn't seem possible to do this directly in C++, and it is easy to achieve the same effect simply by writing, for example: { int x; BEGINX // do something to x // do something that might raise an error EXCEPTION case <something>: // use x ENDX } But this raises the question of what happens if x is allocated to a register. And is there a way to force stack allocation of an auto variable in C++? --- Keith Gorlen Computer Systems Laboratory Division of Computer Research and Technology National Institutes of Health Bethesda, MD 20892 phone: (301) 496-5363 uucp: {decvax!}seismo!elsie!cecil!keith #! /bin/sh # This is a shell archive, meaning: # 1. Remove everything above the #! /bin/sh line. # 2. Save the resulting text in a file. # 3. Execute the file with /bin/sh (not csh) to create the files: # exception.h # exception.c # xtest.c # This archive created: Tue Feb 25 09:14:38 1986 export PATH; PATH=/bin:$PATH echo shar: extracting "'exception.h'" '(1810 characters)' if test -f 'exception.h' then echo shar: will not over-write existing file "'exception.h'" else sed 's/^ X//' << \SHAR_EOF > 'exception.h' X/* exception.h -- Ada -like exception handler X XAuthor: X K. E. Gorlen X Bg. 12A, Rm. 2017 X Computer Systems Laboratory X Division of Computer Research and Technology X National Institutes of Health X Bethesda, Maryland 20892 X Phone: (301) 496-5363 X uucp: {decvax!}seismo!elsie!cecil!keith X February, 1986 X XFunction: X XDeclarations for Ada -like exception handling. X XModification History: X X*/ X X#ifndef EXCEPTIONH X#define EXCEPTIONH X X#include <setjmp.h> X Xclass ExceptionEnv; Xextern ExceptionEnv* exception_env_stack_top; X Xclass ExceptionEnv { X ExceptionEnv* prev; X int exceptionCode; X jmp_buf env; Xpublic: X ExceptionEnv() { // MUST be inline X prev = exception_env_stack_top; X exception_env_stack_top = this; X exceptionCode = setjmp(env); X } X ~ExceptionEnv() { if (exception_env_stack_top == this) pop(); } X int code() { return exceptionCode; } X void pop() { exception_env_stack_top = prev; } X void raise(int exception); X}; X X#define EXCEPTION_CODE exception_environment.code() X X#define BEGINX { \ X ExceptionEnv exception_environment; \ X if (EXCEPTION_CODE == 0) { \ X X// Statements in the scope of this exception handler block go here. X X#define EXCEPTION \ X } \ X else switch(EXCEPTION_CODE) { \ X X/* XException handlers go here; the syntax is that of a switch statement Xbody. The exception code that caused this EXCEPTION block to be entered Xmay be accessed via the macro EXCEPTION_CODE. The statement X"default:RAISE(EXCEPTION_CODE);" will propagate the current exception up Xto the next exception handler block if the exception is not handled by Xthis block; otherwise, execution continues with the first statement Xafter this exception block. X*/ X X#define ENDX \ X }; \ X} \ X Xinline void RAISE(int exception) X{ X exception_env_stack_top->raise(exception); X} X X#endif SHAR_EOF if test 1810 -ne "`wc -c < 'exception.h'`" then echo shar: error transmitting "'exception.h'" '(should have been 1810 characters)' fi fi # end of overwriting check echo shar: extracting "'exception.c'" '(838 characters)' if test -f 'exception.c' then echo shar: will not over-write existing file "'exception.c'" else sed 's/^ X//' << \SHAR_EOF > 'exception.c' X/* exception.c -- Ada -like exception handler library routines X XAuthor: X K. E. Gorlen X Bg. 12A, Rm. 2017 X Computer Systems Laboratory X Division of Computer Research and Technology X National Institutes of Health X Bethesda, Maryland 20892 X Phone: (301) 496-5363 X uucp: {decvax!}seismo!elsie!cecil!keith X February, 1986 X XFunction: X XRun-time support for Ada -like exception handling. X XModification History: X X*/ X#include <stream.h> X#include <osfcn.h> X#include <libc.h> X#include "exception.h" X XExceptionEnv* exception_env_stack_top; XExceptionEnv lastResort; X Xvoid ExceptionEnv::raise(int exception) X{ X if (exception == 0) { X cerr << "Tried to RAISE exception code 0\n"; X abort(); X } X if (prev == 0) { // i.e., this == &lastResort X cerr << "Unhandled exception code " << exception << "\n"; X exit(1); X } X pop(); X longjmp(env,exception); X} SHAR_EOF if test 838 -ne "`wc -c < 'exception.c'`" then echo shar: error transmitting "'exception.c'" '(should have been 838 characters)' fi fi # end of overwriting check echo shar: extracting "'xtest.c'" '(1298 characters)' if test -f 'xtest.c' then echo shar: will not over-write existing file "'xtest.c'" else sed 's/^ X//' << \SHAR_EOF > 'xtest.c' X#include "exception.h" X#include <stream.h> X Xenum exceptionCode { EXCEPTION1=1, EXCEPTION2, EXCEPTION3, EXCEPTION4 }; X Xvoid x(exceptionCode n) X{ X BEGINX X if (n>EXCEPTION2) { X cerr << "Raising EXCEPTION" << n << "..."; X RAISE(n); X } X cerr << "Trying normal return from function\n"; X return; X EXCEPTION X case EXCEPTION3: cerr << "EXCEPTION3 handled\n"; return; X default: cerr << "trying RAISE(EXCEPTION_CODE)..."; X RAISE(EXCEPTION_CODE); X ENDX X} X Xmain() X{ X cerr << "Begin exception handler test\n"; X X BEGINX X cerr << "Testing normal execution\n"; X EXCEPTION X default: cerr << "This should not happen!\n"; X ENDX X X BEGINX X cerr << "Raising EXCEPTION1..."; X RAISE(EXCEPTION1); X cerr << "EXCEPTION1 not handled!\n"; X EXCEPTION X case EXCEPTION1: cerr << "EXCEPTION1 handled\n"; X ENDX X X BEGINX X cerr << "Testing nested exception block\n"; X x(EXCEPTION2); X BEGINX X cerr << "Raising EXCEPTION2..."; X RAISE(EXCEPTION2); X cerr << "EXCEPTION2 not handled!\n"; X EXCEPTION X case EXCEPTION2: cerr << "EXCEPTION2 handled\n"; X cerr << "Raising EXCEPTION3..."; X x(EXCEPTION3); X ENDX X X cerr << "Raising EXCEPTION4..."; X x(EXCEPTION4); X cerr << "EXCEPTION4 not handled!\n"; X X EXCEPTION X default: cerr << "Test unhandled exception handler\n"; X RAISE(EXCEPTION_CODE); X ENDX X} SHAR_EOF if test 1298 -ne "`wc -c < 'xtest.c'`" then echo shar: error transmitting "'xtest.c'" '(should have been 1298 characters)' fi fi # end of overwriting check # End of shell archive exit 0 -- --- Keith Gorlen Computer Systems Laboratory Division of Computer Research and Technology National Institutes of Health Bethesda, MD 20892 phone: (301) 496-5363 uucp: {decvax!}seismo!elsie!cecil!keith
rose@think.ARPA (John Rose) (02/27/86)
(Note: I can't tell if I'm getting onto the net. No one commented on my previous posting on lambda expressions and expression-valued blocks in C (net.lang.c), so I couldn't tell if I'd gotten through. If there is no comment on this paper too, could some kind moderator-type please e-mail me a pat on the back saying it was at least broadcasted?) > 1. Will this implementation work in general, or have I forgotten > something? Are there some dire consequences of using setjmp/longjmp in > this fashion? Note that destructors will not be called when a block is ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > exited by doing a RAISE. This is a bug in longjmp() which is easily fixed with a little conspiracy with the C++ compiler. A guarantee of de-initialization which only applies to local exits is not much of a guarantee. For example, I would like to write an open-file abstraction which ensures a close at the end of its use. I use a stdio FILE instead of a stream to make explicit the resource management: class open_file { FILE* fp; public: open_file(char *name, char *mode = "r") { fp = fopen(name, mode); } ~open_file() { fclose(fp); } operator FILE*() { return fp; } }; void load_file(char *name) { open_file in(name); // do something with the stream, e.g.: extern lispval read(FILE *), eval(lispval); lispval l; while (l = read(in)) (void)eval(l); // ...and when open_file goes out of scope, the right things happen } Implementationally, an "open_file" is just a FILE pointer, with guaranteed de-initialization tacked on. The guarantee should have the force of a Lisp UNWIND-PROTECT, or else the open_file abstraction is much less usable. And if we don't have a garbage collector to manage storage behind our backs, free store management requires such guarantees also. (What other examples of guaranteed deinitialization are there? Do they require UNWIND-PROTECTs? And is the use of non-local exits limited to a few types of application, such as user interfaces and language interpreters? Can you give an example of an application where non-local exits are inconceivable, or where we don't care about non-local deinitialization?) Here is some C code which would be a plausible translation of the above, including UNWIND-PROTECTs. You may have seen this technique in Betz's XLISP, which is written in C. /* #include <longjmp.h>: */ struct catch { void (*catch_fn)(); void *catch_arg; struct catch *catch_prev; }; extern struct catch _thread; /* end include */ struct open_file { FILE* fp; }; void deinit_open_file(this) void *this; { fclose(((struct open_file *)this)->fp); } void load_file(name) char *name; { struct open_file in; extern lispval read(), eval(); lispval l; // Establish a catch block: struct catch deinit_in; deinit_in.catch_fn = deinit_open_file; deinit_in.catch_arg = (void *)∈ deinit_in.catch_prev = _thread; _thread = &deinit_in; in.fp = fopen(name, "r"); while (l = read(in.fp)) (void)eval(l); _thread = _thread->catch_prev; fclose(in.fp); // inline exp. of deinit_open_file } Clearly, there is an expense in establishing the catch block: Four extra accesses to a new structure on the stack, and two accesses to an external variable are needed for each creation/deletion of a protected object; an out-of-line delete function must also be compiled. But this expense is not incurred for classes without guaranteed deinitialization, and perhaps it is reasonable to declare that any class which needs guaranteed deinit. should need it consistently for non-local exits. Also, particular architectures such as the Vax allow cheap establishment of unwind handlers (one write to the stack frame). A compromise: After the signature is an outline of a library class which provides unwind protection, and some applications, including file closing. The idea is that objects of class "catch" are ALWAYS deinitialized, even by longjmp(), and so if you include such an object as one of your members, or as a local variable, you can get a hook on stack unwinds. ---------------------------------------------------------- John R. Rose, Thinking Machines Corporation, Cambridge, MA 245 First St., Cambridge, MA 02142 (617) 876-1111 X270 rose@think.arpa ihnp4!think!rose class catch { static catch* thread = 0; catch* prev; virtual void spring() { /* your code here */ } friend void _unwind_catches(void *); public: catch() { prev = thread; thread = this; auto_check(this); } ~catch() { thread = prev; spring(); } }; void _unwind_catches(void *upto) // Normally only called on { for (catch *cur; (void *)(cur = thread) < upto; ) { thread = cur->prev; // same as ~catch(). cur->spring(); } } /* Just before longjmp unwinds each stack frame, * it calls _unwind_catches() with the address of that frame. */ class generic_catch : catch { void (*catch_fn)(); void *catch_arg; void spring() { catch_fn(catch_arg); } public: generic_catch(void (*fn)(), void *arg = 0) { catch_fn = fn; catch_arg = arg; } } class catch_restart : catch { jmp_buf from_the_top; // HACK! To jump out of the unwind. inline catch_restart() { flag = setjmp(from_the_top); } virtual int restart_p() { return 1; } // Predicate for when to restart. void spring() { if (restart_p()) longjmp(from_the_top, 1); } public: int flag; } // Here is class open_file: class open_file { friend file_catch; class file_catch : catch { void spring() // depends on being 1st in struct: { fclose(((open_file *)this)->fp); } } cb; FILE* fp; public: open_file(char *name, char *mode = "r"); // No destructor: handled by member cb. }; -- ---------------------------------------------------------- John R. Rose, Thinking Machines Corporation, Cambridge, MA 245 First St., Cambridge, MA 02142 (617) 876-1111 X270 rose@think.arpa ihnp4!think!rose
keith@cecil.UUCP (keith gorlen) (03/02/86)
A couple of comments/questions on John Rose's technique described in <4431@think.ARPA> for guaranteeing de-initialization of objects in C++ when longjmp is used: 1) To use the example of class open_file, what if one writes { open_file f1("foo"); open_file f2("bar"); ... } and the block is exited normally; i.e., not by means of longjmp. Does (should) C++ guarantee that the destructors will be called in the opposite order that the constructors were called? If not, the catch frame list might be corrupted. 2) There is also a problem if one does open_file* f1 = new open_file("foo"); The catch frame constructor should probably check for this==0 before linking the frame to the catch frame list. 3) Please explain why spring() must be first in struct: class open_file { friend file_catch; class file_catch : catch { ==> void spring() // depends on being 1st in struct: { fclose(((open_file *)this)->fp); } } cb; FILE* fp; I think this is a very useful concept. One could even define useful classes whose functions are accomplished solely by constructor/destructor functions; for example, a class CriticalRegion might be defined with a constructor that did a WAIT on a specified semaphore and a (guaranteed) destructor that did a SIGNAL on the same semaphore. Declaring a CriticalRegion object at the beginning of a block would then assure that the semaphore was SIGNALled no matter how the block were exited: { CriticalRegion foo(some_semaphore); ... } -- --- Keith Gorlen Computer Systems Laboratory Division of Computer Research and Technology National Institutes of Health Bethesda, MD 20892 phone: (301) 496-5363 uucp: {decvax!}seismo!elsie!cecil!keith
rose@think.ARPA (John Rose) (03/03/86)
In article <56@cecil.UUCP> keith@cecil.UUCP (keith gorlen) writes: >A couple of comments/questions on guaranteeing de-initialization of objects in C++ >when longjmp is used: >1) To use the example of class open_file, what if one writes > { open_file f1("foo"); > open_file f2("bar"); ... } >and the block is exited normally; i.e., not by means of longjmp. Does (should) >C++ guarantee that the destructors will be called in the opposite order that >the constructors were called? "Does": I can only find such a guarantee for static objects (C++ book, p. 158). "Should": Since a declaration/initialiation is guaranteed to be executed after textually preceding decls, it may in general depend on the consistency those preceding objects. It would be a mistake to allow destruction of those objects before the object which relies on them is destroyed. In every other case in C++, initialization and destruction nest properly (statics, base/derived orderings; member object ordering "undefined"). Bjarne, are you silent about auto objects to allow implementations room for some efficiency hack? >2) There is also a problem if one does > open_file* f1 = new open_file("foo"); Yes, allocating catch blocks on the heap has little meaning at best. As you say, they should not be added to the dynamic context. >3) Please explain why spring() must be first in struct: > class open_file { > friend file_catch; > class file_catch : catch { >==> void spring() // depends on being 1st in struct: > { fclose(((open_file *)this)->fp); } > } cb; > FILE* fp; Oops, an unclear comment. The "file_catch" object must be the first member fo the "open_file" object, because of the cast hackery by which the self-pointer of the member object is converted to the self-pointer of the parent object. > { CriticalRegion foo(some_semaphore); ... } Lovely. And why not allow "foo" to be omitted? (Well, that's already defined, and has the wrong scoping.) Now, the only thing left to ensure is that the block can have multiple exits. Have you seen Unix* code like this? for (;;) { int s = spl6(); // Critical section... if (time_to_break()) { splx(s); break; } // More stuff. splx(s); // Do something noncritical. } (E.g., the timeout routine in 4.2.) The dual cleanups are an eyesore, confusing, and bug-prone. How about: for (;;) { { CriticalRegion access_timer_queue(6); // Critical section... if (time_to_break()) break; // More stuff. } // Do something noncritical. } The C++ spec currently disallows the dual construction: Multiple block entries, and the book doesn't say anything about multiple exits either. Since the two constructions are equivalent in implementation difficulty (both require either a switch or duplication of code), I suggest that both be allowed. Imagine how clean a kernel could be if it were written well in C++. (Or C for that matter :-}.) -- ---------------------------------------------------------- John R. Rose, Thinking Machines Corporation, Cambridge, MA 245 First St., Cambridge, MA 02142 (617) 876-1111 X270 rose@think.arpa ihnp4!think!rose