Subtle Bugs

It wouldn't surprise me for people to wonder how it’s possible for reasonably competent programmers to introduce bugs by making seemingly innocuous changes to some program’s code. The short answer is that we’re often too clever for our own good. We’ll employ little tricks that subtly change the semantics of the data that we’re manipulating. These small changes in semantics can result in weird bugs.

There’s a good example of this in the sample code that Apple distributes with the developer tools for Mac OS X. One bit of sample code implements a rudimentary framework for Carbon Human Interface objects. Among this code is the implementation of a wrapper C++ class for Carbon Events. The following is a method implemented in this class. It extracts a Boolean parameter from a Carbon Event, and returns it in a pointer to a bool.

//-----------------------------------------------------------------------------------

//     GetParameter

//-----------------------------------------------------------------------------------

//

OSStatus

TCarbonEvent::GetParameter(

    EventParamName  inName,

    bool*           outValue )

{

    return GetParameter<Boolean>( inName, sizeof( Boolean ), (Boolean*) outValue );

}

For my non-programming readers, both a Boolean and a bool represent values in Boolean algebra. Boolean algebra is the algebra of logic, and Boolean variables take on only one of two values: true or false. In computing practice, and in the C++ Language, false is equivalent to 0, and true is any non-zero value.

The problem, however, is that Boolean and bool are different language types. Boolean represents the traditional Macintosh implementation of the concept, while bool is the concept as specified in the C++ Language standard. Boolean is type-equivalent to an unsignedchar—a single-byte quantity, while the underlying implementation of bool can change from one compiler to the next. In Metrowerks, for example, bool is usually type equivalent to an unsignedint—a four-byte quantity.

And therein lies the problem. The code above blandly takes a pointer to a four-byte quantity and passes it to a piece of code that expects a pointer to a single-byte quantity. This would not be a problem on Intel processors, but the PowerPC is big endian. On a big endian system, when you take the address of a four-byte quantity the resulting pointer actually points to the highest-order byte (i.e. the “big” end of the four-byte quantity) rather than the lowest-order byte (i.e. the “little” end of the four-byte quantity).

Suppose the called code uses the number 1 as a constant value representing true. If the requested parameter of the wrapped Carbon event is true, then this implementation of GetParameter will put the number 1 in the high-order byte of outValue. Instead of getting the number 1, then, outValue actually gets the number 16777216 (or 0x1000000 hexadecimal).

Now, in terms of language semantics, there’s nothing specifically wrong with this notion. Both numbers, 1 and 16777216, being non-zero values are valid representations for true. However, most compilers favor using the number 1 to represent the actual value for the constant true, and “clever” programmers often take advantage of this fact, and will use the result of a Boolean expression in arithmetic expecting a true result to be the same as the number 1.

If some other programmer has written a piece of code that takes advantage of this tendency to use 1 to represent true, the implementation above breaks that code. This is how subtle bugs enter programs: two programmers being “clever” in mutually exclusive ways.

The immediate problem with the above code is that it doesn’t make its cleverness explicit. One way to do this is to add a CompileAssert:

#define CompileAssert(_expr) extern int _compile_assert_failed[!!(_expr)]

//-----------------------------------------------------------------------------------

//     GetParameter

//-----------------------------------------------------------------------------------

//
OSStatus

TCarbonEvent::GetParameter(

    EventParamName  inName,

    bool*           outValue )

{

    CompileAssert( sizeof(bool) >= sizeof(Boolean) );

    return GetParameter<Boolean>( inName, sizeof( Boolean ), (Boolean*) outValue );

}

Adding the CompileAssert not only makes the assumption explicit, it will generate a compiler error should we ever move this code to a system where the assumption is no longer valid. But that doesn’t really resolve the underlying problem.

Another attempt to resolve the problem might to replace the C-style cast with a C++ explicit static_cast:

//-----------------------------------------------------------------------------------

//     GetParameter

//-----------------------------------------------------------------------------------

//
OSStatus

TCarbonEvent::GetParameter(

    EventParamName  inName,

    bool*           outValue )

{

    return GetParameter<Boolean>( inName,

                                  sizeof( Boolean ),

                                  static_cast<Boolean*> (outValue) );

}

This actually works provided that our particular compiler defines bool in a manner that’s type equivalent to Boolean. It has the added benefit that it will generate a compiler error should the underlying type-equivalence ever change. Unfortunately, if we’re already working with a compiler where Boolean and bool are not type equivalent, then our code no longer compiles. Metrowerks, for example, generates a compiler error with the message, illegal explicit type cast from bool* to Boolean*.

For my money, the best solution is the pedantically correct solution:

//-----------------------------------------------------------------------------------

//     GetParameter

//-----------------------------------------------------------------------------------

//
OSStatus

TCarbonEvent::GetParameter(

    EventParamName  inName,

    bool*           outValue )

{

    OSStatus result;

    Boolean value;

    result = GetParameter<Boolean>( inName, sizeof( Boolean ), &value );

    if (noErr == result)

       *outValue = value != 0;

    return result;

}

Note that this solution doesn’t simply assign value to the location in memory pointed to by outValue. Rather it assigns the result of the Boolean expression value != 0 to value.

When you work on a large software project with a number of other programmers, being so pedantically correct as to use real Boolean expressions for Boolean results will often mean the difference between code that works and plays well with others and code that introduces subtle bugs that can take you hours to track down, or worse, months before users actually discover that the bugs exist.

 

Rick

Currently playing in iTunes: Rhyme & Reason by Dave Matthews Band