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.