[moon] home
IPv4

Erlkönig: Ad-Hoc Exceptions in C

Ad-hoc exceptions and RAII in C
parent
[parent webpage]

server
[webserver base]

search
[search erlkonig webpages]

trust
[import certificates]


homes
[talisman]
[zoion]

This is an article section I wrote for Wikipedia's article Resource Acquisition is Initialization in 2010-10. A direct link to the code is provided, as well as to the directory of all the snippets that were involved.

Ad-Hoc Mechanisms

Languages with no direct support for defining constructors and destructors sometimes still have the ability to support ad-hoc RAII methods using programmer idioms. C, for example, lacks direct support for these features, yet programming languages with such support are often implemented in C, and fragile support can be achieved in C itself, although at the cost of additional cognitive overhead on the part of the programmer.

The lack of any standardized idiom for dynamic object allocation beyond malloc and free can be addressed numerous ways, such as through a programming idiom of having TNew, TDelete, and Tmethod functions for any given type T. The ability of throw to do non-local jumps can be simulated under Unix with the setjmp/longjmp library functions, and the syntactic sugar needed to make the result readable can be injected using the C preprocessor. A C compiler supporting variadic macros and in-expression variable declarations, like gcc, makes code like the following possible - although needing to go outside of C proper (the macros are actually textual substitutions performed by the preprocessor) causing difficulty both in maintenance and especially in interpreting debugger results.

#include <stdio.h>
#include <malloc.h>
#include <setjmp.h>
 
jmp_buf       _jmp_bufs[100]; /* avoid recursion with funcs using try/RAII */
int           _jmp_i = 0;       /* _jmp_bufs[_jmp_i] is the next to use */
const char   *_jmp_text = 0;
 
/* a variant of Catch without "break;" might be useful for partial cleanups */
#define Try        do{ switch( setjmp(_jmp_bufs[_jmp_i++]) ){ case 0: while(1){
#define Throw(x)       (longjmp(_jmp_bufs[--_jmp_i], x))
#define ThrowText(x,s) (_jmp_text = s, longjmp(_jmp_bufs[--_jmp_i], x))
#define CatchText()    (_jmp_text )
#define Catch(x)       break; case x:
#define Finally        break; } default:
#define Done           } }while(0)
 
enum { ExceptionOpen = 1 /* must be =1 */, ExceptionWrite, ExceptionClose };

/* Relies on GCC's option "-std=c99" and variadic macro capability.
 * Avoid using 'return', 'break', 'goto' to escape RAII blocks early!
 * Without the exception handling, this macro would be simpler:
 *   #define RAII(t, v, a...) for(t *v = t ## New(a) ; v ; t ## Delete(&v))
 */
#define RAII(t, v, a...) \
    for(int _outer_once = 1, _inner_once = 1, _jumped = 0 ; _outer_once-- ; \
        _jumped && (Throw(_jumped), 1), 1)                              \
        for(_jumped = setjmp(_jmp_bufs[_jmp_i++]) ; _inner_once-- ; )   \
            for( t *v = t ## New(a) ; v ; t ## Delete(&v))
 
typedef struct _File { FILE *fp; } File;

void FileDelete(File **faddr) {
    if((*faddr)->fp)
        if(EOF == fclose((*faddr)->fp))
            Throw(ExceptionClose);
    free(*faddr);;
    *faddr = 0;
}
File *FileNew(const char *pathname, char *mode) {
    File *f = 0;
    if(f = (File*)calloc(1,sizeof(FILE))) {
        if( ! (f->fp = fopen(pathname, mode))) {
            FileDelete(&f);
            ThrowText(ExceptionOpen, pathname);
        }
    }
    return f;
}
void FilePutc(File *f, int c) {
    if(EOF == fputc(c, f->fp)) Throw(ExceptionWrite);
}
int FileGetc(File *f) { return fgetc(f->fp); }

int main(int ac, char **av) {
    Try {
        RAII(File, from, "file-from", "r+") { /* r+ to ban opening dirs */
            RAII(File, to, "file-to", "w") {
                int c;
                while(EOF != (c = FileGetc(from)))
                    FilePutc(to, c);
                puts("copy complete");
            }
        }
    }
    Catch(ExceptionOpen)  { printf(">>> open error on \"%s\"\n", CatchText()); }
    Catch(ExceptionClose) { puts(">>> close error!"); }
    Catch(ExceptionWrite) { puts(">>> write error!"); }
    Finally { puts("finally :-)"); } Done;
}

There are many different, often partial, solutions to the problem, such as handling object cleanup with goto[3] (violating standard practice[4]), code that creates objects for the exceptions[5], or addresses multithreading issues [6]. The real weakness of not having direct language support for RAII features is in situations where code from multiple sources is being combined, and all the ad-hoc methods being used fail to interact properly, or worse yet, directly conflict with each other.

Additionally, in languages where the common idiom doesn't include exceptions, there can be significant resistance from the entrenched community to write code like the example above, instead of the superficially more compact, but unscalable, code below.

#include <stdio.h>
#include <string.h>
int main(int ac, char **av) {
    int succp = 0;
    FILE *from = 0, *to = 0;
    if(    from = fopen("file-from", "r+")) {
        if(to   = fopen("file-to",   "w")) {
            int c;
            while(EOF != (c = fgetc(from)))
                if(EOF == fputc(c, to)) {
                    perror("fputc");
                    break;
                }
            puts("copy complete"), succp = 1;
            if(EOF == fclose(to))
                perror("fclose -to-");
        } else perror("fopen -to-");
        if(EOF == fclose(from))
            perror("fclose -from-");
    } else perror("fopen -from-");
    return succp ? 0 : -1;
}

In such code, error handling now dominates the main function, error output must now be interpreted by the user rather than providing convenient catch hooks. Further, if the same style were converted to library code, those functions would now be rife with uninternationalized text strings. All of these factors tend to inhibit the implementation of sufficient error handling by programmers; it is common to ignore return values from fputc, and fclose, in particular, despite the fact that the failure of fputc could be due to a filesystem being full, and proceeding blithely could result in catastrophic data loss (a problem once exhibited by the compress program, leading to undesired, 100% file compression).

The biggest with ad-hoc mechanisms, even for projects where mixing with other ad-hoc approaches can be obviated, is one of questionable code resilience. Ad-hocs are typically specific to individual projects, and usually haven't been subjected to the same rigorous testing and code review that RAII and exception mechanisms are when supported as a primary language feature. It's far more reasonable for a team to base the success of a important project on the exception handling of an internationally supported C++ compiler, than to rely on an ad-hoc C mechanism such as the example provided above, and usually even preferable to mechanisms constructed in house by the same team.

References

  1. ^ Stroustrup, Bjarne (1994). The Design and Evolution of C++. Addison-Wesley. ISBN 0-201-54330-3. 
  2. ^ Python 2.5.2 manual: 1.10 Reference Counts Accessed on 2009-05-15.
  3. ^ CERT MEM12-C. Consider using a Goto-Chain when leaving a function on error when using and releasing resources
  4. ^ Edsger Dijkstra (March 1968). "Go To Statement Considered Harmful" (PDF). Communications of the ACM 11 (3): 147–148. doi:10.1145/362929.362947. http://www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF. "The unbridled use of the go to statement has as an immediate consequence that it becomes terribly hard to find a meaningful set of coordinates in which to describe the process progress. ... The go to statement as it stands is just too primitive, it is too much an invitation to make a mess of one's program.". 
  5. ^ Exceptions in C
  6. ^ Exception Handling in C without C++
encrypt lang [de jp fr] diff backlinks (sec) validate printable
Klein bottle for rent; inquire within.
[ Your browser's CSS support is broken. Upgrade! ]
alexsiodhe, alex north-keys