Handling Allocation Errors

Mark Grosberg http://www.conman.org/projects/essays/allocerr.html

Unfortunately, most programmers don't think much about errorhandling. In many situations, error handling code is not eventested. The funny thing is that most of the time, with a littleforethought, error handling can be made generic just like any otherprogramming construct.

Looking back on history, programmers implementing databases havealways had to guarantee what are called the ACID properties:

  • Atomicity
  • Consistancy
  • Isolation
  • Durability

Most commercial database engines guarantee these properties byusing something called a journal. Conceptually, a journal recordswhat the database engine is going to do so that it can be undone (orredone in the event of a crash) if necessary. While the actualmechanics of implementing a journal are not to be discussed here,the idea behind journaling provides a unique solution to errorhandling. In particular, the “undoing” effect of ajournal is exactly what most error handling code in programs (well,those that attempt to recover from errors) does.

Lets look at a typical routine from a ficticious text editor:

typedef struct
{
  FILE     *file;
  char     *name;

  char     *line_buffer;
  char     *body_buffer;
} EDITBUFFER;

BOOL OpenFile(EDITBUFFER *eb, char *fn)
{
  long  file_size;

  /* First, copy the file name */
  eb->name = malloc(strlen(fn) + 1);
  if (eb->name == NULL)
     return FALSE;
  strcpy(eb->name, fn);

  /* Now, open the file. */
  eb->file = fopen(fn, "r+");
  if (eb->file == NULL)
   {
      free(eb->name);

      return FALSE;
   }

  /* Create the memory buffers. */
  eb->line_buffer = malloc(81); 
  if (eb->line_buffer == NULL)
   {
      free(eb->name);
      fclose(eb->file);

      return FALSE;
   }
  
  /* Since this could fail, check the return code. */
  file_size = GetFileSize(eb->file);
  if (file_size == NULL)
   {
      free(eb->name);   
      fclose(eb->file);
      free(eb->line_buffer);

      return FALSE;
   }
  eb->body_buffer = malloc(file_size);
  if (eb->body_buffer == NULL)
   { 
      free(eb->name);    /* Notice this code is duplicated. */
      fclose(eb->file); 

      return FALSE;
   }

  return TRUE;
}

The majority of that function is actually error handling code.Some programmers get rather tedious of this and simply omit theerror handling code (after all, how could a malloc()for 81 bytes fail?). This is simply unacceptable in saftey-criticalsoftware (not that it is any more acceptable in non-criticalsoftware).

Even those programmers that don't mind writing code like theabove can make mistakes and potentially cause a failure at worst (orleak resources at best). When programming, you should always say toyourself: “This is a computer, it is here to do the dirty workfor me, not the other way around.” It may sound silly, but manyprogrammers just don't think that way.

Some languages have garbage collection, but this only solves partof the problem. Leaking resources may be the most obvious case whereerror handling can fail. A more sinister problem is actions thathave been partially completed. In general, an action some either runto completion or it should have no side effects (thats the Ain ACID).

For example, if a routine that creates a file fails aftercreation of the file (but before completing successfully) the fileshould be automatically deleted. Garbage collection (at least mostimplementations) can not solve this problem. Journaling does.

One simple solution to fix the problematicOpenFile() function is to put all of the error cleanupas a block and use goto to jump into the propercleanup location. While this solution has its merits (simplicityfor one) it does not work in more complex situations.

What we really want for the computer to track resources as weallocate them. If we detect a failure, the computer shouldautomatically clean up the resources. The problem is how does thecomputer know if I want a file closed, deleted, or perhaps copied toa backup file. Simple answer: You tell it!

Each resource keeps a function pointer that it associates withthe resource. In the event of an error the list is walked and thefunction associated with each pending resource is called. Thefunction can be set to perform different actions using internallogic. Alternatively, different functions to perform differentlevels of cleanup for the same type of resource can be written.


There are many different ways to implement this scheme. A simpleapproach using a union can be used for an efficientimplementation:

/* This calculates the number of elements in an array. */
#define NUMELEM(a)    (sizeof(a) / sizeof(a[0]))

typedef void (*CleanupFunction)(void *Resources);
typedef struct
{
  void            *Resource;
  CleanupFunction  Cleaner;
} Allocation;
typedef struct
{
  size_t      Size, Count;
} ResourceListHead;
typedef union
{
  ResourceListHead  head;   /* This must come first so we can initialize it */
  Allocation        alloc;
} ResourceList;

void TrackResource(ResourceList *list, void *Resource, CleanupFunction Cleaner)
{
   size_t  next;

   if (list[0].head.Count != list[0].head.Size)
    {
      next = (list[0].head.Count)++;
   
      list[next].alloc.Resource = Resource;
      list[next].alloc.Cleaner  = Cleaner;
    }
}

void DestroyResources(ResourceList *list)
{
   size_t  count;
   
   count = list[0].head.Count;
   for(list++; count; count--)
    {
      /* Call the cleanup function. */
      (list->alloc.Cleaner)(list->alloc.Resource);

      list++;
    }
}

This is the most minimal resource tracking you can get. There isno error handling. Do not track more resources than you reservespace for. As an example of how to use this (specific)implementation, I present the problematic OpenFile()function using these resource lists:

/* These would (normally) be in a runtime library somewhere */
void CleanMalloc(void *buf)
{
  free(buf);
}

void CloseFile(void *buf)
{
  fclose((FILE *)buf);
}

BOOL OpenFile(EDITBUFFER *eb, char *fn)
{
  /* 
      * We always need one more structure than we plan to track for
      * the header. Also, we must initialize the first element so that the 
      * list is considered empty. 
      */
  ResourceList   allocs[6] = {  NUMELEM(allocs) - 1, 0 }; 
  long           file_size;

  /* First, copy the file name */
  eb->name = malloc(strlen(fn) + 1);
  if (eb->name == NULL)
     goto error;
  TrackResource(allocs, eb->name, CleanMalloc);
  strcpy(eb->name, fn);

  /* Now, open the file. */
  eb->file = fopen(fn, "r+");
  if (eb->file == NULL)
     goto error;
  TrackResource(allocs, eb->file, CloseFile);
 
  /* Create the memory buffers. */
  eb->line_buffer = malloc(81); 
  if (eb->line_buffer == NULL)
     goto error;
  TrackResource(allocs, eb->line_buffer, CleanMalloc);  

  /* Since this could fail, check the return code. */
  file_size = GetFileSize(eb->file);
  if (file_size == NULL)
     goto error;

  eb->body_buffer = malloc(file_size);
  if (eb->body_buffer == NULL)
     goto error;

  return TRUE;

error:
  DestroyResources(allocs);
  return FALSE;  
}

That is only a small example of how resource lists can removeerror handling code. In a real application, the tracking should bedone by the allocation routines directly. This eliminates all of theconditionals that are repeated on every allocation.


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章