Lecture #12: Robust Programming
Reading: none
Handouts: "Bomb-Proof" Code
- Greetings and felicitations
- Four principles
- paranoia
- assume maximum stupidity
- don't hand out dangerous implements
- worry about "impossible" cases
- Discuss design of stack example
- access stacks through tickets containing index and "generation" number
- walk through handout discussing each point
"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:
- 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.
- 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!
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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:
- 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.
- 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?
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- "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.
- 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.
- 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.
- 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.
- Explicitly giving the maximum stack size in the error message.
The remarks given under comment 3 apply here as well.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- "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.
- 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