Exception Handling
Windows CE .NET, along with eMbedded C++ 4.0, supports both Microsoft's standard structured exception handling extensions to the C language (the __try, __except and __try, __finally blocks) and the ANSI-standard C++ exception handling framework, with keywords such as catch and throw.Windows exception handling is complex, and if I were to cover it completely, I could easily write another entire chapter. The following review introduces the concepts to non-Win32 programmers and conveys enough information about the subject for you to get your feet wet. If you want to wade all the way in, the best source for a complete explanation of Win32 exception handling is Jeffrey Richter's Programming Applications for Windows, 4th edition (Microsoft Press, 1999).
C++ Exception Handling
Support for C++ exception handling was added in Windows CE .NET 4.0. The statements, try, catch, and throw are familiar to C++ programmers and work as expected in Windows CE. To use C++ exception handling in a Windows CE C++ application, the application must be compiled with the –GX compiler switch. For those not familiar with the operation of these keywords, what follows is a quick introduction.
Using Exceptions to Report Errors
It's the vogue in programming circles these days to report errors in a function by throwing an exception. Using this scheme, a calling function that doesn't check for errors will have the exception automatically passed on to its calling function. If no function ever checks for the exception, the exception will be passed to the operating system, which will act appropriately on the offending application. Functions that simply report an error code in a return code can't enforce error checking because the lack of verification of the error code isn't automatically reported up the stack chain.
A simple example of the different methods of reporting errors is shown in the following code fragments. In the first code fragment, the failure of LocalAlloc in AddItem is reported by returning 0. Note how each call to AddItem has to be checked to see whether an error occurred in AddItem.
PMYITEM AddItem (PMYITEM pLast, DWORD dwData) {
// Allocate the item
PMYITEM p = (PMYITEM)LocalAlloc (LPTR, sizeof (MYITEM));
if (p == 0)
return 0;
// Link the list
p->pPrev = pLast;
if (pLast) pLast->pNext = p;
p->dwData = dwData;
return p;
}
int test (HWND hWnd) {
PMYITEM pNext;
pNext = AddItem (NULL, 1);
if (pNext == NULL)
return ERROR_CODE;
pNext = AddItem (pNext, 2);
if (pNext == NULL)
return ERROR_CODE;
pNext = AddItem (pNext, 3);
if (pNext == NULL)
return ERROR_CODE;
return 0;
}
In the following code fragment, AddItem throws an exception if the memory allocation fails. Notice how much cleaner the calling routine test1 looks.
PMYITEM AddItem (PMYITEM pLast, DWORD dwData) {
// Allocate the item
PMYITEM p = (PMYITEM)LocalAlloc (LPTR, sizeof (MYITEM));
if (p == 0)
throw ("failure to allocate item in AddItem");
// Link the list
p->pPrev = pLast;
if (pLast) pLast->pNext = p;
p->dwData = dwData;
return p;
}
int test1 (HWND hWnd) {
PMYITEM pNext;
try {
pNext = AddItem (NULL, 1);
pNext = AddItem (pNext, 2);
pNext = AddItem (pNext, 3);
}
catch (char * strException) {
return ERROR_CODE;
}
return 0;
}
The simple structure of the foregoing routines demonstrates the ease with which C++ exception handling can be added to an application. The try keyword wraps code that might generate an exception. The wrapped code includes any routines called from within the try block. If an exception is thrown with a string argument, the exception will be caught by the catch block in test1. What happens if some other exception is thrown? Let's look at the basics of the try, catch, and throw keywords to see.
The try, catch Block
The basic structure of the exception keywords is demonstrated in the following pseudocode.
try
{
throw (arg of type_t);
}
catch (type_t arg)
{
// catches all throws with argument of type_t
}
Within the try block, if an exception is thrown with an argument, the exception will be caught by the catch block that has the matching argument. If no catch block has a matching argument, the exception is passed to the function that called the code containing the try block. If no enclosing try, catch block is found, the thread is terminated. If no exception occurs within the try block, none of the associated catch blocks are executed.For example
try
{
throw (1);
}
would be caught if the try block had an associated catch block with an integer argument such as
catch (int nExceptionCode)
{
// Exception caught!
}
The argument doesn't have to be a simple type; it can be a C++ class. It's also permissible to have multiple catch blocks each with a different argument string associated with the try block. Catch blocks are evaluated in the order they appear in the code. Finally, a catch block with ellipsis arguments catches all exceptions within the try block.
try
{
throw (1);
throw ("This is an ascii string");
throw (CMyException cEx);
}
catch (int nExCode)
{
// catches all throws with an integer argument
}
catch (char * szExCode)
{
// catches all throws with a string argument
}
catch (CMyException cEx)
{
// catches all throws with a CMyException class argument
}
catch (...)
{
// catches all exceptions not caught above
}
Win32 Exception Handling
Windows CE has always supported the Win32 method of exception handling, using the __try, __except, and __finally keywords. What follows is a brief overview of these statements.
The __try, __except Block
The __try, __except block looks like this:
__try {
// Try some code here that might cause an exception.
}
__except (exception filter) {
// This code is depending on the filter on the except line.
}
Essentially, the try-except pair allows you the ability to anticipate exceptions and handle them locally instead of having Windows terminate the thread or the process because of an unhandled exception.The exception filter is essentially a return code that tells Windows how to handle the exception. You can hard code one of the three possible values or call a function that dynamically decides how to respond to the exception.If the filter returns EXCEPTION_EXECUTE_HANDLER, Windows aborts the execution in the try block and jumps to the first statement in the except block. This is helpful if you're expecting the exception and you know how to handle it. In the code that follows, the access to memory is protected by a __try, __except block.
BYTE ReadByteFromMemory (LPBYTE pPtr, BOOL *bDataValid) {
BYTE ucData = 0;
*bDataValid = TRUE;
__try {
ucData = *pPtr;
}
__except (DecideHowToHandleException ()) {
// The pointer isn't valid; clean up.
ucData = 0;
*bDataValid = FALSE;
}
return ucData;
}
int DecideHowToHandleException (void) {
return EXCEPTION_EXECUTE_HANDLER;
}
If the memory read line above wasn't protected by a __try, __except block and an invalid pointer was passed to the routine, the exception generated would have been passed up to the system, causing the thread and perhaps the process to be terminated. If you use the __try, __except block, the exception is handled locally and the process continues with the error handled locally.Another possibility is to have the system retry the instruction that caused the exception. You can do this by having the filter return EXCEPTION_CONTINUE_EXECUTION. On the surface, this sounds like a great option—simply fix the problem and retry the operation your program was performing. The problem with this approach is that what will be retried isn't the line that caused the exception, but the machine instruction that caused the exception. The difference is illustrated by the following code fragment that looks okay but probably won't work:
// An example that doesn't work...
int DivideIt (int aVal, int bVal) {
int cVal;
__try {
cVal = aVal / bVal;
}
__except (EXCEPTION_CONTINUE_EXECUTION) {
bVal = 1;
}
return cVal;
}
The idea in this code is noble: protect the program from a divide-by-zero error by ensuring that if the error occurs, the error is corrected by replacing bVal with 1. The problem is that the line
cVal = aVal / bVal;
is probably compiled to something like the following on a MIPS-compatible CPU:
lw t6,aVal(sp) ;Load aVal
lw t7,bVal(sp) ;Load bVal
div t6,t7 ;Perform the divide
sw t6,cVal(sp) ;Save result into cVal
In this case, the third instruction, the div, causes the exception. Restarting the code after the exception results in the restart beginning with the div instruction. The problem is that the execution needs to start at least one instruction earlier to load the new value from bVal into the register. The moral of the story is that attempting to restart code at the point of an exception requires knowledge of the specific machine instruction that caused the exception.
The third option for the exception filter is to not even attempt to solve the problem and to pass the exception up to the next, higher, __try, __except block in code. The exception filter returns EXCEPTION_CONTINUE_SEARCH. Because __try, __except blocks can be nested, it's good practice to handle specific problems in a lower, nested, __try, __except block and more global errors at a higher level.
Determining the Problem
With these three options available, it would be nice if Windows let you in on why the exception occurred. Fortunately, Windows provides the function
DWORD GetExceptionCode (void);
This function returns a code that indicates why the exception occurred in the first place. The codes are defined in WINBASE.H and range from EXCEPTION_ACCESS_VIOLATION to CONTROL_C_EXIT, with a number of codes in between. Another function allows even more information:
LPEXCEPTION_POINTERS GetExceptionInformation (void);
GetExceptionInformation returns a pointer to a structure that contains pointers to two structures: EXCEPTION_RECORD and CONTEXT. EXCEPTION_RECORD is defined as
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
The fields in this structure go into explicit detail about why an exception occurred. To narrow the problem down even further, you can use the CONTEXT structure. The CONTEXT structure is different for each CPU and essentially defines the exact state of the CPU when the exception occurred.There are limitations on when these two exception information functions can be called. GetExceptionCode can be called only from inside an except block or from within the exception filter function. The GetExceptionInformation function can be called only from within the exception filter function.
Generating Your Own Exceptions
There are times when an application might want to generate its own exceptions. The Win32 method for raising an exception is the function RaiseException, prototyped as follows:
void RaiseException (DWORD dwExceptionCode, DWORD dwExceptionFlags,
DWORD nNumberOfArguments, const DWORD *lpArguments);
The first parameter is the exception code, which will be the value returned by GetExceptionCode from within the __except block. The codes understood by the system are the same codes defined for GetExceptionCode, discussed earlier. The dwExceptionFlags parameter can be EXCEPTION_NONCONTINUABLE to indicate that the exception can't be continued or 0 if the exception can be continued. The last two parameters, nNumberOfArguments and lpArguments, allow the thread to pass additional data to the exception handler. The data passed can be retrieved with the GetExceptionInformation function in the __except filter function.
The __try, __finally Block
Another tool of the structured exception handling features of the Win32 API is the __try, __finally block. It looks like this:
__try {
// Do something here.
}
__finally {
// This code is executed regardless of what happens in the try block.
}
The goal of the __try, __finally block is to provide a block of code, the finally block, that always executes regardless of how the other code in the try block attempts to leave the block. Unfortunately, the current Windows CE C compilers don't support leaving the __try block by a return or a goto statement. The Windows CE compilers do support the __leave statement that immediately exits the __try block and executes the __finally block, so there is some limited use of a __try, __finally block if only to avoid using a goto statement simply to jump to some common cleanup code.In the preceding three chapters, I've covered the basics of the Windows CE kernel from memory to files to processes and threads. Now it's time to break from this low-level stuff and start looking outward. In the final chapter of this section, I'll cover the Windows CE notification API. The notification API frees applications from having to stay running in the background to monitor what is going on in the system. Let's take a look.