Lecture #12: Robust Programming

Reading: none
Handouts: "Bomb-Proof" Code
  1. Greetings and felicitations
  2. Four principles
  3. Discuss design of stack example

"Bomb-Proof" Code

Even if code is correct, it can still have fatal flaws. It may, for example, be unreadable or unmodifiable. We have discussed these problems in class.

Another common flaw is that code may be fragile: it works fine as long as it's used very carefully -- but if you make any mistake in using the code, you will get subtle, far-reaching, and untraceable errors. The hallmark of such code is is brevity . . . and the endless hours spent debugging code that relies on it!

This handout explains how to write robust code. That's what it's called in the books. In the trade, we call it "bomb-proof" code, which is a term of high esteem. Amateurs write fragile code; professionals write bomb-proof code. That's one of the biggest differences between the two.

Fragile code is very easy to write. It takes no time at all. Robust or "bomb-proof" code takes longer to writer and occupies more lines of code. It's harder to do than writing the obvious fragile code. And it takes more effort. However, the time and effort spent writing robust code is in general far less than that needed to debug fragile code. So overall it is a net advantage to write robust code.

Basic Principles

There are just a few basic principles to writing robust code:

Paranoia. Be extremely suspicious of any code that calls your code; and be fairly suspicious of your own code, and routines that you use. Check everything! Trust no one! You should assume that all users that call your code are malicious and trying to break it.

Assume Maximum Stupidity. Assume that your caller will be a complete blockhead. Or that you yourself, in a week or two, will suffer from a bout of supreme stupidity. Prepare in advance to (a) block any damage that stupidity may cause, and (b) give clear error messages that point out the stupidity.

Don't Hand Out Dangerous Implements. Don't let others see your internal data structures. Don't ever let others have a chance to modify your data structures!! And to make things really robust, don't hand out C pointers to your callers. Use "tickets" instead.

Worry About Cases That "Can't Happen". Spend time thinking about unusual cases. Your assumptions about how things "will be" used will often be wrong. Other users will misunderstand and do things differently. Or you yourself will forget and slip up. Check for weird cases and make sure they don't have bad effects.

Examples: Fragile vs. Robust

We will illustrate these basic principles by two extended examples. Both are modules that implement a stack of integers.

The first -- in the typical "terse and elegant" C style -- is amazingly fragile. It's exactly the kind of code that you see littering a lot of amateur projects. We will take the time to point out scores of subtle flaws lurking in only a few lines of code!

The second approach is considerably lengthier. In part, that's because we have illustrated many of the ways to make a module bomb-proof. It may be a little more bomb-proof than is strictly necessary. But perhaps not. The code is longer and "less elegant" -- but it gives you an error message when misused, rather than silently trashing odd portions of memory. Code written to use this module will take minutes to debug rather than days!

The robust version illustrates basic principles as follows:

Paranoia. It checks all its parameters to notice and complain about user errors. It uses consistency checks to make sure that internal bugs are quickly caught. It does not let users see or access any internal data structures, except through authorized routines.

Assume Maximum Stupidity. It worries about users passing wrong or uninitialized variables. It worries about users deleting stacks and then trying to use them. It gives clear and comprehensive error messages whenever anything goes wrong.

Don't Hand Out Dangerous Implements. It does not let users have pointers to internal data structures. Instead all access is controlled through "tickets." Users are never allowed anywhere near internal data. Naturally it guards carefully against itself overwriting random storage in memory.

Worry About Cases That "Can't Happen". This code worries (at great length) about users who delete stacks and then try to use them, try to create zero-length stacks, try to create stacks where calculating the size gives an arithmetic overflow, and so forth. It is extremely difficult to think of all these "impossible" cases but they're worth agonizing over. These are the ones that are really impossible to debug if a module does not check for them, and give an explicit error message.

The Fragile Version

The fragile version can handle any number of stacks. Users can create a new stack at any time by calling a function; and they can free the storage used by any stack. Stacks can be of any size. The modules contains three simple functions:

stackmanage() creates or destroys a stack
push() pushes an integer onto a stack
pop() pops an integer from a stack

We will discuss each function in turn. First we'll describe how it works; and then we'll begin to point out all the subtle traps it poses for its unwary users!

stackmanage()

/*
 *  st-fragile.c              Chip Elliott     8 Feb 91
 *                modified by Matt Bishop     10 Oct 96
 *  Implement a very fragile "stack of integers" module.
 *  For comparison with st-robust.c only!
 *  +---------------------------------------------------+
 *  |   This code should NOT be used in any program!!   |
 *  +---------------------------------------------------+
 */
#include <malloc.h>
/*
 * stackmanage --
 *
 * Create a new stack, or delete an existing stack.
 *
 * Entered by:  st   -- stack pointer
 *              flag -- 0 means create a stack
 *                      1 means delete a stack
 *              size -- size of stack to create
 *
 * Exits with:  stack created or destroyed
 *
 * Exceptions:  NONE
 */
void stackmanage(int **st,int flag,int size)
{
    if (flag==0)                /* zero -- create stack */
        *st = (int *) alloc(size*sizeof(int));
    else                        /* otherwise free the storage */
        free(*st);
}
Stackmanage is called to either create or delete a stack. A simple flag tells which function to perform. When you create a stack, you must tell it how much room to allocate for the stack. When you delete a stack, you need only pass a pointer to the stack. It has "no exceptions."

Note that, among other problems, this routine fails the test of cohesion. It is a single function that does two different things. That leads to several of the problems listed below. But there are other, more subtle problems as well.

Things that can go wrong:

  1. User interchanges flag and size parameters.
    It's very easy to call stackmanage wrong. In particular, getting the flag and size parameters in the wrong order has bad consequences. If the user is trying to allocate storage, it will be freed instead.
  2. User gets flag backwards.
    The flag parameter is not easy to remember -- does 0 allocate a stack or free it? If the user guesses wrong, no message will be given. Instead, extremely subtle bugs will occur. It's hard to say which is worse: accidentally allocating a new stack when you meant to deallocate one, or vice versa. The first will cause invisible loss of storage; at some later point, you may (rather mysteriously) run out of storage for no obvious reason. And if you accidentally deallocate what a pointer points to, it can have truly awful effects -- but again they won't be visible until much later in the program. You will spend weeks trying to find bugs like this!
  3. User passes negative or zero size.
    Allocating a zero-length block of storage will almost certainly lead to trashing memory when you start to push items onto the stack. Negative sizes will have unpredictable results; most likely, negative numbers will be taken as very large positive numbers and the alloc() will return NULL, which is, of course, not noticed. This will cause a segment fault later in the program.
  4. alloc() fails.
    It returns NULL, which is, of course, not noticed. This will cause a segment fault later in the program. Since "segmentation fault" is the generic C error message, you won't have even one clue as to which part of your program had trouble. It would be better to get a message that explicitly said that you made a mistake allocating a new stack.
  5. size*sizeof(int) overflows.
    A subtle and unpleasant bug, which happens quite often on microcomputers. Since arithmetic is done to finite precision, some numbers are too big to represent. When you calculate those numbers, C keeps only the least significant digits! Hence you might be requesting, say, room for 10000012 integers. But if the calculation overflows, you might actually only request room for 12 integers! Naturally you will never notice; the stack will later overflow when you push onto it; this will trash odd pieces of memory; and your program will die mysterious some time later. You won't have a clue what hit you.
  6. User frees a stack without having allocated it.
    Since you never allocated a stack, the pointer you're using contains garbage. If you're lucky, it will contain NULL and you'll get the generic "segmentation fault" error. Or if not, you may be lucky enough to have free() ignore the bogus request. But it's possible that it will deallocate some random part of your memory; if so, your program will die horribly at a much later date and you will never understand what happened to you.
  7. User frees stack and then continues to use it.
    This is guaranteed to have horrible results, but they will not become apparent until after your program has run for some time. You will be trashing some random part of memory, and will never figure out what happened.
  8. User calls stackmanage(p,...) rather than stackmanage(&p,...).
    An ANSI C compiler should catch this mistake if function prototypes are used. That's a major advance over traditional C, which would give a segmentation fault sometime later in the program. However, I suspect that this calling sequence is awkward enough so that most compilations would get it wrong. This is only a nuisance, but why have calling sequences that require every user to make a mistake, compile, curse, use vi again, and recompile?

push()

/*
 * push --
 *
 * Push an integer onto a stack.
 *
 * Entered by:  st -- stack
 *              n  -- integer to push
 *
 * Exits with:  'n' pushed onto stack 'st'
 *
 * Exceptions:  NONE
 */
void push(int **st,int n)
{
    *((*st)++) = n;                /* push n onto stack */
}
Push is called to push an integer onto a stack. It has "no exceptions."

Things that can go wrong:

  1. The stack overflows.
    No error message is given, but some random part of memory is trashed. The results will only become apparent later in the program. Symptoms: a completely unrelated variable has a strange value. Or gives a segmentation fault. This kind of error takes a long time to debug.
  2. st has never been allocated, and/or doesn't really point to a stack.
    If you're lucky, the pointer contains a NULL value and you get a segmentation fault. But it's unlikely that you'd be so lucky. Most likely, you trash memory. Again, the results will be extremely mysterious and will not become apparent until much later.
  3. st once pointed to a stack, but the user has deleted that stack.
    Same problems as above. This is a little bit unlikely with a stack, since they don't usually get allocated and freed over and over. But it happens all the time with other kinds of data structures.

pop()

/*
 * pop --
 *
 * Pop an integer from a stack.
 *
 * Entered by:  n  -- integer to pop
 *              st -- stack
 *
 * Exits with:  'n' popped from stack 'st'
 *
 * Exceptions:  NONE
 */
void pop(int *n,int **st)
{
    *n = *(--(*st));               /* pop n from stack */
}
Pop is called to pop an integer from a stack. It has "no exceptions." Things that can go wrong:
  1. The stack underflows.
    No error message is given, but some random part of memory is trashed. The results will only become apparent later in the program. Symptoms: a completely unrelated variable has a strange value. Or gives a segmentation fault. This kind of error takes a long time to debug.
  2. User writes pop(&st,n) to mimic push(&st,n) syntax.
    An ANSI C compiler should catch this mistake if function prototypes are used. However, I suspect that these calling sequences are awkward enough so that most users would get it wrong. This is only a nuisance, but why have calling sequences that require every user to make a mistake, compile, curse, use vi again, and recompile?
  3. st has never been allocated, and/or doesn't really point to a stack.
    If you're lucky, the pointer contains a NULL value and you get a segmentation fault. But it's unlikely that you'd be so lucky. Most likely, you trash memory. Again, the results will be extremely mysterious and will not become apparent until much later.
  4. st once pointed to a stack, but the user has deleted that stack.
    Same problems as above. This is a little bit unlikely with a stack, since they don't usually get allocated and freed over and over. But it happens all the time with other kinds of data structures.

The Robust Version

The robust version privides roughly the same functionality as the fragile version, but requires a lot more code. On the other hand, it is pretty close to bomb-proof. Here's its header file:
typedef unsigned long int STICKET;      /* stack ticket (kept as integer) */

STICKET stmake(int size);
void stdel(STICKET st);
void push(STICKET st,int n);
int pop(STICKET st);
void stacksize(STICKET st,long int *size,long int *elts);
Note that stacks are referenced by STICKETs rather than pointers. Oddly enough, STICKETs are implemented as long integers. We will see why when reading the code. But the primary pointer is to give the "user" code no way to screw up with pointers. There is no way user code can overwrite memory by misusing stack pointers -- because the robust stack module does not give it any tools with which to do so.

The first four functions mimic those of the fragile version; stacksize() is a "peep-hole" into the closed and hidden worlds inside the stack module. It's designed to make it a little easier for users to make assertions about the use of their stack storage.

Robustness is one good thing. But providing some kind of debugging tools is another good thing. This module does both. (Most modules do a poor job at both these goals.)

/*
 *  st-robust.c              Chip Elliott     8 Feb 91
 *
 *  Implement a very robust "stack of integers" module. This
 *  code could safely be used in any program.
 *
 *  This is demonstration code, rather than production code.
 *  It illustrates several techniques that can be used to
 *  create "bomb-proof" and "easy-to-debug" modules. Production
 *  code would probably pull a few tricks to improve efficiency.
 *
 *  This package allows up to NSTACKS stacks to be in use at
 *  the same time. Each can hold up to 'n' elements, where 'n'
 *  is supplied when the stack is created, and must be <= MAXSIZE.
 *
 *  Internally, we keep an array stacks[NSTACKS]. Each element
 *  is a structure that contains a pointer to allocated storage
 *  for the stack (or NULL if the stack is not in use); the
 *  current number of elements in the stack; and the maximum
 *  number of elements in the stack.
 *
 *  All stacks are accessed through "stack tickets", which are
 *  integers that index our internal stacks[] array. However,
 *  we do not give a direct index into this array, since then
 *  the common error value of 0 would probably refer to the
 *  first stack created. Instead we add an offset to the index
 *  to make such accidents less likely. Each ticket also contains
 *  a unique "stamp" to make sure we catch dangling references.
 */
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
#include "st-robust.h"

#define NSTACKS 100             /* number of stacks we can handle */
#define MAXSIZE 5000            /* maximum size of any given stack */
#define STKOFF  2000            /* reduce chance that random # is ticket */

static struct {
   int *ptr;                    /* ptr   -- pointer to malloc storage */
   int stamp;                   /* stamp -- unique stamp for a stack */
   int elts;                    /* elts  -- # elements currently pushed */
   int size;                    /* size  -- max # elements */
} stacks[NSTACKS];              /* stacks[] is our array of stack info */

static int stamp = 1000;        /* usage stamp for stacks[i].stamp */

stkerror()

/*
 * stkerror                     (PRIVATE)
 *
 * Print an error message, identifying the routine which
 * caught the error; and then terminate.
 *
 * Entered by:  rtn -- routine name (eg, "pop")
 *              msg -- message      (eg, "stack is full (%d elements)")
 *              n   -- optional number to insert into 'msg' at %d
 *
 * Exits with:  prints message      (eg, "pop: stack is full (50 elements)")
 *              terminates
 *
 * Exceptions:  NONE
 */
static void stkerror(char *rtn,char *msg,int n)
{
    assert(rtn!=NULL);          /* trust no one! */
    assert(msg!=NULL);

    fprintf(stderr,"%s: ",rtn); /* print message on stderr */
    fprintf(stderr,msg,n);
    fprintf(stderr,"\n");

    exit(EXIT_FAILURE);         /* and terminate badly */
}
Stkerror is a private routine used within the stack module to handle error conditions. It prints a message and terminates. Note that the message can have three parts:

routine name The routine which noticed the error
message Text of the message to print
number An optional number that can be plugged into message

Points to ponder:

  1. Centralized handling of errors.
    Since all errors are handled in one place, it's easy to change what happens when an error occurs. For instance, this routine terminates with an error code. Centralization makes it very easy to change the error code -- or even to allow something other than termination. If code were spread out over the module, this would be more tedious. A slightly higher level of centralization would put all the error message texts in one location, and refer to them by #define symbols. This makes it easy to translate messages into languages other than English, and keep track of different versions in different languages. But that's more than we need for this course.
  2. assert() for internal consistency checks.
    We "know" that we can trust routines in the st-robust module. After all, they are not under user control and so cannot contain any mistakes! However, it's very wise to insert such consistency checks into your code. First, they help catch errors in the module coding. And believe me, these errors are there. Second, they will catch errors when new bugs are introduced to the module. Most updates introduce new bugs; consistency checks mean that these bugs are found immediately, rather than after extensive testing.
  3. Supplying the routine name in error messages.
    A message such as "argument is not a stack" is much better than "segmentation fault" because it's more explicit. It can only happen when you're using one of the stack routines. Better yet, though, is to tell which stack routine. That helps the poor user home in on his/her errors faster. And most likely you will be the poor user.
  4. Allowing optional numbers in error messages.
    This is often overlooked in designing error messages. A typical message says something like "too many items in a list". When you get this message, you have to head for the manual to find out how many items are the maximum. Why not include such numbers in your error messages and save this trip to the manual? In this case, the difference between messages stack is full (10 elements) and stack is full (5000 elements) can be very significant. In one case, you might immediately realize that you need to allocate a bigger stack. In the other, you might suspect that recursion had run out of control. Without that explicit number, you wouldn't have a clue.

getstack()

/*
 * getstack                      (PRIVATE)
 *
 * Private routine that converts a stack 'ticket' into
 * an index into our private array of stack information.
 *
 * Entered by:  rtn -- name of calling routine
 *              st  -- purported stack ticket
 *
 * Exits with:  returns index into stacks[] array
 *              if error, prints message and terminates
 *
 * Exceptions:  "<rtn>: argument is not a stack"
 *              "<rtn>: trying to access a deleted stack"
 */
static int getstack(char *rtn,STICKET st)
{
    int tst;                            /* stamp encoded in this ticket */

    tst = st & 0xFFFF;                  /* get ticket's unique stamp */
    st  = (st >> 16)-STKOFF;            /* get st in range 0..NSTACKS-1 */

    if (st<0 || st>=NSTACKS)
        stkerror(rtn,"argument is not a stack",0);

    if (tst != stacks[st].stamp)        /* catch dangling references */
        stkerror(rtn,"trying to access a deleted stack",0);

    assert(stacks[st].elts  >= 0);      /* consistency checks */
    assert(stacks[st].size  >  0);
    assert(stacks[st].stamp >  0);
    assert(stacks[st].elts  <= stacks[st].size);
    assert(stacks[st].ptr   != NULL);

    return st;
}
Getstack is an internal routine that converts a user's "stack ticket" into a pointer into the internal data structure stacks[] in which stack information is stored. It's called by all stack routines that accept stack tickets from the user.

Notice the advantages of a centralized routine to handle all this housekeeping. We can make all sorts of checks, and change them at whim without rewriting other portions of the code.

  1. The caller must pass its routine name for error messages.
    We require our caller to provide its routine name, so we can use it in error messages. Since getstack is a private routine, it can be used only by routines inthe stack module. This allows us to tailor the generic error messages -- thus we can print the error message "pop: argument is not a stack" so the user can home in on calls to the pop() routine.
  2. Checking for parameters that aren't really stack tickets.
    Half the point of using "stack tickets" instead of pointers is that we can check whether the user is screwing up and passing a bogus parameter. If so, we should immediately complain. The most likely cause is using a stack without allocating it; but passing an unitilialized variable could also be the problem. In either case, it's good to spot it immediately and give a clear error message.
  3. Checking for "dangling references" -- stack tickets which are no longer valid.
    Another point to using "stack tickets" is that we can identify tickets that used to point to stacks, but whose stacks have since been deleted. That's what the stamps are for, both in the stack itself and in its ticket. Since stamps are never reused, stack tickets are unique. This allows us to notice when the user is trying to use an outdated ticket, and to give a clear and explicit error message.
  4. assert() for internal consistency checks.
    We "know" that we can trust routines in the st-robust module. After all, they are not under user control and so cannot contain any mistakes! However, it's very wise to insert such consistency checks into your code. First, they help catch errors in the module coding. And believe me, these errors are there. Second, they will catch errors when new bugs are introduced to the module. Most updates introduce new bugs; consistency checks mean that these bugs are found immediately, rather than after extensive testing.
  5. Picking a range that does not include 0 for tickets.
    The most common values lying around in a computer's memory -- and hence in C variables -- are 0 and, more rarely, 1. So we explicitly disallow those as meaningful stack tickets. That way random junk (e.g., 0) will be spotted as junk rather than accepted as a real stack ticket.
/*
 * stmake
 *
 * Create a new stack, allowing up to 'size' elements.
 *
 * Entered by:  size -- # spaces to reserve for stack
 *
 * Exits with:  stack created
 *              returns ticket to new stack
 *
 * Exceptions:  "stmake: too many (>%d) stacks being created"
 *              "stmake: out of memory"
 *              "stmake: stack size must be in range 1..%d"
 */
STICKET stmake(int size)
{
   int i,*p;

   for (i=0; i<NSTACKS; i++)            /* look for i = empty spot... */
       if (stacks[i].ptr==NULL) break;  /* ...in our list of stacks */

   if (i==NSTACKS)
       stkerror("stmake","too many (>%d) stacks being created",NSTACKS);

   if (size<=0 || size>MAXSIZE)
       stkerror("stmake","stack size must be in range 1..%d",MAXSIZE);

   p = (int *) malloc(size*sizeof(int));
   if (p==NULL) stkerror("stmake","out of memory",0);

   stamp++;                      /* get a new unique stamp */

   stacks[i].ptr   = p;          /* keep ptr to this stack */
   stacks[i].stamp = stamp;      /* bump # times [i] has been used */
   stacks[i].elts  = 0;          /* currently 0 items pushed */
   stacks[i].size  = size;       /* remember max size */

				/* return ticket to stack */
   return ((i+STKOFF)<<16) | (stamp & 0xFFFF);  
}
Stmake allocates a new stack from an internal storage pool, and returns a "stack ticket" to the user. This ticket must be used in all further references to the stack.

Points to ponder:

  1. "Functional" programming style.
    Whenever possible, it's best to create functions that do not change their parameters. Such functions are usually easier to use than ones that do change parameters (you don't have to remember to add a "&" in front of arguments) -- and they're generally easier to understand. In a "functional" style, all parameters are for input only; the return value is the only result.
  2. Running out of stacks.
    This limitation does not exist in the fragile version. Actually, with a bit more programming, we could remove it from this robust version as well. But the resulting code and data structures would be more complicated, and so we chose to keep things simple. Hence this code allows no more than 100 stacks to be active at one time.
    Sometimes you can get robust implementations for free. But in this case, you've got to pay for the robustness. The reason? No matter how you arrange things, the module must keep a data structure for each stack that's created. The fragile version doesn't.
  3. Explicitly giving the number of stacks in the error message.
    Again, we would like to make life a little easier for the user. There's a world of difference between seeing the message too many (>2) stacks created and too many (>5000) stacks created. One indicates that the module should be recompiled with a larger limit; the other indicates that the calling program probably has some horrible flaw.
  4. Checking for a stack size out of range.
    We are worried about the extremely subtle bug in the fragile version, in which an arithmetic overflow caused a smaller stack to be allocated than the user expected. Hence we have chosen a stack size small enough to avoid any chance for overflow, thus nipping that problem in the bud. However, this solution is not entirely satisfactory since it limits stacks to a pointlessly small size. Best would be to allow stacks of up to exactly the maximum size that can be handled without overflow errors. We have chosen to finesse this bit of obscure coding by imposing a flat limit that will work on all computers.
  5. Explicitly giving the maximum stack size in the error message.
    The remarks given under comment 3 apply here as well.
  6. Checking for alloc() failure.
    Every program should always check for NULL values from storage allocators! And for every other error return as well. If you check, you can replace obscure segmentation faults that happen sometime later by clear, explicit error messages that immediately pinpoint that error when it occurs.
  7. Giving a unique stamp to each stack and ticket.
    This idea is perhaps the hardest to understand, since it deals with the most subtle of bugs. I will explain in class, or elsewhere, the problem of the lifetime of allocated storage, and dangling references. Suffice it to say that we add a unique stamp to each stack, and to each ticket, so that each ticket matches only one stack, thus eliminating bugs with dangling references.
  8. Maintenance of internal information about each stack.
    Notice that it's not actually necessary to set elts to zero, since it's guaranteed to be zero already. Nonetheless, it's comforting to whoever reads the code to see that all stack information has been set to known values. This saves a certain amount of anxiety and page-flipping on the part of the reader/maintainer.
  9. Picking a range that does not include 0 for tickets.
    As mentioned elsewhere, it's a good idea to keep zero, one, NULL, and other common random values from being valid tickets. This lessens the chance that the user can pass an uninitialized variable to some routine, or the wrong variable, and get away with it.
Subtle point: Note that our tickets can never be zero, since the unique stamps start with 2,000 as their smallest value and increment by one for each new stack created. (Okay, after tens of thousands of stack creations they will eventually be zero; but in practice that will not happen.) However, we also make sure that the stacks[] index is nonzero. Why? We would like to give a nice error message if the user passes us a variable that contains 0. But since 0 is a valid stack[] index and an invalid stamp, we would complain that the user had deleted this stack. But that's not true: the user has simply passed us the number 0. So we make sure that the stack index, as well as the stamp, is invalid for 0. That way getstack can give a clearer error message for this case.

stdel()

/*
 * stdel
 *
 * Delete an existing stack.
 *
 * Entered by:  st -- stack ticket
 *
 * Exits with:  stack destroyed and its storage freed
 *
 * Exceptions:  "stdel: argument is not a stack"
 *              "stdel: you've already deleted this stack"
 */
void stdel(STICKET st)
{
    int i = getstack("stdel",st);       /* get stack[] index */

    free(stacks[i].ptr);                /* now free the storage */
    stacks[i].ptr   = NULL;             /* nullify ptr */
    stacks[i].stamp = 0;                /* make sure stamp is invalid */
    stacks[i].elts  = 0;                /* for tidiness, # elements = 0 */
    stacks[i].size  = 0;                /* ...size is also 0 */
}
Stdel deallocates an existing stack and returns it to the internal storage pool.
  1. The same stack cannot be deleted twice.
    Remember that each stamp ticket is unique. Hence once a stack has been deleted, its stamp will never be reused. This makes it easy to check for "dangling references" to old stacks, i.e., old stack tickets that are lying around but which no longer refer to anything. If the user tries to deallocate the same stack twice, getstack notices that the ticket doesn't refer to any existing stack and gives an appropriate error message.
  2. Getstack checks for a NULL pointer, so we don't have to do it here.
    Still, it wouldn't hurt to add another consistency check just before the free. That way if something did go wrong, we'd get a clear error message -- giving the line number and file -- instead of the ubiquitous segmentation fault message.
  3. Cleaning up the stacks[] information.
    It makes debugging a lot easier if you explicitly get rid of old trash. This way, if the programmer ever needs to debug the stacks module, it will be very clear which stacks are in use and which are not. If we left old values around in these array variables, it would be hard to tell "live" stack elements from ones which have been deallocated. Furthermore, setting the stamp to zero makes it very clear that this stack is no longer alive. Since stack tickets never contain 0 stamps, no outstanding stack ticket can possibly match this (invalid) stack. Finally, setting the size and elts variables to zero let us easily implement the stacksize function. It doesn't have to know anything about which stacks are in use and which are not. It just sums up size and elts variables for each stack.

push()

/*
 * push
 *
 * Push an integer onto a stack.
 *
 * Entered by:  st -- stack ticket
 *              n  -- integer to push
 *
 * Exits with:  'n' pushed onto stack 'st'
 *
 * Exceptions:  "push: stack is full"
 *              "push: argument is not a stack"
 *              "push: you've already deleted this stack"
 */
void push(STICKET st,int n)
{
    int i = getstack("push",st);        /* get stack[] index */

    if (stacks[i].nelt==stacks[i].size) /* error if stack is full */
        stkerror("push","stack is full (%d elements)",stacks[i].size);

    stacks[i].nelt++;                   /* keep track of # elements */

    *(stacks[i].ptr++) = n;             /* push n onto stack */
}
Push is called to push an integer onto a stack.
  1. Checking for parameters that aren't really stacks.
    Half the point of using "stack tickets" instead of pointers is so we can check whether the user is screwing up and passing a bogus parameter. If so, we should immediately complain. The most likely cause is using a stack without allocating it; but passing an unitilialized variable could also be the problem. In either case, it's good to spot it immediately and give a clear error message.
  2. Avoiding "&" in arguments.
    Here again we design an interface in which parameters are not altered. Remember that in the fragile style, the pointer must be passed with an "&" since it's altered by push(). However we do not need to alter the stack ticket, and so the caller does not need to remember to add "&" to one of the arguments.
  3. Checking for stack overflow.
    We give a clear, explicit message the moment that the caller tries to push more onto a stack than it can hold. Contrast this with the fragile version which gives no error message at all, but rather trashes some unrelated piece of memory -- with results that will only show up later, in other parts of the program. The robust version is vastly easier to debug!

pop()

/*
 * pop
 *
 * Pop an integer from a stack.
 *
 * Entered by:  st -- stack ticket
 *
 * Exits with:  pops stack
 *              returns popped number
 *
 * Exceptions:  "pop: stack is empty"
 *              "pop: argument is not a stack"
 *              "pop: you've already deleted this stack"
 */
int pop(STICKET st)
{
    int i = getstack("pop",st);         /* get stack[] index */

    if (stacks[i].nelt==0)              /* error if stack is empty */
        stkerror("pop","stack is empty",0);

    stacks[i].nelt--;                   /* keep track of # elements */

    return *(--stacks[i].ptr);          /* pop element from stack */
}
Pop is called to pop an integer from a stack.
  1. Checking for parameters that aren't really stacks.
    Half the point of using "stack tickets" instead of pointers is so we can check whether the user is screwing up and passing a bogus parameter. If so, we should immediately complain. The most likely cause is using a stack without allocating it; but passing an unitilialized variable could also be the problem. In either case, it's good to spot it immediately and give a clear error message.
  2. "Functional" programming style.
    Here again we design an interface in which parameters are not altered. Remember that in the fragile style, both pointer and returned value must be passed with an "&" since they are altered. However we do not need to alter the stack ticket, and we return the popped value, and so the caller does not need to remember to add "&" to any of the arguments.
  3. Checking for stack underflow.
    We give a clear, explicit message the moment that the caller tries to pop from an empty stack. Contrast this with the fragile version which gives no error message at all, but rather returns some meaningless value. The robust version is vastly easier to debug!

stacksize()

/*
 * stacksize
 *
 * Return the size and number of elements currently allocated for
 * some stack. Makes it easier for users to write modular routines.
 * If st==0, all stacks. Handy for users who want to make sure that
 * they have freed all storage that they've allocated.
 *
 * Entered by:  st -- stack ticket; or 0
 *
 * Exits with:  *size -- space allocated for stack(s)
 *              *elts -- number of elements stored in stack(s)
 *
 * Exceptions:  "stacksize: argument is not a stack"
 *              "stacksize: trying to access a deleted stack"
 */
void stacksize(STICKET st,long int *size,long int *elts)
{
    int i;

    if (st!=0) {                        /* if user wants particular stack... */
        i = getstack("stackspace",st);  /* get stack index */
        *size = stacks[i].size;         /* return its size */
        *elts = stacks[i].elts;         /* and number of elements */
        return;
    }

    for (i=0,*size=0,*elts=0; i<NSTACKS; i++) {    /* add up every stack */
        *size += stacks[i].size;
        *elts += stacks[i].elts;
    }
}
Stacksize lets the user find out how much storage is set aside for one stack, or all stacks, and how many elements are actually in use in said stack(s). This is primarily to make debugging easier. It lets the user check to make sure that, say, all stacks really have been deleted, or that a stack really is empty when he/she thinks it is.

This is a rather rudimentary debugging aid. One can think of many other nice possibilities: for instance, a routine that lets you find out how many stacks are in existence, etc. But we simply want to point out that supplying a few simple debugging tools can make the module's user much happier. And after all, you may well be the user!


You can also see this document as a RTF document, a Postscript document, or a plain ASCII text document.
Send email to [email protected].

Department of Computer Science
University of California at Davis
Davis, CA 95616-8562



Page last modified on 11/26/97