SUMMARY Predefined error codes returned from HRESULT aren't always much help for debugging COM C++ code. The C++ macros provided with this article produce an XML file listing the error and its context to make debugging easier. This article begins with an overview of error handling in COM, then discusses the COM interfaces used in the macros. It explains how C++ exceptions are caught and converted to COM-compatible error information, how events are logged with the event viewer, and how context is reported in the description string of IErrorInfo. The macros handle logic errors and errors returned by an object or API.
rror handling in software systems tends to be an afterthought. This is probably the result of the way software development is taught in computer science curricula and textbooks. The focus is on correctness; error handling is always left as an exercise for the reader. Even components shipping from commercial software vendors have poor error handling.
In the real world, errors occur, and then troubleshooting is required. One of the main problems with troubleshooting and debugging is lack of information. Exactly where did the error happen? What was the sequence that provoked the error? Well-constructed software should propagate the source and the context of an error to the caller. I call this the error propagation principle of software construction. You expect that if all the components in the chain follow the principle, they will propagate the error back to the original user, who will be able to quickly troubleshoot the problem.
The challenge for the developer of a component or module is to follow the error propagation principle while maintaining productivity by writing a minimum of code for handling errors. Since troubleshooting is similar to debugging, the solution I'll present can shorten the debugging cycle as well.
The error-handling code provided in this article builds on C++ exceptions and uses the description string of the IErrorInfo object to propagate information about an error. The information is encoded in XML for easy parsing.
Error Handling in COM
Since simplicity is one of the cornerstones of the COM architecture, it's no surprise that the COM specification devotes only one small section to error handling. This section introduces HRESULT, a 32-bit code that every COM method returns. HRESULT is divided in four sections, as shown in Figure 1.
Field |
Bit(s) |
Severity |
31 |
Reserved |
29-30 |
Facility |
16-28 |
Code |
0-15 |
Figure 1.
The severity field is probably the most important one. If the field is set when a method returns, an error has occurred. This field makes all COM error codes appear as negative decimal integers.
The facility field provides a space for 8192 facilities. A centralized authority assigns these facilities. Microsoft has defined approximately 14 facilities, as shown in Figure 2. FACILITY_ITF is usually the most important of the facilities. When this facility is used, the error code, combined with the interface ID, creates a globally unique error identifier.
Facility |
Value |
Description |
FACILITY_NULL |
0 |
Used for generic codes like S_OK. |
FACILITY_RPC |
1 |
Remote Procedure Call-related error codes. |
FACILITY_DISPATCH |
2 |
For IDispatch-related errors. |
FACILITY_STORAGE |
3 |
Status codes returned by structured storage interfaces like IStream and IStorage. |
FACILITY_ITF |
4 |
The most important facility for your code. This facility means that the code is interface-dependent; the same code returned by methods of different interfaces has different meanings. |
FACILITY_WIN32 |
7 |
Provides a way to handle error codes from functions in the Win32 API as an HRESULT. Error codes in 16-bit OLE that duplicated Win32 error codes have also been changed to FACILITY_WIN32. |
FACILITY_WINDOWS |
8 |
Used for additional error codes from Microsoft-defined interfaces. |
FACILITY_SSPI |
9 |
Used for status code returned by security providers. |
FACILITY_CONTROL |
10 |
Used by OLE controls. You can find a complete list of the error codes in OLECTL.H. |
FACILITY_CERT |
11 |
Status codes for certificate services. |
FACILITY_INTERNET |
12 |
Status codes for Internet-related APIs. |
FACILITY_MEDIASERVER |
13 |
Status codes for Media Server APIs. |
FACILITY_MSMQ |
14 |
Status codes for Microsoft Message Queue Server (MSMQ). |
FACILITY_SETUPAPI |
15 |
Status codes for setup APIs. |
Figure 2.
Finally, the code field provides a space for 65536 codes. If you use FACILITY_ITF, as you should, this space is big enough to cover all of the errors an interface can return. Microsoft recommends starting from the code 0x200 to avoid confusion with predefined codes. For instance, OLE has already defined 0x80040000 to be OLE_E_FIRST. If you return the same code from one of your interfaces, a client might get confused.
The COM header files provide a number of predefined error codes that cover some common cases. Figure 3 shows a few of the most commonly used predefined error codes.
Macro |
Value |
Description |
E_FAIL |
0x80004005 |
Generic failure |
E_NOTIMPL |
0x80004001 |
Not implemented yet |
E_UNEXPECTED |
0x8000FFFF |
Catastrophic failure |
E_INVALIDARG |
0x80070057 |
One or more arguments are invalid |
E_POINTER |
0x80004003 |
Invalid pointer |
E_NOINTERFACE |
0x80004002 |
Interface is not supported by object |
E_OUTOFMEMORY |
0x8007000E |
Ran out of memory |
Figure 3
Most COM programmers use just one of these generic codes to communicate errors in their components. For instance, the following lines return an error code when opening a file fails:
This is a good practice for your first COM program, but it fails the error propagation principle. When you return an E_FAIL HRESULT using Visual Basic, the programmer who calls your object sees the dialog shown in Figure 4.
Figure 4 Error Handling Not Supported
You will probably agree that this is not very useful. The caller would like to have more complete information about the failure of the invocation. In this case, you could wrap the Windows® error code into an HRESULT using the HRESULT_FROM_WIN32 macro. This would be much better, but it still hides important evidence. For instance, if the error was a permissions problem, it might be necessary to have the name of the file to troubleshoot the error.
The IErrorInfo Interface
COM provides an interface called IErrorInfo that allows designers of objects to provide extended error information. In contrast to HRESULT, which is absolutely necessary, IErrorInfo is optional. The object designer can decide whether or not he wants to support it. Unless you design a component for an environment with very limited resources, I strongly urge you to support this error-handling method.
IErrorInfo has five properties. The description property, which I find to be the most useful, is a string containing information about the error. The developer of the object can provide any information she thinks will be useful for troubleshooting.
Object developers who provide extended information for an interface of an object must implement an additional interface called ISupportErrorInfo. The ATL Object Wizard in Visual C++® provides a default implementation when you mark the support ISupportErrorInfo checkbox.
This ISupportErrorInfo interface has only one method: InterfaceSupportsErrorInfo. Clients of the object can call the method to find out which interfaces of the object provide extended error information. In the same object, some interfaces could provide extended error information, while others do not.
COM provides a few APIs to manipulate the IErrorInfo structure. When an error occurs, the object developer should start with the COM CreateErrorInfo API to create and populate an extended error information object. On the client side, the GetErrorInfo API should be called to retrieve the error object. The details are well-documented in MSDN® Online, but they are tedious to program every time an error occurs. The mechanism by which the extended error information is communicated from the callee to the caller, on the other hand, is not very well-documented. It appears as if the information in the IErrorInfo object is passed through some out-of-band mechanism. For all practical purposes, it suffices to think of it as an additional hidden argument.
Simplifying IErrorInfo
IErrorInfo is great for clients, but fairly cumbersome for C++ objects to implement. For every error to be reported, several APIs should be called to set the extended error information. ATL has made the situation better by introducing the AtlReportError function and the CComCoClass<>::Error set of methods.
The macros provided with this article make handling errors even easier. The code reports both the error and the context in which the error occurred. For instance, if object A calls object B, and object B throws an error, the code maintains and reports the error. The information provided includes the file name and line number in object B, and the file number and line number in object A. The error and the context are encoded as an XML string that is communicated in the description string of IErrorInfo.
Here is an example from a Web application. An ASP page calls a method on a business object. The business object calls a data object. The data object needs to open a database connection to accomplish its task, but the call fails because the connection string has a spelling error. Figure 5 shows what the original client will get in the description string of the IErrorInfo object. The client can parse the string and communicate as much information as needed to the user. The rest of the information could be logged into a file for further troubleshooting.
Figure 5 Maintaining the Context of an Error
The XML schema for reporting error information uses six elements (see Figure 6). In violation of the XML spirit, I kept the element names short so that the description string does not grow too long.
Element Name |
Description |
A list of messages |
|
Delimits one message |
|
Delimits the text description of a message |
|
Delimits a numeric error code |
|
Delimits the line number of the file where the error occurred |
|
Delimits the file name of the file where the error occurred |
Figure 6
Six macros make reporting an error using COM as simple as throwing an exception or writing a printf statement. The following section introduces the macros and explains the guidelines that govern their usage.
C++ Error-handling Macros
There are two cases where you want to raise an error inside a COM object. In the first situation, the logic of the application dictates that this happen. For instance, let's assume that you have a method of a COM object that expects the input (encoded in the IDL as SHORT) to be between 1 and 10. If you get an input of 11, you will have to raise an error. For every such situation you should define a code (between 0x0200 and 0xFFFF). I usually define these codes as an enum in the IDL file where the interface is defined (see Figure 7).
Figure 7
A client can now programmatically check for the specific error and take appropriate action. For instance, Figure 8 shows how to check for this error in Visual Basic code. Note that IntelliSense® shows that the error was specified as an enum in the IDL file.
Figure 8 Checking for a COM Error
In the C++ code that implements the object, I throw the error using the COMTHROW/COMTHROWEND macros as follows:
COMTHROW E_GenericError, IID_IGoodObject, _T("Demo Error %s %d"), _T("test"), 20 COMTHROWENDThe syntax is similar to printf. The first argument is the error code as defined in the IDL file. The second argument is the IID of the interface where the error occurred. The rest of the arguments have the syntax of printf and format a string that appears in the description of the error.
All the macros discussed so far raise or respond to C++ exceptions. Since C++ exceptions do not propagate across COM object boundaries (see the sidebar "COM and C++ Exceptions"), it is important to catch these exceptions and convert them to COM-compatible error information. There are two macros, COMTRY and COMCATCH, that provide this functionality. I use these macros at the beginning and end of every function, as shown in Figure 9.
Figure 9
By default, the macro will attempt to acquire a transaction context and abort any transaction. If you do not use transactions or do not want to abort in case of errors, you can use the COMCATCH_NT variation. COMCATCH_NT behaves exactly like COMCATCH, but ignores transactions.
The Logging Object
When an error occurs in a COM object, it might be a good idea to log an event with the event viewer. In fact, logging events is a good idea even under normal circumstances. For that reason I have introduced a logging object.
You should declare your log object in global scope and call it gLog. The best place is in the stdafx.h and stdafx.cpp files of your application, as shown in the following lines of code.