[comp.lang.c] error handling and freeing resources using setjmp/longjmp

tada@athena.mit.edu (Michael J Zehr) (05/08/90)

Enough people have requested this (after a posting of mine about 2 weeks
ago) that i'm posting it here.

I hope the comments in error.h and the samples in err_test.c give enough
information for people to use it.  note particularly the restrictions in
the first comment in error.h.

warning 1 -- this required ANSI headers. 
warning 2 -- i only had one machine to test this on. it's possible that
something i'm doing works by coincidence only. (gosh i hope not, though!
*sigh* i don't have lint, though) use with the caution you should use
anything you get from the net... and feel free to respond with any
suggestions.

three files: error.h, error.c, and err_test.c

lines of '---' separate the files (i don't know how to make a shell archive)

error.h:
--------------------------------------------------
/* error.h -- created 7-27-89 by michael j zehr
**
** i am indebted to Sam Itkin for the inspiration for this design.
** Sam and i currently work for Consultants for Management Decision, Inc,
** in Cambridge Mass, where he gave a luncheon seminar on error-handling
** methods.  this represents an implementation of a design he sketched out.
**
** feel free to use this code, but please attribute it.
**
** error handling macros, definitions, and structures
**
** the error-handling mechanism is designed to allow error-trapping through
** non-local jumps, including deallocating resources along the way
**
** an example of use would be as follows:
**
**   ...
**   if (error_exit(error_code, error_class) {
**     error-handling code; 
**     return;
**   }
**
**   id = on_error(cleanup, param1, param2, ...);
**   do_something();
**   if (problem) error(err_code);
**   no_error(id);
**   ...
**   clear_error_exit();
**
** if do_something() contains a call to error(), or if the problem condition is
** true and error() is called at that level, then cleanup() will be executed
** (along with any other routines registered through on_error())
** the cleanup will continue back to the most recent error_exit for which
** the error_class & (error function parameter) is nonzero, and control 
** returns to the 'if' clause at the top of the procedure.
** (note that error(0) will not be trapped by any error_exit call, and will
** execute all functions on the error stack and eventually exit execution.)
**
** RESTRICTIONS:
** defining more than MAX_ERROR_LEVELS error_exits simlultaneously is a
** considered a fatal error, and halts execution.  
**
** calling error() from withing a "cleanup()" routine registered 
** with the error handler also halts execution.
**
** calling error() before checking error_exit is a bug too.
**
** because the non-local jumps depend on the state of the procedure stack, 
** clear_error_exit() must be called from the same procedure as error_exit().
**
** this code is written for ANSI C compilers and headers.  if you want to run
** on something else, you need to include varargs.h, probably, and change
** the macros for variable parameter lists.  you also need to change the
** void *'s to char *'s, include something other than stdlib.h, etc...
**
** this code is not intended to be a completely robust error-handler. 
** it does, however, provide a way of using a non-local jump and also 
** freeing allocated resources.
**
** one thing in particular that needs to be addressed is how error_exit
** handles the error-code returned. should it look for only one error
** code and propagate the rest through error()? should it look for a class
** of error codes (checking a particular bit)? ideally it should look for
** a list of error codes, but what is the best way to code that?
**
** one other area for modification would be in the error_stack structure.
** the arrays could be changed to pointers, and an error_init function
** could initialize it given values for error_depth, etc...
**
*/

#ifndef _ERROR_H_
#define _ERROR_H_

#include <stdlib.h>
#include <setjmp.h>
#include <errno.h>

#define ERROR_DEPTH 15
#define ERROR_PARAMS 6
#define ERROR_STACK_SIZE 250


/* depending on what method you want to use for error codes, theses may 
** or may not be useful
*/

#define FATAL		(1<<15)
#define ERROR		(1<<14)
#define WARNING 	(1<<13)
#define ALL_CLASSES	(~0)

/* in VAXC, the last predefined error message is 65 - starting at 128
** leaves room for predefined expansions without having to renumber
*/
#ifdef VAXC
#define ENOMEM		(128)		/* out of memory */
#define EBADARGS	(129)		/* bad arguments */
#endif

/* errno.h labelled as verion 7.1 (Berkeley) 6/4/86 has an entirely different
** set which goes up to 75 and includes ENOMEM ...
** you'll have to pick your own values to match whatever platform(s) you
** need to run on
*/

/* variable definitions and declarations */
typedef struct {
  int num_params;			/* parameters for function */
  void *params[ERROR_PARAMS];		/* each paramter */
  void (*func)();			/* error function */
} Err_func;

typedef struct {
  int error_fcnt;		/* number of error functions on stack */
  int error_code;		/* error code if unwinding, zero otherwise */
  int error_depth;		/* number of error_exits active */
  jmp_buf env[ERROR_DEPTH];	/* data for longjmp */
  int jmp_index[ERROR_DEPTH];	/* function stack location of mylongjmp */
  Err_func f[ERROR_STACK_SIZE];	/* cleanup functions */
} Err_stack;

#pragma nostandard
extern noshare Err_stack err_stack;
extern noshare int _tmp_int;
extern noshare void *_tmp_ptr;
#pragma standard

/* function declarations */
void mylongjmp(void); /* interface to longjmp() */

void error(int error_code);	/* unwinds stack, returns to error_exit */

/* sets up stack to call func if an error is triggered. */
/* returns an ID  used to clear error stack */
int on_error(void (*func)(), int num_params, ...);

void no_error(int id);		/* clears the error stack of that routine */

void clear_error_exit(void);	/* clears last error trap */

void *malloc_remember(size_t size, int error_code);


/* must be a macro, since it depends on setjmp */
#define error_exit(error_code, error_class) \
  (((err_stack.error_depth+1 > ERROR_DEPTH) ? (abort(),0) : 0), \
   (err_stack.jmp_index[err_stack.error_depth] = err_stack.error_fcnt), \
   (on_error(mylongjmp, 0)), \
   (((error_code) = setjmp(err_stack.env[err_stack.error_depth++])) ? \
     (((error_code) & (error_class)) ? \
       (error_code) : \
       (error(error_code),0)) : \
     (error_code)) \
  )

/* sample macros for mallo */
#define safe_new(t) \
  ((t*)malloc_remember(sizeof(t), ENOMEM))

#define safe_new_array(t, n) \
  ((t*)malloc_remember(sizeof(t) * (n), ENOMEM))

#endif
--------------------------------------------------
error.c
--------------------------------------------------
/* error.c -- created 7-31-89 by michael j zehr
**
** error handling function definitions
*/

#include <stdio.h>
#include <stdarg.h>
#include <assert.h>
#include "error.h"


/* global error stack declaration, plus temp variables for macros */
#ifdef VAXC
#pragma nostandard
noshare Err_stack err_stack;
#pragma standard
#else
Err_stack err_stack;
#endif


/* function declarations */

/* interface to longjmp -- clears the current error from the 
** error stack, decrements error_depth, and calls longjmp with
** the env from the err_stack structure
*/
void mylongjmp(void)
{
  int err_code;

  err_code = err_stack.error_code;
  err_stack.error_code = 0;
  err_stack.error_depth--;
  longjmp(err_stack.env[err_stack.error_depth], err_code);
}


/* triggers an error.
** sets the error_code in the err_stack structure and starts
** unwinding the error_stack, calling each function in turn until
** it hits the call to mylongjmp, which then returns control to
** the last error_exit macro
*/
void error(int error_code)
{
  int findex;
  Err_func *errf;

  if (err_stack.error_depth <= 0) {
    fprintf(stderr, "error with no error handler in scope.  code = %d\n", error_code);
    assert(0);
  }

  err_stack.error_code = error_code;

  while(1) {
    errf = &err_stack.f[--err_stack.error_fcnt];
    switch (errf->num_params) {
      case 0: 
        (*errf->func)();
        break;
      case 1:
        (*errf->func)(errf->params[0]);
        break;
      case 2:
        (*errf->func)(errf->params[0], errf->params[1]);
        break;
      case 3:
        (*errf->func)(errf->params[0], errf->params[1], errf->params[2]);
        break;
      case 4:
        (*errf->func)(errf->params[0], errf->params[1], errf->params[2],
          errf->params[3]);
        break;
      case 5:
        (*errf->func)(errf->params[0], errf->params[1], errf->params[2],
          errf->params[3], errf->params[4]);
        break;
      case 6:
        (*errf->func)(errf->params[0], errf->params[1], errf->params[2],
          errf->params[3], errf->params[4], errf->params[5]);
        break;
    } /* end of switch */
  } /* end of while -- execution always transfers through longjmp */
}


/* puts func on the error stack, along with the parameters 
** an ID is returned which is used to clear the error in no_error()
*/
int on_error(void (*func)(), int num_params, ...)
{
  va_list ap;
  int i;

  err_stack.f[err_stack.error_fcnt].func = func;
  err_stack.f[err_stack.error_fcnt].num_params = num_params;

  if (num_params > ERROR_PARAMS) {
    fprintf(stderr, "%d too many params for error function\n", num_params);
    assert(0);
  }

  va_start(ap, num_params);

  /* set up all the parameters for the error function */
  for(i=0; i<num_params; i++) 
    err_stack.f[err_stack.error_fcnt].params[i] = va_arg(ap, void *);
    
  err_stack.error_fcnt++;

  va_end(ap);
  return err_stack.error_fcnt;

}

/* clears the error function associated with that ID.
** if it isn't the most recent routine pushed on the error stack,
** then it prints a diagnostic and exits
*/
void no_error(int id)
{
  if (id != err_stack.error_fcnt) {
    fprintf(stderr, "attempt to clear error func %d out of scope\n", id);
    assert(0);
  }

  /* 'remove' it from the top of the stack */
  err_stack.error_fcnt--;
}

/* clears the last error_exit macro trap */
void clear_error_exit(void)
{
  if (err_stack.error_depth <= 0) {
    fprintf(stderr, "attempt to clear error trap with no error trap set\n");
     assert(0);
  }

  err_stack.error_depth--;
  err_stack.error_fcnt = err_stack.jmp_index[err_stack.error_depth];
}


/* dynamic memory allocation -- allocates memory and sets up the error stack
** to free it.  because the result of on_error() is discarded, either the
** entire error stack is executed through an error() call, or it's all
** discarded through a call to clear_error_exit().
*/
void *malloc_remember(size_t size, int err_code)
{
  void *temp;
  void free();

  temp = malloc(size);

  if (temp == NULL)
    error(err_code);

  on_error(free, 1, temp);
  return temp;
}
--------------------------------------------------
err_test.c -- shows how it's used a bit... can be used to test it.
--------------------------------------------------
/* test program for error handling routines */

#include "error.h"
#include <stdio.h>

void foo(char *str)
{
  puts("in trap1");
  puts(str);
}

void foo2(char *str)
{
  puts("in trap2");
  puts(str);
}

void foo1(void)
{
  int err_id, err_code;
  char *str = "second error trap function";  

  puts("in foo1");

  if (error_exit(err_code, 2)) {
    puts("arrived at error_exit2");
    printf("error code: %d\n", err_code);
    exit(1);
  }

  err_id = on_error(foo2, 1, str);

  clear_error_exit();

#ifdef DEBUG
  puts("about to generate error");
  error(1);
#else
  puts("returning without error");
#endif
}

int main(void)
{
  int err_code, err_id;
  char *str = "test string";

  if (error_exit(err_code, 1)) {
    puts("arrived at error_exit");
    printf("error code: %d\n", err_code);
    exit(1);
  }
  
  puts("past error_exit");

  err_id = on_error(foo, 1, str, NULL, NULL, NULL, NULL, NULL);

  foo1();

  no_error(err_id);

  puts("about to call clear_error_exit");

  clear_error_exit();
}