Assert, Assert, Assert, and Assert

[Previous] [Next]

I hope that most of you already know what an assertion is, because it's the most important proactive programming tool in your debugging arsenal. For those who are unfamiliar with the term, here's a brief definition: an assertion declares that a certain condition must be true at a specific point in a program. The assertion is said to fail if the condition is false. You use assertions in addition to your normal error checking. Traditionally, assertions are functions or macros that execute only in debug builds and bring up a message box telling you what condition failed. I extend the definition of assertions to include conditionally compiled code that checks conditions and assumptions that are too complex for a general assertion function or macro to handle. Assertions are a key component of proactive programming because they help developers and test engineers determine not just that bugs are present but also why the errors are happening.

Even if you've heard of assertions and drop them in your code occasionally, you might not be familiar enough with them to use them effectively. Engineers can never be too rich or too thin—or use too many assertions. The rule of thumb I've always followed to judge whether I've used enough assertions is simple: I have enough assertions when my junior coworkers complain that they get multiple message boxes reporting assertion failures whenever they call into my code with invalid information or assumptions.

If used sufficiently, assertions will tell you most of the information that you need to diagnose a problem at the first sign of trouble. Without assertions, you'll spend considerable time in the debugger working backward from the crash searching for where things started to go wrong. A good assertion will tell you where and why a condition was invalid. A good assertion will also let you get into the debugger after a condition fails so that you can see the complete state of the program at the point of failure.

A side benefit of using plenty of assertions is that they serve as additional documentation in your code. Although assertions will never replace thorough comments, they can serve as reminders to others who are maintaining your code about what you were expecting for function data.

How and What to Assert

My stock answer when asked what to assert is to assert everything. You should assert any condition because it might be the one you need to solve a nasty bug in the future. Don't worry that putting in too many assertions will hamper your program's performance—assertions usually are active only in debug builds, and the bug-finding opportunities created more than outweigh the small performance hit.

Before jumping into examples of how to use assertions and what to assert, I need to point out that assertions should never change any variables or states of a program. Treat all data you check in assertions as read-only. Because assertions are active only in debug builds, if you do change data with an assertion, you'll have different behavior between debug and release builds and tracking down the difference will be extremely difficult.

How to Assert

The first rule to using assertions is to check a single item at a time. If you check multiple conditions with just one assertion, you have no way of knowing which condition caused the failure. In the following example, I show the same function with two assertion checks. Although the assertion in the first function will catch a bad parameter, the assertion won't tell you which condition failed or even which of the three parameters is the offending one.

// The wrong way to write an assertion. Which parameter was bad? BOOL GetPathItem ( int i , LPTSTR szItem , int iLen ) {    ASSERT ( ( i > 0                                          ) &&             ( NULL != szItem                                 ) &&             ( ( iLen > 0 ) && ( iLen < MAX_PATH )            ) &&             ( FALSE == IsBadWriteStringPtr ( szItem , iLen ) ) ) ;     } // The proper way. Each parameter is checked individually so that you // can see which one failed. BOOL GetPathItem ( int i , LPTSTR szItem , int iLen ) {    ASSERT ( i > 0 ) ;    ASSERT ( NULL != szItem ) ;    ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;    ASSERT ( FALSE == IsBadWriteStringPtr ( szItem , iLen ) ) ;     }

When you assert a condition, you need to strive to check the condition completely. For example, if your function takes a pointer to a function as a parameter and you simply check that parameter against NULL, you're checking only part of the error condition. If you have a stack overrun that overwrites the function parameter and the value changes to 1, an assertion such as the following wouldn't fail but you'd still crash later.

// An example of checking only a part of the error condition BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback ) {     ASSERT ( NULL != pfnCallback ) ;      }

You can check the full condition by using the IsBadCodePtr application programming interface (API) function to completely validate the pointer.

// An example of completely checking the error condition BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback ) {     ASSERT ( FALSE == IsBadCodePtr ( pfnCallback ) ) ;      }

Another step I always take is to ensure that I'm asserting against specific values. The following example shows an incorrect way to check for a positive value and then the correct way.

' Example of a poorly written assertion: nCount should be positive, ' but the assertion doesn't fail when nCount is negative. Function UpdateListEntries(ByVal nCount As Integer) as Integer     Debug.Assert nCount      End Function ' A proper assertion that explicitly checks against what the value ' is supposed to be Function UpdateListEntries(ByVal nCount As Integer) as Integer     Debug.Assert nCount > 0      End Function

The incorrect sample essentially checks only whether nCount isn't 0, which is only half the information that needs to be asserted. By explicitly checking the acceptable values, you guarantee that your assertion is self-documenting, and you also ensure that your assertion catches corrupted data.

C and C++ have all sorts of functions that can help make your assertions as descriptive as possible. Table 3-1 shows the helper functions that you can use to check exactly the condition needed. You can call these functions from Microsoft Visual Basic, but Visual Basic does a good job of avoiding pointer problems, so you might not need to use them.

Table 3-1 Helper Functions for Descriptive C and C++ Assertions

Function Description
GetObjectType A graphics device interface (GDI) subsystem function that returns the type for a GDI handle
IsBadCodePtr Checks that the memory pointer can be executed
IsBadReadPtr Checks that the memory pointer is readable for the specified number of bytes
IsBadStringPtr Checks that the string pointer is readable up to the string's NULL terminator or the maximum number of characters specified
IsBadWritePtr Checks that the memory pointer is writable for the specified number of bytes
IsWindow Checks whether the HWND parameter is a valid window

IsBadStringPtr and IsBadWritePtr are not thread-safe. While one thread calls IsBadWritePtr to check the access permissions on a piece of memory, another thread could be changing the access permissions. If you're using either function just to check normal C run-time heap allocated memory, you shouldn't have any problems. However, if your application updates page permissions and does other advanced memory manipulations, you should provide your own thread-safe versions of IsBadStringPtr and IsBadWritePtr.

Visual Basic has its own set of functions that can help you validate Visual Basic-specific conditions. All those great Is functions are listed in Table 3-2. If you follow the good Visual Basic programming practice of not using Variants and explicitly specifying ByVal and ByRef for all your parameters, you might not need to validate variable types that often; if you do need to, however, at least you have some rich means to do it.

Table 3-2 Helper Functions for Descriptive Visual Basic Assertions

Function Description
IsArray Checks whether the variable is an Array type
IsDate Checks whether the variable can be converted to a date
IsEmpty Checks whether the Variant variable has been initialized
IsError Checks whether the variable is an error value
IsMissing Checks whether the optional Variant argument was passed to the procedure
IsNull Checks whether the Variant variable is Null
IsNumeric Checks whether the variable can be converted to a numeric type
IsObject Checks whether the variable is an object
TypeName Returns the type name for the variable

The following code shows one of the mistakes that I used to make with my assertions.

// Poor assertion usage BOOL CheckDriveFreeSpace ( LPCTSTR szDrive ) {     ULARGE_INTEGER ulgAvail ;     ULARGE_INTEGER ulgNumBytes ;     ULARGE INTEGER ulgFree ;     if ( FALSE == GetDiskFreeSpaceEx ( szDrive       ,                                        &ulgAvail     ,                                        &ulgNumBytes  ,                                        &ulgFree       ) )     {        ASSERT ( FALSE ) ;        return ( FALSE ) ;     }      }

Although I was using an ASSERT, which is good, I wasn't showing the condition that failed. The assertion message box will show just the expression "FALSE," which isn't that helpful. When using an assertion, you want to try to get as much information about the assertion failure in the message box as possible.

My friend Dave Angel pointed out to me that in C and C++ you can just use the logical-NOT operator (!) and use a string as its operand. This combination will give you a much better expression in the assertion message box so that you at least have an idea of what failed without looking at the source code. The following example shows the proper way to assert a false condition. Unfortunately, Dave's trick doesn't work in Visual Basic.

// Proper assertion usage BOOL CheckDriveFreeSpace ( LPCTSTR szDrive ) {     ULARGE_INTEGER ulgAvail ;     ULARGE_INTEGER ulgNumBytes ;     ULARGE INTEGER ulgFree ;     if ( FALSE == GetDiskFreeSpaceEx ( szDrive      ,                                        &ulgAvail    ,                                        &ulgNumBytes ,                                        &ulgFree      ) )     {          ASSERT ( !"GetDiskFreeSpaceEx failed!" ) ;          return ( FALSE ) ;     }      }

You can also extend Dave's assertion trick by using the logical-AND conditional operator (&&) to perform a normal assertion and still have the message text. The following example shows how.

 BOOL AddToDataTree ( PTREENODE pNode ) {   ASSERT ( ( FALSE == IsBadReadPtr ( pNode , sizeof ( TREENODE) ) ) &&             "Invalid parameter!"                  ) ;      }

What to Assert

Now that you have an idea of how to use assertions, let's turn to what you need to assert. As you saw in the examples above, you need to assert the parameters coming into a function. Asserting parameters is especially critical with interface functions and public class members that others on your team call. Because those gateway functions are the entry points into your code, you want to make sure that each parameter and assumption is valid.

In Chapter 4, I write an example debugger to give you an idea of how debuggers work. I set up a dynamic-link library (DLL) that contains the debug loop. One of the key functions in that DLL is the StopDebugging function. In the following code snippet, I show the assertion for the parameter to the function. Notice that the assertion comes first and that the real error handling immediately follows the assertion. Remember that assertions in no way replace any normal error handling.

 BOOL DEBUGINTERFACE_DLLINTERFACE __stdcall     StopDebugging ( LPHANDLE lpDebugSyncEvents ) {     ASSERT ( FALSE ==                 IsBadWritePtr ( lpDebugSyncEvents ,                                 sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) );     if ( TRUE == IsBadWritePtr ( lpDebugSyncEvents ,                                  sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) )     {        SetLastError ( ERROR_INVALID_PARAMETER ) ;        return ( FALSE ) ;     }     // Signal the debug thread with the event name to close.     VERIFY ( SetEvent ( lpDebugSyncEvents[ CLOSEDEBUGGER ] ) ) ;     return ( TRUE ) ; }

As you move inside your module, the parameters of the module's private functions might not require as much checking, mainly depending on where the parameters originated. Much of the decision about which parameters to validate comes down to a judgment call. It doesn't hurt to assert every parameter of every function, but if a parameter comes from outside the module, and if you fully asserted it once, you might not need to again. By asserting each parameter on every function, however, you can possibly catch some errors internal to your module.

I sit right in the middle of the two extremes. Deciding how many parameter assertions are right for you just takes some experience. As you get a feel for how you program and learn where you typically encounter problems in your code, you'll figure out where and when you need to assert parameters internal to your module. One safeguard I've learned to use is to add parameter assertions whenever a bad parameter blows up my code. That way, the mistake won't get repeated because the assertion will catch it.

Another area in which I routinely use assertions is on return values to functions called in the normal processing flow. Asserting the return values alerts you to problems as they're happening. I tend to check almost every return value with an assertion. In Listing 3-1, which is the StartDebugging function from the debugger in Chapter 4, I assert various return values that cause me to fail the function.

Listing 3-1 Examples of assertions on return values

 HANDLE DEBUGINTERFACE_DLLINTERFACE __stdcall StartDebugging ( LPCTSTR szDebuggee , LPCTSTR szCmdLine , LPDWORD lpPID , CDebugBaseUser * pUserClass , LPHANDLE lpDebugSyncEvents ) { // Assert the parameters. ASSERT ( FALSE == IsBadStringPtr ( szDebuggee , MAX_PATH ) ) ; ASSERT ( FALSE == IsBadStringPtr ( szCmdLine , MAX_PATH ) ) ; ASSERT ( FALSE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) ) ; ASSERT ( FALSE == IsBadReadPtr ( pUserClass , sizeof ( CDebugBaseUser * ) ) ) ; ASSERT ( FALSE == IsBadWritePtr ( lpDebugSyncEvents , sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) ) ; // Check them all for real. if ( ( TRUE == IsBadStringPtr ( szDebuggee , MAX_PATH ) ) || ( TRUE == IsBadStringPtr ( szCmdLine , MAX_PATH ) ) || ( TRUE == IsBadWritePtr ( lpPID , sizeof ( DWORD ) ) ) || ( TRUE == IsBadReadPtr ( pUserClass , sizeof ( CDebugBaseUser * ) ) ) || ( TRUE == IsBadWritePtr ( lpDebugSyncEvents , sizeof ( HANDLE ) * NUM_DEBUGEVENTS ) ) ) { SetLastError ( ERROR_INVALID_PARAMETER ) ; return ( INVALID_HANDLE_VALUE ) ; } // The handle of the startup acknowledgment that this function // will wait on until the debug thread gets started HANDLE hStartAck ; // The string used for the startup acknowledgment event TCHAR szStartAck [ MAX_PATH ] ; // Load up the string for startup acknowledgment. if ( 0 == LoadString ( GetDllHandle ( ) , IDS_DBGEVENTINIT , szStartAck , sizeof ( szStartAck ) ) ) { ASSERT ( !"LoadString IDS_DBGEVENTINIT failed!" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Create the startup acknowledgment event. hStartAck = CreateEvent ( NULL , // Default security TRUE , // Manual-reset event FALSE , // Initial state=Not signaled szStartAck ) ; // Event name ASSERT ( FALSE != hStartAck ) ; if ( FALSE == hStartAck ) { TRACE ( "StartDebugging : Unable to create Start Ack event\n" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Bundle up the parameters. THREADPARAMS stParams ; stParams.lpPID = lpPID ; stParams.pUserClass = pUserClass ; stParams.szDebuggee = szDebuggee ; stParams.szCmdLine = szCmdLine ; // The handle to the debug thread HANDLE hDbgThread ; // Try to create the thread. hDbgThread = (HANDLE)_beginthread ( DebugThread , 0 , &stParams ) ; ASSERT ( NULL != hDbgThread ) ; if ( NULL == hDbgThread ) { TRACE ( "StartDebugging : _beginthread failed\n" ) ; VERIFY ( CloseHandle ( hStartAck ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Wait until the debug thread gets good and cranking. ::WaitForSingleObject ( hStartAck , INFINITE ) ; // Get rid of the acknowledgment handle. VERIFY ( CloseHandle ( hStartAck ) ) ; // Check that the debug thread is still running. If it isn t, // the debuggee probably couldn t get started. DWORD dwExitCode = ~STILL_ACTIVE ; if ( FALSE == GetExitCodeThread ( hDbgThread , &dwExitCode ) ) { ASSERT ( !"GetExitCodeThread failed!" ) ; return ( INVALID_HANDLE_VALUE ) ; } ASSERT ( STILL_ACTIVE == dwExitCode ) ; if ( STILL_ACTIVE != dwExitCode ) { TRACE ( "StartDebugging : GetExitCodeThread failed\n" ) ; return ( INVALID_HANDLE_VALUE ) ; } // Create the synchronization events so that the main thread can // tell the debug loop what to do. BOOL bCreateDbgSyncEvts = CreateDebugSyncEvents ( lpDebugSyncEvents , *lpPID ) ; ASSERT ( TRUE == bCreateDbgSyncEvts ) ; if ( FALSE == bCreateDbgSyncEvts ) { // This is a serious problem. I got the debug thread going, but // I was unable to create the synchronization events that the // user interface thread needs to control the debug thread. My // only option here is to punt. I ll kill the // debug thread and just return. I can t do much else. TRACE ( "StartDebugging : CreateDebugSyncEvents failed\n" ) ; VERIFY ( TerminateThread ( hDbgThread , (DWORD)-1 ) ) ; return ( INVALID_HANDLE_VALUE ) ; } // Life is good! return ( hDbgThread ) ; } 

The final time to use assertions is when you need to check an assumption. For example, if the specifications for a function say that it requires 3 MB of disk space, you should check this assumption with an assertion. Here's another example: if your function takes an array of pointers to a specific data structure, you should walk through the data structure and validate that each individual item is valid.

In both these cases, as with most assumption assertions, you can't check the assumption with a general function/macro. In these situations, you need to use the conditional compilation technique that I indicated earlier should be part of your assertion toolkit. Because the code you'll be executing in the conditional compilation will be working on live data, you must take extra precautions to ensure that you don't change the state of the program. In both Microsoft Visual C++ and Visual Basic programs, I prefer to implement these types of assertions in separate functions if possible. That way, you avoid changing any local variables inside the original function. Additionally, the conditionally compiled assertion functions can come in handy in the Watch window, as you'll see in Chapter 5 when we talk about the Visual C++ debugger. The following example shows a conditionally compiled assertion function, ValidatePointerArray, which does deep validations on an array of data.

#ifdef _DEBUG void ValidatePointerArray ( STDATA * pData , int iCount ) {     // Check the array buffer first.     ASSERT ( FALSE == IsBadReadPtr ( pData ,                                      iCount * sizeof ( STDATA * ) ) ) ;     for ( int i = 0 ; i < iCount ; i++ )     {         ASSERT ( pData[ i ].bFlags < DF_HIGHVAL ) ;         ASSERT ( FALSE == IsBadStringPtr ( pData[ i ].pszName ,                                            MAX_PATH ) ) ;     } } #endif void PlotDataItems ( STDATA * pData , int iCount ) { #ifdef _DEBUG     ValidatePointerArray ( pData , iCount ) ; #endif      }

The VERIFY Macro

Before we get into the various assertion macros and functions you'll encounter in Microsoft Windows development, and some of the problems with them, I want to talk about the VERIFY macro that's used in Microsoft Foundation Class (MFC) library development. In a debug build, the VERIFY macro behaves the same way as a normal assertion. If the condition evaluates to 0, the VERIFY macro triggers the normal assertion message box to warn you. Unlike a normal assertion, however, in a release build, the parameter to the VERIFY macro stays in the source code and is evaluated as a normal part of processing.

In essence, the VERIFY macro allows you to have normal assertions with side effects, and those side effects stay in release builds as well. Ideally, you should never use conditions to any type of assertion that cause any side effects. However, in one situation the VERIFY macro is useful—when you have a function that returns an error value that you wouldn't check otherwise. For example, when you call ResetEvent to clear a signaled event handle and the call fails, there's not much you can do, which is why most engineers call ResetEvent and never check the return value in either debug or release builds. If you wrap the call with the VERIFY macro, at least you'll be notified in your debug builds that something went wrong. Of course, I could achieve the same results by using ASSERT, but VERIFY saves me the trouble of creating a new variable just to store and verify the return value of the ResetEvent call—a variable that would probably be used only in debug builds anyway.

I think most MFC programmers use the VERIFY macro for convenience, but you should try to break yourself of the habit. In most cases, when engineers use the VERIFY macro, they should be checking the return value instead. A good example of where everyone seems to use VERIFY is around the CString::LoadString member function, which loads resource strings. Using VERIFY this way is fine in a debug build because if LoadString fails, the VERIFY macro warns you. In a release build, however, if LoadString fails, you end up using an uninitialized variable. If you're lucky, you'll just have a blank string, but most of the time, you'll crash in your release build. The moral of this story is to check your return values. If you're about to use a VERIFY macro, you need to ask whether ignoring the return value will cause you any problems in release builds.

The Different Types of Visual C++ and Visual Basic Assertions

Even though I define all my C++ assertion macros and functions to just plain ASSERT, which I'll talk about in a moment, I want to quickly go over the different types of assertions available in Visual C++ and Visual Basic and provide a little information about their implementation. That way, if you see one of them in someone else's code, you can recognize it. Additionally, I want to alert you to the problems with some of the implementations.

assert, _ASSERT, and _ASSERTE

The first type of assertion is from the C run-time library, the ANSI C standard assert macro. This version is portable across all C compilers and platforms and is defined by including ASSERT.H. In the Windows world, if your application is a console application and it fails an assertion, assert will send the output to stderr. If your application is a Windows graphical user interface (GUI) application, assert will show the assertion failure as a message box.

The second type of assertion in the C run-time library is specific to Windows. These assertions are _ASSERT and _ASSERTE, which are defined in CRTDBG.H. The only difference between the two is that the _ASSERTE version also prints the expression passed as its parameter. Because the expression is so important to have, especially when your test engineers are testing, if you're using the C run-time library, you should always use _ASSERTE. Both macros are part of the extremely useful debug run-time library code, and the assertions are only one of its many features.

Debugging War Story
Disappearing Files and Threads

The Battle

While working on a version of NuMega's BoundsChecker, we had an incredibly difficult problem with random crashes that were almost impossible to duplicate. The only clues we had were that file handles and thread handles would occasionally become invalid, which meant that files would randomly close and thread synchronization would sometimes break. The user interface (UI) developers were also experiencing occasional crashes, but only when running under the debugger. These problems plagued us throughout development and finally escalated to the point that all the developers on the team stopped what they were doing and started trying to solve these bugs.

The Outcome

The team nearly tarred and feathered me because the problem turned out to be my fault. I was responsible for the debug loop in BoundsChecker. In the debug loop, you use the Windows Debugging API to start and control another process, the debuggee, and to respond to debug events the debuggee generates. Being a conscientious programmer, I saw that the WaitForDebugEvent function was returning handle values for some of the debugging event notifications. For example, when a process started under a debugger, the debugger would get a structure that contained a handle to the process and the initial thread for that process.

Because I'm so careful, I knew that if an API gave you a handle to some object and you no longer needed the object, you called CloseHandle to free the underlying memory for that object. Therefore, whenever the Debugging API gave me a handle, I closed that handle as soon as I finished using it. That seemed like the reasonable thing to do.

However, much to my chagrin, I hadn't read the fine print in the Debugging API documentation, which says that the Debugging API itself closes any handles it generates. What was happening was that I was holding some of the handles returned by the Debugging API until I needed them. However, I was closing those same handles after I finished using them—after the Debugging API had already closed them.

To understand how this situation led to our problem, you need to know that when you close a handle, the operating system marks that handle value as available. Microsoft Windows NT 4, the operating system we were using at the time, is particularly aggressive about recycling handle values. (Microsoft Windows 2000 exhibits the same aggressive behavior toward handle values.) Our UI portions, which were heavily multithreaded and opened many files, were creating and using new handles all the time. Because the Debugging API was closing my handles and the operating system was recycling them, sometimes the UI portions would get one of the handles that I was saving. As I closed my copies of the handles later, I was actually closing the UI's threads and file handles!

I was barely able to avoid the tar and feathers because I showed that this bug was also in the debug loop of previous versions of BoundsChecker. We'd just gotten lucky before. What had changed was that the version we were working on had a new and improved UI that was doing much more with files and threads, so the conditions were ripe for my bug to do more damage.

The Lesson

I could have avoided this problem if I'd read the fine print in the Debugging API documentation. Additionally, and this is the big lesson, I learned that you always check the return values to CloseHandle. Although you can't do much when you close an invalid handle, the operating system does tell you when you're doing something wrong, and you should pay attention.

As a side note, I want to mention that if you attempt to double close a handle or pass a bad value to CloseHandle and you're running under a debugger, Windows NT 4 and Windows 2000 will report an Invalid Handle exception (0xC0000008). When you see that exception value, you can stop and explore why it occurred.

The other lesson I learned is that it really helps to be able to out-sprint your coworkers when they're chasing you with a pot of tar and bags of feathers.

Although assert, _ASSERT, and _ASSERTE are convenient to use and free of charge, they do have a few drawbacks. The assert macro has two problems that can cause you some grief. The first is that the filename display truncates to 60 characters, so you can sometimes end up not having any idea what file triggered an assertion. The second problem with assert occurs if you're working on a project that doesn't have a UI, such as a Windows 2000 service or a Component Object Model (COM) out-of-process server. With assert sending its output to stderr or a message box, you can miss the assertion. And in the case of using a message box, your application will hang because you can't dismiss the message box when your UI isn't displayable.

The C run-time implementation macros, on the other hand, address the issue with defaulting to a message box by allowing you to redirect the assertion to a file or to the OutputDebugString API function by calling the _CrtSetReportMode function. All the Microsoft-supplied assertions suffer from one fatal flaw, however: they change the state of the system, which is the cardinal rule that assertions can't break. Having your assertion calls suffer from side effects is almost worse than not using assertions at all.

The following code shows an example of how the supplied assertions can change your state between debug and release builds. Can you spot the problem?

// Send the message over to the window. If it times out, the other // thread is hung, so I need to abort the thread. As a reminder, the // only way to check whether SendMessageTimeout failed is to check // GetLastError. If the function returned 0 and the last error is // 0, SendMessageTimeout timed out. _ASSERTE ( NULL != pDataPacket ) if ( NULL == pDataPacket ) {     return ( ERR_INVALID_DATA ) ; } LRESULT lRes = SendMessageTimeout ( hUIWnd ,                                     WM_USER_NEEDNEXTPACKET ,                                     0                      ,                                     (LPARAM)pDataPacket    ,                                     SMTO_BLOCK             ,                                     10000                  ,                                     &pdwRes                 ) ; _ASSERTE ( FALSE != lRes ) ; if ( 0 == lRes ) {     // Get the last error value.     DWORD dwLastErr = GetLastError ( ) ;     if ( 0 == dwLastErr )     {         // The UI is hung or not processing data fast enough.         return ( ERR_UI_IS_HUNG ) ;     }     // If the error is anything else, there was a problem     // with the data sent as a parameter.     return ( ERR_INVALID_DATA ) ; } return ( ERR_SUCCESS ) ;     

The problem, which is insidious, is that the supplied assertions destroy the last error value. In the case above, the "_ASSERTE ( FALSE != lRes )" would execute, show the message box, and change the last error value to 0. Thus in debug builds, the UI thread always appears to hang, whereas in the release build, you would see the cases in which the parameters passed to SendMessageTimeout were bad.

The fact that the last error value is destroyed with the system-supplied assertions might never be an issue in the code you write, but my own experience has been different—two bugs that took a great deal of time to track down turned out to be related to this problem. Fortunately, if you use the assertion presented later in this section, I'll take care of this problem for you as well as give you some information that the system-supplied version doesn't.

ASSERT_KINDOF and ASSERT_VALID

If you're doing MFC programming, you'll run into two additional assertion macros that are specific to MFC and are fantastic examples of proactive debugging. If you've declared your classes with DECLARE_DYNAMIC or DECLARE_SERIAL, you can use the ASSERT_KINDOF macro to check whether a pointer to a CObject-derived class is a specific class or is derived from a specific class. The ASSERT_KINDOF assertion is just a wrapper around the CObject::IsKindOf method. The following code snippet first checks the parameter in the ASSERT_KINDOF assertion and then does the real parameter error checking.

BOOL DoSomeMFCStuffToAFrame ( CWnd * pWnd ) {     ASSERT ( NULL != pWnd ) ;     ASSERT_KINDOF ( CFrameWnd , pWnd ) ;     if ( ( NULL == pWnd ) ||          ( FALSE == pWnd->IsKindOf ( RUNTIME_CLASS ( CFrameWnd ) ) ) )     {         return ( FALSE ) ;            // Do some MFC stuff; pWnd is guaranteed to be a CFrameWnd or      // to be derived from a CFrameWnd.       }

The second MFC-specific assertion macro is ASSERT_VALID. This assertion resolves down to AfxAssertValidObject, which completely validates that the pointer is a proper pointer to a CObject-derived class. After validating the pointer, ASSERT_VALID calls the object's AssertValid method. AssertValid is a method that you can override in your derived classes so that you can check each of the internal data structures in your class. This method is a great way to do a deep validation on your classes. You should override AssertValid for all your key classes.

Debug.Assert

In some ways, Visual Basic programmers have it easier than C and C++ programmers because Visual Basic doesn't require extensive parameter type and pointer validations as long as you're not using Variant s as parameters. In other ways, however, properly employing proactive programming in Visual Basic has been made much more difficult than it should be. Even though it took four versions, Visual Basic finally has a built-in assertion with Debug.Assert.

That's the good news. The bad news is that Debug.Assert isn't usable when you really need it, which is when you're debugging compiled code. I think that the Visual Basic team made a big mistake in not allowing Debug.Assert to compile into native code. Debug.Assert is available only when running inside the Visual Basic integrated development environment (IDE), and when an assertion fails when debugging, it drops you into the IDE at the Debug.Assert line. Even though Debug.Assert is active only in the IDE, you still need to use it as much as possible so that you can proactively check for problems.

I was all set to solve the problems with Debug.Assert when I ran across the book Advanced Visual Basic 6 (2nd ed., Microsoft Press, 1998) by The Mandelbrot Set, a development company based in England. In that book, Mark Pearce wrote a wonderful Visual Basic add-in called Assertion Sourcerer. It alone is worth the price of the book (though I found the rest of the book excellent as well). Assertion Sourcerer automatically hunts down your Debug.Asserts and puts a call to a real assertion after Debug.Assert. It also calculates the source and line so that you can find out exactly where the problem occurred. In addition to putting the real assertions in your code, Assertion Sourcerer even takes them out when you're done with them!

I found it quite easy to extend Mark's code to look for Debug.Print statements as well and to insert real trace statements for those. Additionally, I use my own VBASSERTANDTRACE.BAS file, shown in Listing 3-2, to supply the implementation for the real assertion and trace statements. I wanted to leverage my own SUPERASSERT code, discussed in the next section, to handle the assertions.

Listing 3-2 VBASSERTANDTRACE.BAS

Attribute VB_Name = "VBAssertAndTrace" '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ' Copyright (c) 1999-2000 John Robbins -- All rights reserved. ' "Debugging Applications" (Microsoft Press) ' ' To use this file: ' ' Optional (but highly recommended!): '   Use Mark Pearce's Assertion Sourcerer Visual Basic add-in from '   "Advanced Microsoft Visual Basic 6.0" (2nd ed). '   Mark's add-in will hunt down all the Debug.Assert statements in '   your program and put a call to BugAssert under each one so that '   you can have real assertions in compiled Visual Basic. '   I use Mark's utility all the time, and you should too! ' ' 1. Compile BUGSLAYERUTIL.DLL because this file uses several of the '    exported functions. ' 2. Liberally sprinkle Debug.Assert statements around your code. ' 3. When you're ready to compile, use Mark's add-in to add the '    calls to BugAssert. ' 4. Include this file in your project. ' 5. Compile your project and have fun watching assertions. ' ' You can also call the various BUGSLAYERUTIL.DLL functions to set ' the options and output handles. '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Option Explicit ' Declare all the BUGSLAYERUTIL.DLL functions that this module can use. Public Declare Sub DiagOutputVB Lib "BugslayerUtil" _                     (ByVal sMsg As String) Public Declare Function DiagAssertVB Lib "BugslayerUtil" _                     (ByVal dwOverrideOpts As Long, _                     ByVal bAllowHalts As Long, _                     ByVal sMsg As String) _                     As Long Public Declare Function AddDiagAssertModule Lib "BugslayerUtil" _                     (ByVal hMod As Long) _                     As Long Public Declare Function SetDiagAssertFile Lib "BugslayerUtil" _                     (ByVal hFile As Long) _                     As Long Public Declare Function SetDiagAssertOptions Lib "BugslayerUtil" _                     (ByVal dwOpts As Long) _                     As Long Public Declare Function SetDiagOutputFile Lib "BugslayerUtil" _                     (ByVal dwOpts As Long) _                     As Long Private Declare Function GetModuleFileName Lib "kernel32" _                     Alias "GetModuleFileNameA" _                     (ByVal hModule As Long, _                      ByVal lpFileName As String, _                      ByVal nSize As Long) _                     As Long Public Declare Sub DebugBreak Lib "kernel32" () ' My TRACE statement. I'm too used to TRACE to be able to use any other ' macro. Additionally, I updated the Assertion Sourcerer to add TRACE ' calls after Debug.Print. You might want to do that as well. Public Sub TRACE(ByVal sMsg As String)     DiagOutputVB sMsg End Sub ' The BugAssert function inserted by Assertion Sourcerer Public Sub BugAssert(ByVal vntiExpression As Variant, sMsg As String)     CallAssert vntiExpression, 0, sMsg End Sub ' Sometimes I know I'm in trouble so I resort to SUPERASSERT. Public Sub SUPERASSERT(ByVal vntiExpression As Variant, sMsg As String)     CallAssert vntiExpression, 7, sMsg End Sub Private Sub CallAssert(ByVal vntiExpression As Variant, _                        ByVal iOpt As Long, _                        sMsg As String)     If (vntiExpression) Then         Exit Sub     Else         ' The flag used to determine whether I already called InDesign. No         ' need to call the function repeatedly.         Static bCheckedDesign As Boolean ' False, by default.         ' The allow halts flag I pass to DiagAssertVB. If this flag is         ' set to 1, DiagAssertVB will allow you to halt the application.         ' If this flag is set to 0, the user is running in the VB IDE so         ' DiagAssertVB won't allow halts. If the user is running inside         ' the VB IDE, breaking is a little too dangerous. Nothing like         ' a little lost data to ruin your day!         Static lAllowHalts As Long         ' Call InDesign only once.         If (False = bCheckedDesign) Then             If (True = InDesign()) Then                 lAllowHalts = 0             Else                 lAllowHalts = 1             End If             bCheckedDesign = True         End If         Dim lRet As Long         lRet = DiagAssertVB(iOpt, lAllowHalts, sMsg)         If (1 = lRet) Then             ' The user wants to break. However, I won't allow the             ' break if the user is running inside the VB IDE.             If (1 = lAllowHalts) Then                 DebugBreak             End If         End If      End If End Sub '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ' This wonderful function comes from Peet Morris's excellent chapter ' "On Error GoTo Hell," pages 25 and 26 in "Advanced Microsoft Visual ' Basic 6.0." InDesign allows you to check whether you're running in the ' VB IDE. I greatly appreciate Peet letting me use this function in my ' book! '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Public Function InDesign() As Boolean     ' I'll leave Peet's comment alone--he has an excellent point.     '*****************************************     ' The only thing Debug.Assert is good for!     '*****************************************     Static nCallCount As Integer     Static bRet As Boolean         ' By default, this flag is False.     nCallCount = nCallCount + 1     Select Case nCallCount         Case 1: ' First time in             Debug.Assert InDesign()         Case 2: ' Second time in, so Debug.Assert must have executed             bRet = True     End Select     ' If Debug.Assert was called, return True to prevent the trap.     InDesign = bRet     ' Reset for future calls.     nCallCount = 0 End Function

SUPERASSERT

Having told you what the problems are with the supplied assertions, now I want to show you how I was able to fix and extend the assertions to really make them tell you how and why you had a problem. Figure 3-1 shows an example of a SUPERASSERT message box. The program, file, line, and expression fields are self-explanatory. The interesting fields follow the Last Error field.

click to view at full size.

Figure 3-1 Example SUPERASSERT message box

In SUPERASSERT, I translate the last error values into their textual representations. Seeing the error messages written out as text is extremely helpful when an API function fails: you can see why it failed and can start debugging faster. For example, if GetModuleFileName fails because the input buffer isn't large enough, SUPERASSERT will set the last error value to 122, which is ERROR_INSUFFICIENT_BUFFER from WINERROR.H. By immediately seeing the text "The data area passed to a system call is too small," you know exactly what the problem is and how to fix it.

Additionally, if you look at the last error line in Figure 3-1, you see that it certainly isn't a standard Windows error message. If you set your own last error values, which I recommend that you do, you can add your own message resources module into the SUPERASSERT last error message translation. For more information on using your own message resources, look up the "Message Compiler" topic in MSDN. An added incentive to using message resources is that they make internationalizing your application much easier.

The part after the last error is where SUPERASSERT earns its money. It's a stack trace of how you got into the assertion. I show as much information as possible in an assertion so that I don't have to gather that same information by using the debugger. By showing the stack trace, I know exactly which path leads to the problem.

Another feature of SUPERASSERT is that you can choose not to have the assertions pop up a message box. At first, that might seem counterproductive, but I assure you that it isn't! If you followed my recommendations from Chapter 2 and started testing your debug builds with a regression-testing tool, you know that handling those random assertion message boxes is almost impossible. Because of the problems in handling assertion message boxes, your test engineers are much less likely to use the debug build. With my assertion code, you can specify that you want the output to go to OutputDebugString, a file handle, or both. This flexibility allows you to run the code and get all the great assertion information but still be able to automate your debug builds. Additionally, in those cases in which your application doesn't have a UI, my assertion will still work for you.

Using my assertions is easy. For C and C++, you just need to include BUGSLAYERUTIL.H and link against BUGSLAYERUTIL.LIB. Listing 3-3 shows DIAGASSERT.H, which contains all the macros and functions (and is included automatically by BUGSLAYERUTIL.H).

Listing 3-3 DIAGASSERT.H (included by BUGSLAYERUTIL.H)

/*---------------------------------------------------------------------- "Debugging Applications" (Microsoft Press) Copyright (c) 1999-2000 John Robbins -- All Rights Reserved. ----------------------------------------------------------------------*/ #ifndef _DIAGASSERT_H #define _DIAGASSERT_H #ifdef __cplusplus extern "C" { #endif //__cplusplus #include <tchar.h> /*//////////////////////////////////////////////////////////////////////                                 Defines //////////////////////////////////////////////////////////////////////*/ // Keep the core stuff available in both release and debug builds. // Uses the global assert flags. #define DA_USEDEFAULTS      0x0000 // Turns on showing the assert in a message box. This is the default. #define DA_SHOWMSGBOX       0x0001 // Turns on showing the assert as through OutputDebugString. This is // the default. #define DA_SHOWODS          0x0002 // Shows a stack trace in the assert. This is off by default with the // ASSERT macro and on in the SUPERASSERT macro. #define DA_SHOWSTACKTRACE   0x0004 /*---------------------------------------------------------------------- FUNCTION         :    SetDiagAssertOptions DISCUSSION       :     Sets the global options for normal ASSERT macros. PARAMETERS       :     dwOpts - The new options flags RETURNS          :     The previous options ----------------------------------------------------------------------*/ DWORD BUGSUTIL_DLLINTERFACE __stdcall     SetDiagAssertOptions ( DWORD dwOpts ) ; /*---------------------------------------------------------------------- FUNCTION         :    SetDiagAssertFile DISCUSSION       :     Sets a HANDLE where the data in any assertion will be written. To turn off logging, call this function with INVALID_HANDLE_VALUE. The options set in SetDiagAssertOptions still apply; this function just lets you log the assertion information to a file.     No error checking is done on the file handle or on any writes to it. PARAMETERS       :     hFile - The file handle RETURNS          :     The previous file handle ----------------------------------------------------------------------*/ HANDLE BUGSUTIL_DLLINTERFACE __stdcall     SetDiagAssertFile ( HANDLE hFile ) ; /*---------------------------------------------------------------------- FUNCTION          :    AddDiagAssertModule DISCUSSION        :     Adds the specified module to the list of modules that error strings will be pulled from PARAMETERS        :     hMod - The module to add RETURNS           :     TRUE  - The module was added.     FALSE - The internal table is full. ----------------------------------------------------------------------*/ BOOL BUGSUTIL_DLLINTERFACE __stdcall     AddDiagAssertModule ( HMODULE hMod ) ; /*---------------------------------------------------------------------- FUNCTION         :    DiagAssert DISCUSSION       :     The assert function for C and C++ programs PARAMETERS       :     dwOverrideOpts - The DA_* options to override the global defaults                      for this call into DiagAssert     szMsg          - The message to show     szFile         - The file that showed the assert     dwLine         - The line that had the assert RETURNS          :     FALSE - Ignore the assert.     TRUE  - Trigger the DebugBreak. ----------------------------------------------------------------------*/ BOOL BUGSUTIL_DLLINTERFACE __stdcall     DiagAssertA ( DWORD dwOverrideOpts  ,                   LPCSTR szMsg          ,                   LPCSTR szFile         ,                   DWORD dwLine            ) ; BOOL BUGSUTIL_DLLINTERFACE __stdcall     DiagAssertW ( DWORD dwOverrideOpts  ,     LPCWSTR szMsg                       ,     LPCSTR szFile                       ,     DWORD dwLine                          ) ; #ifdef UNICODE #define DiagAssert DiagAssertW #else #define DiagAssert DiagAssertA #endif /*---------------------------------------------------------------------- FUNCTION         :    DiagAssertVB DISCUSSION       :     The assert function for VB programs. PARAMETERS       :     dwOverrideOpts - The DA_* options to override the global defaults                      for this call into DiagAssert.     bAllowHalts    - If TRUE, doesn't show Retry and Ignore buttons     szMsg          - The message to show. The Visual Basic side is responsible                       for formatting the string. RETURNS           :    FALSE - Ignore the assert.    TRUE  - Trigger DebugBreak. ----------------------------------------------------------------------*/ BOOL BUGSUTIL_DLLINTERFACE __stdcall     DiagAssertVB ( DWORD dwOverrideOpts ,                    BOOL bAllowHalts ,                    LPCSTR szMsg ) ; /*---------------------------------------------------------------------- FUNCTION          :    SetDiagOutputFile DISCUSSION        :     Sets a HANDLE where the data in any trace statements will optionally be written. To turn off logging, call this function with INVALID_HANDLE_VALUE.    No error checking is done on the file handle or on any writes to it. PARAMETERS        :     hFile - The file handle RETURNS           :     The previous file handle ----------------------------------------------------------------------*/ HANDLE BUGSUTIL_DLLINTERFACE __stdcall     SetDiagOutputFile ( HANDLE hFile ) ; /*---------------------------------------------------------------------- FUNCTION          :     DiagOutput DISCUSSION        :     Provides a tracing routine to send strings through OutputDebugString PARAMETERS        :     szFmt - The format string     ...   - Parameters that will be expanded into szFmt RETURNS           :     None. ----------------------------------------------------------------------*/ void BUGSUTIL_DLLINTERFACE     DiagOutputA ( LPCSTR szFmt , ... ) ; void BUGSUTIL_DLLINTERFACE     DiagOutputW ( LPCWSTR szFmt , ... ) ; #ifdef UNICODE #define DiagOutput DiagOutputW #else #define DiagOutput DiagOutputA #endif /*---------------------------------------------------------------------- FUNCTION          :     DiagOutputVB DISCUSSION        :     Provides a tracing routine to send strings through OutputDebugString for Visual Basic programs PARAMETERS        :     szMsg - The message string RETURNS           :     None. ----------------------------------------------------------------------*/ void BUGSUTIL_DLLINTERFACE __stdcall     DiagOutputVB ( LPCSTR szMsg ) ; /*//////////////////////////////////////////////////////////////////////                                UNDEFINES //////////////////////////////////////////////////////////////////////*/ #ifdef ASSERT #undef ASSERT #endif #ifdef assert #undef assert #endif #ifdef VERIFY #undef VERIFY #endif #ifdef TRACE #undef TRACE #endif #ifdef TRACE0 #undef TRACE0 #endif #ifdef TRACE1 #undef TRACE1 #endif #ifdef TRACE2 #undef TRACE2 #endif #ifdef TRACE3 #undef TRACE3 #endif /*//////////////////////////////////////////////////////////////////////                             _DEBUG Is Defined //////////////////////////////////////////////////////////////////////*/ #ifdef _DEBUG /*//////////////////////////////////////////////////////////////////////                                 Defines //////////////////////////////////////////////////////////////////////*/ // The different global options that can be set with // SetDiagAssertOptions. If any of these options are passed to DiagAssert in // the first parameter, that value will override whatever the // global settings are. // The assert macro used by ASSERT and SUPERASSERT. // Turn off "conditional expression is constant" because of while(0). // I need to turn this off globally because the compilation error // occurs on the expansion of the macro. #pragma warning ( disable : 4127 ) #ifdef PORTABLE_BUGSLAYERUTIL #define ASSERTMACRO(a,x)                                               \     do                                                                 \     {                                                                  \        if ( !(x)                                                     &&\             DiagAssert ( a , _T ( #x ) , __FILE__ , __LINE__)         )\         {                                                              \                DebugBreak ( ) ;                                        \         }                                                              \     } while (0) #else //!PORTABLE_BUGSLAYERUTIL #define ASSERTMACRO(a,x) \     do                                                                  \     {                                                                   \         if ( !(x)                                                     &&\               DiagAssert ( a , _T ( #x ) , __FILE__ , __LINE__)        )\         {                                                               \                  __asm int 3                                            \         }                                                               \     } while (0) #endif // PORTABLE_BUGSLAYERUTIL // The normal assert. It just uses the module defaults. #define ASSERT(x) ASSERTMACRO(DA_USEDEFAULTS,x) // Do the lowercase one. #define assert ASSERT // Trust, but verify. #define VERIFY(x)   ASSERT(x) // Full-blown assert with all the trimmings #define SUPERASSERT(x) ASSERTMACRO ( DA_SHOWSTACKTRACE |   \                                         DA_SHOWMSGBOX  |   \                                         DA_SHOWODS      ,  \                                      x                  , ) // The options macro #define SETDIAGASSERTOPTIONS(x) SetDiagAssertOptions(x) // The add module macro #define ADDDIAGASSERTMODULE(x) AddDiagAssertModule(x) // The TRACE macros #ifdef __cplusplus #define TRACE ::DiagOutput #endif #define TRACE0(sz)              DiagOutput(_T("%s"), _T(sz)) #define TRACE1(sz, p1)          DiagOutput(_T(sz), p1) #define TRACE2(sz, p1, p2)      DiagOutput(_T(sz), p1, p2) #define TRACE3(sz, p1, p2, p3)  DiagOutput(_T(sz), p1, p2, p3) #else // !_DEBUG /*//////////////////////////////////////////////////////////////////////                        _DEBUG Is !!NOT!! Defined //////////////////////////////////////////////////////////////////////*/ #define ASSERTMACRO(a,x) #define ASSERT(x) #define VERIFY(x) ((void)(x)) #define SUPERASSERT(x) #define SETDIAGASSERTOPTIONS(x) #define ADDDIAGASSERTMODULE(x) #ifdef __cplusplus //inline void TraceOutput(LPCTSTR, ...) { } #define TRACE (void)0 #endif #define TRACE0(fmt) #define TRACE1(fmt,arg1) #define TRACE2(fmt,arg1,arg2) #define TRACE3(fmt,arg1,arg2,arg3) #endif // _DEBUG #ifdef __cplusplus } #endif //__cplusplus #endif // _DIAGASSERT_H

With SUPERASSERT, I automatically redirect all ASSERT and assert calls to my functions. I don't redirect the _ASSERT and _ASSERTE macros because you might be doing some advanced work with the debug run-time library and I didn't want to break your existing solutions. I leave ASSERT_KINDOF and ASSERT_VALID alone as well. For Visual Basic, you just need to include VBASSERTANDTRACE.BAS in your project.

If you use the SUPERASSERT macro or function, you'll automatically get the stack trace. By default, the stack tracing is off for ASSERT. I didn't want to incur the overhead associated with stack tracing on general assertions. If you want to use stack tracing, however, turning it on is easy: just set the options with the SETDIAGASSERTOPTIONS macro or the SetDiagAssertOptions function and pass in the DA_SHOWSTACKTRACE bit flag. I tend to use SUPERASSERT where I least expect problems or would be surprised to be executing—for example, in an exception block. For normal assertion situations, ASSERT works fine for me. The choice is yours because you can set the defaults.

Common Debugging Question
Why do you always put the constants on the left-hand side of conditional statements?

As you look through my code, you'll notice that I always use statements such as "if ( INVALID_HANDLE_VALUE == hFile )" instead of "if ( hFile == INVALID_ HANDLE_VALUE )." The reason I use this style is to avoid bugs. You can easily forget one of the equal signs, and using the former version will yield a compiler error if you do forget. The latter version might not issue a warning—whether or not it does depends on the warning level—and you'll change the variable's value. In both C/C++ and Visual Basic, trying to assign a value to a constant will produce a compiler error. If you've ever had to track down a bug involving an accidental assignment, you know just how difficult this type of bug is to find.

If you pay close attention, you'll notice that I also use constant variables on the left side of the equalities. As with the constant values case, the compilers will report errors when you try to assign a value to a constant variable. I've found that it's a lot easier to fix compiler errors than to fix bugs in the debugger.

Some developers have complained, sometimes vociferously, that the way I write conditional statements makes the code more confusing to read. I don't agree. My conditional statements take only a second longer to read and translate. I'm willing to give up that second to avoid wasting huge amounts of time later.



Debugging Applications
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2000
Pages: 122
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net