WSU Tri-Cities CptS 360 (Spring, 2021)

Lab 7: Exceptions

In this lab, you'll see how to implement exceptions in C. Not C++, C. In his (recommended) book, "C Interfaces and Implementations", David Hanson presents an implementation of exceptions using C preprocessor (cpp) macros and one compiled function.

Templates and other necessary files are in:

http://www.tricity.wsu.edu/~bobl/cpts360/lab07_exceptions

Part 2 depends on Part 1, so be sure that part is working before you move on.

Part 1: Implementing Exceptions

There's an example of how Hansen's except package works in the file except_demo.c:

#ifndef SHOW_CPP_ONLY // this is for documentation: the #includes are required
#include <stdlib.h>
#include <stdio.h>
#endif

#include "except.h"

Except_T NoSuchFile = { "Failed to open file." };

void doFile(char *fn)
{
    FILE *f = fopen(fn, "r");
    if (!f)
        RAISE(NoSuchFile);
    fclose(f);
}

int main(int argc, char *argv[])
{
    int i;
    int ctAttempted = 0, ctFail = 0;

    for (i = 1; i < argc; i++) {
        ctAttempted++;
        TRY
            doFile(argv[i]);
        EXCEPT(NoSuchFile)
            ctFail++;
        END_TRY;
    }
    printf("%d open(s) attempted, %d failed\n", ctAttempted, ctFail);
    exit(EXIT_SUCCESS);
}

The program is very simple: You pass an arbitrary number of file name arguments on the command line. It attempts to open all of them for reading, maintaining counts of the number of attempts and of failures. At the end, it prints both counts on standard output. This is meant for illustration only: It would be silly to use exceptions to implement such a trivial program. (We'll do a less trivial one later.)

Although it is implemented in C, except's use of macros makes C exceptions resemble those in C++, Java, or Python. The syntax of an except exception block is:

TRY
block of code to watch for exceptions in
[ EXCEPT( exception )
how to "handle" exception if it is raised anywhere in the block ]*
[ ELSE
what to do if an exception not otherwise handled is raised ]
[ FINALLY
stuff that must be done, whether an exception is raised or not ]
[ RETURN [ value ]
return from the function containing the exception block ]
END_TRY;

The "["s and "]"s bracket optional parts and the "*" says there can be 0 or more EXCEPT() parts. Remember that the exception can be raised anywhere within the block and, more commonly, in any function in the call tree of the block. It is important to note that the FINALLY is always executed, even if the block raises an exception that will be handled in a caller above this one in the calling tree.

In addition to these macros, the RERAISE macro (like END_TRY above: no arguments, no parens, terminating ;) can be used in any handler to re-raise that handler's exception at a higher level.

The cpp magic happens in the except.h header file:




/*
 * Adapted (with very few changes) from David Hansen's "C Interfaces
 * and Implementations".
 */

#ifndef EXCEPT_INCLUDED
#define EXCEPT_INCLUDED

# ifndef SHOW_CPP_ONLY // this is for documentation: the #include is required
#include <setjmp.h>
# endif

typedef struct Except_T {
    char *reason;
} Except_T;

typedef struct Except_Frame Except_Frame;

struct Except_Frame {
    Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const Except_T *exception;
};

enum { Except_entered=0, Except_raised,
       Except_handled,   Except_finalized };

#ifdef WIN32
__declspec(thread)
#endif

extern Except_Frame *Except_stack;
extern const Except_T Assert_Failed;
void Except_raise(const Except_T *e, const char *file,int line);

#define RAISE(e) Except_raise(&(e), __FILE__, __LINE__)

#define RERAISE Except_raise(Except_frame.exception, \
    Except_frame.file, Except_frame.line)

#define RETURN switch (Except_stack = Except_stack->prev,0) \
    default: return

#define TRY \
do { \
    volatile int Except_flag; \
    Except_Frame Except_frame; \
    Except_frame.prev = Except_stack; \
    Except_stack = &Except_frame;  \
    Except_flag = setjmp(Except_frame.env); \
    if (Except_flag == Except_entered) {

#define EXCEPT(e) \
        if (Except_flag == Except_entered) \
            Except_stack = Except_stack->prev;   \
    } else if (Except_frame.exception == &(e)) { \
        Except_flag = Except_handled;

#define ELSE \
        if (Except_flag == Except_entered) \
            Except_stack = Except_stack->prev;  \
    } else { \
        Except_flag = Except_handled;

#define FINALLY \
        if (Except_flag == Except_entered) \
            Except_stack = Except_stack->prev; \
    } { \
        if (Except_flag == Except_entered) \
            Except_flag = Except_finalized;

#define END_TRY \
        if (Except_flag == Except_entered) \
            Except_stack = Except_stack->prev; \
    } if (Except_flag == Except_raised) RERAISE; \
} while (0)

#endif

Note the following:

This is pretty sophisticated cpp, but it helps to see what it expands to. Here's what we get if we expand these macros and "prettyprint" the resulting C:


typedef struct Except_T
{
    char *reason;
} Except_T;
typedef struct Except_Frame Except_Frame;
struct Except_Frame
{
    Except_Frame *prev;
    jmp_buf env;
    const char *file;
    int line;
    const Except_T *exception;
};
enum
{ Except_entered = 0, Except_raised,
    Except_handled, Except_finalized
};
extern Except_Frame *Except_stack;
extern const Except_T Assert_Failed;
void Except_raise (const Except_T * e, const char *file, int line);
Except_T NoSuchFile = { "Failed to open file." };

void doFile (char *fn)
{
    FILE *f = fopen (fn, "r");

    if (!f)
        Except_raise (&(NoSuchFile), "except_demo.c", 14);
    fclose (f);
}

int main (int argc, char *argv[])
{
    int i;
    int ctAttempted = 0, ctFail = 0;

    for (i = 1; i < argc; i++)
      {
          ctAttempted++;
          do
            {
                volatile int Except_flag;
                Except_Frame Except_frame;

                Except_frame.prev = Except_stack;
                Except_stack = &Except_frame;
                Except_flag = setjmp (Except_frame.env);
                if (Except_flag == Except_entered)
                  {
                      doFile (argv[i]);
                      if (Except_flag == Except_entered)
                          Except_stack = Except_stack->prev;
                  }
                else if (Except_frame.exception == &(NoSuchFile))
                  {
                      Except_flag = Except_handled;
                      ctFail++;
                      if (Except_flag == Except_entered)
                          Except_stack = Except_stack->prev;
                  }
                if (Except_flag == Except_raised)
                    Except_raise (Except_frame.exception, Except_frame.file,
                                  Except_frame.line);
            }
          while (0);
      }
    printf ("%d open(s) attempted, %d failed\n", ctAttempted, ctFail);
    exit (EXIT_SUCCESS);
}

except uses one additional function in this file, except.c:

/*
 * Adapted (with very few changes) from David Hansen's "C Interfaces
 * and Implementations".
 */

#include <stdlib.h>
#include <stdio.h>
#include "assert.h"
#include "except.h"

#ifdef WIN32
__declspec(thread)
#endif

Except_Frame *Except_stack = NULL;

void Except_raise(const Except_T *e, const char *file,
        int line) {
        Except_Frame *p = Except_stack;
        assert(e);
        if (p == NULL) {
                fprintf(stderr, "Uncaught exception");
                if (e->reason)
                        fprintf(stderr, " %s", e->reason);
                else
                        fprintf(stderr, " at 0x%p", e);
                if (file && line > 0)
                        fprintf(stderr, " raised at %s:%d\n", file, line);
                fprintf(stderr, "aborting...\n");
                fflush(stderr);
                abort();
        }
        p->exception = e;
        p->file = file;
        p->line = line;
        Except_stack = Except_stack->prev;
        longjmp(p->env, Except_raised);
}

Note the longjmp() in Except_raise(). This file must be compiled and linked into any program that uses except.

So here's what you need to do for Part 1:

  1. Download all of the above-named files.
  2. Create a Makefile to compile except.o and except_demo.o and link them together to make the executable except_demo.
  3. Test except_demo to make sure it's working.
  4. Be sure to include all the files you download (even though you don't change them) in your submission tarball.

Part 2: find_file -- A Significant Application of Exceptions

find_file is a non-trivial example of using exceptions. Its syntax is:

$ find_file [ -v ] fileName [ directoryName ]*

This will look for a file named fileName in all of the directories specified and all of their subdirectories, stopping immediately if the file is found. If no directoryNames are specified, it uses the current one ("."). If the file is found, it exits with a 0 (i.e. success in a shell sense). If not, it exits with a a 1 (i.e. failure).

If the option "-v" is given, it will print the path of the file, if found, on standard output. (If the same fileName occurs more than once in the directories only the first instance is printed.)

Here is the code for this part of the assignment, contained in the file find_file_tplt.c.

#define _GNU_SOURCE // to get asprintf() prototype
#include <stdio.h>  // this needs to be the first #include in that case

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <getopt.h>

#include "except.h"

/*
 * Here's how you declare an exception with the "except" package:
 */
Except_T StatFailed = { "Failed to open file." };
/*
 * ASSIGNMENT
 *
 * Add additional declarations for exceptions TargetFound and
 * MiscellaneousError.
 */

char *progname = "*** progname not set ***"; /* should be argv[0] */

int verbose = 0; /* set on command line */

static void explore(char *path, char *target);

static void traverseDirectory(char path[], char *target)
{
    /*
     * ASSIGNMENT
     *
     * Implement the following pseudocode:
     *
     * open the directory associated with `path` (hint: opendir(3))
     * if the open fails,
     *     raise the MiscellaneousError exception
     * for each entry in the directory (hint: readdir(3)),
     *     if the entry's name is "."  or ".."
     *         skip that entry
     *     allocate a string `subpath` concatenatiing `path`, "/", and
     *      the entry's name (hint: asprintf())
     *     if in the following ... (hint: TRY ... END_TRY)
     *         call explore on `subpath` and pass `target` as well
     *     ... the TargetFound exception is raised (hint: EXCEPT())
     *         free `subpath`
     *         close the open directory (hint: closedir(3)
     *         re-raise the exception (hint: RERAISE)
     *     ... any other exception is raised (hint: ELSE)
     *         print a message to stderr that explore() failed
     *     free `subpath`
     * close the directory associated with `path` (hint: closedir(3))
     */
}


static void explore(char *path, char *target)
/* look at, in, and below `path` for a file named `target` */
{
    /*
     * ASSIGNMENT
     *
     * Implement the following pseudocode:
     *
     * get the status of `path` (hint: stat(2))
     * if it fails,
     *     raise the StatFailed exception (hint: RAISE)
     * find the last '/'-delimited component of `path` (or use `path`
     *  itself if it contains no '/'s, hint: strrchr())
     * if that component is equal to `target`, (hint: strcmp())
     *     if `verbose` is set,
     *         print `path` to standard output, followed by a newline
     *          (hint: printf())
     *     raise the TargetFound exception (hint: RAISE())
     * if `path` is a directory (hint: S_ISDIR())
     *     traverse it (hint: traverseDirectory())
     */
}

void findFile(char *top, char *target)
{
    /*
     * ASSIGNMENT
     *
     * Implement the following pseudocode:
     *
     * if in the following ... (hint: TRY ... END_TRY)
     *     call explore on `top` and pass `target` as well
     * ... the StatFailed exception is raised
     *     do nothing (put a ";" here)
     * ... the TargetFound exception is raised
     *     exit successfully (hint: exit(3))
     */
}


void usage(void)
{
    printf("usage: %s [-h] [-v] target [directory]*\n", progname);
}


int main(int argc, char *argv[])
{
    int i, ch;
    char *target;
    extern int optind;

    progname = argv[0];
    while ((ch = getopt(argc, argv, "hv")) != -1) {
        switch (ch) {

        case 'v':
            verbose = 1;
            break;

        case 'h':
            usage();
            exit(EXIT_SUCCESS);

        case '?':
            usage();
            exit(EXIT_FAILURE);
        }
    }
    if (optind >= argc) {
        usage();
        exit(EXIT_FAILURE);
    }
    target = argv[optind++];
    if (optind == argc) {
        /* directory name(s) not provided */
        findFile(".", target);
    } else {
        /* directory name(s) provided */
        for (i = optind; i < argc; i++)
            findFile(argv[i], target);
    }
    /*
     * If we find the target, we'll exit immediately (and
     * successfully), so if we get to this point...
     */
    exit(EXIT_FAILURE);
}

Here are the instructions for Part 2:

  1. Download find_file_tplt.c and rename it to find_file.c.
  2. Make the above-indicated changes (flagged with "ASSIGNMENT" comments).
  3. Enhance the Makefile you created in Part 1 to compile find_file.o and link it with except.o to make the executable find_file.
  4. Test find_file to make sure it's working. Be sure to try a variety of permutations of arguments and existant/non-existant files and directories.
  5. Be sure to include your find_file.c and Makefile in the submission tarball.
  6. Submit your tarball via Blackboard.