Dumb bugs
Volume 9
Number 11
Column Tags State of the Industry
Glenn’s Philosophy on Programming
Tricks to avoid wasting time on “dumb” bugs
By G. Weinreb, Somerville, Massachusetts
The most important thing in software development is that the programmer must
love their code. If the programmer does not love his code, coding becomes a chore as
opposed to a joy, causing code quality, coding productivity, and the programmer's
mental state to degrade. In order for a programmer to love their code, ALL of the
following must be true:
• The code must be well organized with specific functionality localized to one area,
as opposed to appearing multiple times.
• ALL routines must be extremely well tested.
• ALL routines must be extremely well documented.
• Many routines must contain defensive code, e specially the low level ones.
• The programmer must have a decent machine with which to develop.
• The programmer must have a decent development environment and a decent
understanding of how to use it.
• The programmer must have a good debugger. Working with complex black boxes,
into which you cannot peer, is a hell that I would not wish upon even the fiercest
of competitors.
The fundamental principle behind programming is that each issue must be
localized to one area (e.g. one function); and that this one area do it's job extremely
well (i.e. employ defensive code, include outstanding documentation, and be extremely
well tested). And then all the other routines are to rely on ONE to implement the
specific functionality. If similar functionality is copied and implemented in different
places, then the programmer has less time to make each copy solid. Unsolid (non
defensive, non documented, non tested) code leads to the following scenario:
A programmer is programming one function that calls another, and finds that the
called one is not working well. So the programmer stops what he/she is doing and goes
to fix the lower level routine. Now, if it is undocumented, the programmer needs to
read each line, figure out what it's doing, and then document it - which can take more
time than coding the thing from scratch. Therefore, undocumented code is in a sense
disposable - you use it once and then throw it away and get another (like a Kleenex)
when you need to change it. So the poor programmer documents the routine, tests it
and makes it solid. But then we find that many other routines call this function, and
our making it solid has changed it a little, causing it to not work for some of it's other
callers. Subsequently, the programmer must fight to make it work in all cases -
which requires coding in other areas - which can lead to new frontiers. And much time
goes by while the poor programmer has accomplished very little on the original
routine. And subsequently, they find it very hard to love their code.
So,
• Code MUST be well documented, which includes:
+ function headers for all functions
+ descriptive headers for each file
+ descriptions of passed arguments
+ documentation in the code which describes what it does and how it does it
• Code must be thoroughly tested because the devil is in the details and the details
are what will bite you. Subsequently, you've got to DIG, DIG, DIG to uncover
those subtle little problems. And the key to this is to have the confidence that
they exist, else you will not dig for them - SO ASSUME THEY EXIST AND DIG, DIG,
DIG.
• Code MUST be defensive in order to get ahead of the bugs. Defensive coding entails
looking for problems and showing an alert if one is found. Several examples:
a) Arguments passed to low level routines (i.e. the one's called by many other
routines) are checked if doing development. A global variable, 'gDevelopment', is
set TRUE when doing development, and at any time, the user can press an Option
key to toggle the state of 'gDevelopment' TRUE/FALSE. 'gDevelopment' is
typically used in the following way:
/* 1 */
void SetUserItem (
DialogPtr dialog, // ptr to dialog box
short itemNr, // item's DITL #
ProcPtr doDraw) // procedure pointer for
// user DITL item
{
if (gDevelopment) { // if doing develop...
if (IfPtrIsNullOrBadYell ((Ptr) dialog, 1))
return; // test for null or bad ptr
if (IfDitlNrIsBadYell (itemNr, TRUE))
return; // test for bad ditl #
if (IfPtrIsNullOrBadYell ((Ptr) doDraw, 1))
return; // test for null or bad ptr
}
body .....
body .....
}
Subsequently, if there is trouble, the programmer knows about the problem
before it causes harm. And bugs can cause harm in a manner which is not obvious
and/or a manner which does not lead the programmer to the bad code (e.g. a bug in area
A destroys memory in area B, which crashes 100 mouse clicks latter). Defensive
coding is like being at the window with a shotgun when the burglar climbs in - "Hey,
what the hell do you think you're doing?". Programmers must get ahead of bugs, not
behind, in order to love their code.
Someone playing devil's advocate might say, "Doesn't it take time to install
defensive code and doesn't it slow your program?". It does take some time to
implement, yet not much since it involves mindless cut/copy/paste of similar existing
code fragments. The time for the processor to check for 'gDevelopment' TRUE is less
than a microsecond (for perspective, HLock() costs 121µs on a Mac IIcx).
Subsequently, adding "if (gDevelopment)" increases execution time by only a tiny
amount. Freeing the programmer from fighting bugs will give him/her more time to
optimize code - which will save orders of magnitude more execution time.
b) One can use macros to add defensive coding to toolbox routines. e.g.
/* 2 */
#define HLock_(h)
if (gDevelopment) {
if (CheckHandle((Handle) h, TRUE, TRUE))
HLock((Handle) h);
}
else if (h) {
HLock((Handle) h);
}
This simple little macro has saved my life on a number of occasions.
c) We will define the phrase "memory problem" as code which writes to memory
where it should not, and possibly causes trouble in an unrelated area and/or in an
intermittent way - both of which are infamous to programmers. In the case of a
"memory problem", one must have a strategy for dealing with it (other than
fumbling around and saying, "Gee's, this really IS a tough one."). One technique
is to use a global, 'gShakeLikeHell', which is toggled TRUE/FALSE by pressing an
Option key (it is always set to FALSE when you first launch the application), and
a macro defined as follows:
/* 3 */
#define SHAKELIKEHELLIFSHAKING
if (gShakeLikeHell)
ShakeLikeHellifShaking();
ShakeLikeHellifShaking() compacts the heap, creates handles, disposes of
handles, and tests existing data structures. If you have an intermittent or nebulous
bug, the best response is to install the SHAKELIKEHELLIFSHAKING macro throughout
your code under development, run the application, and press the Option key to turn it
on. Chances are you will crash, or see the problem sooner (i.e. closer to the bad code).
Ideally you would like to isolate the problem area between 2
SHAKELIKEHELLIFSHAKING macros (which is a mechanism which identifies if the
problem does or does not originate from a specified range of code). After installing the
macro, you can leave it in your code - it cost less than a microsecond. [Except for the
time spent in the ShakeLikeHellifShaking routine. - Tech. Ed]. Installing this macro
may seem silly, yet I can attest it has worked beautifully on many occasions -
encouraging me to love my code.