(Part 3 of 3) In the last column, it was obvious that writing code to detect
and process rich error information provided by a COM interface is a lot of work
(in C++). No wonder most developers dont bother with it. The majority
of COM code Ive seen just tests for failed HRESULT
values (using
a common macro like #define FAILED) and then shunts off into some run-of-the-mill
error code. Usually this is just dumping the error to a log or aborting the
action at that point.
C++ is an awesome and powerful language; however it is also often a pain to work with, particularly if you are concentrating more on the logic of your application than the mechanics of the code. Reading through pages of code is not a whole lot of fun if its clouded with error detection and handling logic. Trying to determine the flow of the code is more work, and even for the original writer of the code, it probably explains why most developers pass on robust error handling.
This is unfortunate because C++ has a much maligned, and Id argue misunderstood,
featurethe preprocessor. When Bjarne Stroustrop and his friends (http://www.research.att.com/~bs/homepage.html)
at Bell Labs designed C++ in the early 1980s, a key goal of the language to
ensure its adoption by the large base of C developers was to make it upwardly
compatible. That is, I could take a typical C application and compile it as
C++ as-is. Then I could add support for C++ features such as classes, inheritance,
and so on as time permitted. One of the features of C that had to be retained
was the preprocessor. Now some argue that the preprocessor in C is a relic similar
to the goto
statement and should not be used since there are reasonable
replacements such as templates (whether classes or functions).
But the point I think is missedthe preprocessor is easy to use, fast, and most C/C++ developers are familiar with it. It also can make your code easier to read, although a bit harder to debugif a macro is poorly written, you might find the debugging part a nightmare. But again, it comes down to how well thought out the macro is and how limited its scope is. Basically the macro should be easy to understand when you see it used in code, and it should hide unnecessary detail.
Heres a macro that can be used to handle rich error information from a COM object:
#define CHECK_HRESULT_THROW( call,iface ) \ { HRESULT _hr = (call); \ if ( FAILED(_hr) ) \ { \ CComQIPtr<ISupportErrorInfo> pError( iface ); \ if ( pError ) \ { \ hr = pError->InterfaceSupportsErrorInfo( \ __uuidof(iface) ); \ if ( hr == S_OK ) \ { \ CComPtr<IErrorInfo> pErrInfo; \ hr = ::GetErrorInfo(0, // reserved; must be zero \ &pErrInfo); \ if ( hr == S_OK && pErrInfo ) \ { \ throw new com_richerror_exception( pErrInfo ); \ } \ } \ } \ } }
Its the same block of code we looked at in the last issueif the
HRESULT
of the method were calling returns a failure code, we ask
the object if it supports rich errors, and then acquire any rich error information
it may have.
Whats new is the following line of code:
throw new com_richerror_exception( pErrInfo );
When a rich error interface is retrieved from an object, the question becomes
how do I handle the error in my code? In this example, I made the
decision to throw an exception derived from the Standard Template Library std::exception
class that takes an IErrorInfo
interface in its constructor. This exception
can then be handled by a local try/catch block or one higher up the call stack.
Another option is to use the Native C++ compiler support and the function _com_issue_errorex
,
which will issue a _com_error
type exception. But not all developers
wish to use both ATL and Native C++ COM support in the same object, so I will
focus on doing this in ATL.
A word of caution: Never throw a C++ style exception out of a COM method. Its
bad form and unlikely to be handled properly by callers. COM defines HRESULT
s
as the normal form of returning errors (although some also return [out,retval]
values as more advanced errors).
So to use this macro in some code, it might look like this:
void MyFunction() { CComPtr<ISomeInterface> pI; pI.CoCreateInstance(__uuidof(SomeObject)); try { CHECK_HRESULT_THROW( pI->SomeMethod(),pI ); } catch( com_richerror_exception& err ) { CComPtr<IErrorInfo> pErr( err.errorInfo ); // do something with the error. } }
If the method that you needed to call had parameters, the code in bold would change slightly:
void MyFunction() { CComPtr<ISomeInterface> pI; pI.CoCreateInstance(__uuidof(SomeObject)); try { CHECK_HRESULT_THROW( pI-><code>SomeMethod(Parm1,Parm2),pI</code> ); } catch( com_richerror_exception& err ) { CComPtr<IErrorInfo> pErr( err.errorInfo ); // do something with the error. } }
When the macro expands, all of that nice error handling is added to this code, yet doesn't clutter up the readability of it. Also, if I want to modify how I handle rich errors in my application, I can modify the macro in one spot rather than changing this code all over the place. I can also enforce standard behavior such as logging the exception to an external log file for debugging purposes.
This is not the only way this macro could have been written. However it was simple, fast, and straightforward to put together. As I said earlier, debugging it can be a pain, so I usually manually expand code like this in a module and debug it thoroughly before moving to a macro. Once its working though, using it is a snap.
A downside to the macro approach is the bulk it adds to your code. Since the rich error handling is expanded each time it is used, the compiled code size can increase quite a bit and will impact the applications memory usage and quite possibly its performance.
But not to fearwe can make a final change to the macro implementation to further optimize it without losing the usability of the macro. Well hide the implementation inside of a template function. Template functions are the less appreciated cousins of template classes, but they do have useful purposes, particularly when you have a function that offers the same behaviors for different types of data.
Reworking our macro implementation into a template function, heres how that would look:
template <class T> void COMRichErrorHandler( T* p ) throw(...c) { CComQIPtr<ISupportErrorInfo> pError( p ); if ( pError ) { hr = pError->InterfaceSupportsErrorInfo( __uuidof(T) ); if ( hr == S_OK ) { CComPtr<IErrorInfo> pErrInfo; hr = ::GetErrorInfo(0,&pErrInfo); if ( hr == S_OK && pErrInfo ) { throw new com_richerror_exception( pErrInfo ); } } } }
The same code as before, but now it expands once per type T rather than each time it is used (like the earlier macro approach). However, we can still use this template function with the macro to give us the best of both:
#define CHECK_HRESULT_THROW( call,iface ) \ { HRESULT _hr = (call); \ if ( FAILED(_hr) ) COMRichErrorHandler( iface ); }
With this new macro, the template function is expanded for the type of the interface that is passed to the macro. So instead of many lines of code expanded every time we use the macro, we have just a few.
Up to now weve been looking at how to handle rich errors from COM objects. The natural next question (to me at least) is how to report the errors from a COM object in the first place. If youre a fan of ATL, youre in luck because ATL makes this a snap to do.
The first step in ATL is to add support for the ISupportErrorInfo
interfaceI
mentioned in the first article in this series that this can be done easily when
you create a new simple object in ATL using the ATL wizard and checking the
ISupportErrorInfo
checkbox. This will add code into your COM object that
enables the object to report back to callers that it supports rich errors.
The next step is to actually report rich errors when they occur. To do this,
ATL provides the AtlReportError
function. This function takes the UUID
of the interface that is reporting the error (usually the primary interface
of the object), the CLSID of the object itself, and a description. There are
variants of this function that also provide support for passing through a help
filename and topic ID if this makes sense for your application. Behind the scenes,
ATL creates an instance of an error object that supports the IErrorInfo
object using the CreateErrorInfo
SDK function. The error object is then
stored using the SetErrorInfo
SDK function, which stores the interface
for the current thread. If a subsequent error occurs and it generates another
rich error, the current rich error is discarded (released) and the new one takes
its place.
Be aware of a gotcha with using the AtlReportError
function, thoughyou
cannot use this function within the catch
block of a try/catch
because it uses some stack-based memory allocation functions that wont
work properly in a catch block. Here is the warning from Microsoft:
"Caution: Do not use AtlReportError
in C++ catch handlers.
Some overrides of these functions use the ATL string conversion macros internally,
which in turn use the _alloca
function internally. Using AtlReportError
in a C++ catch handler can cause exceptions in C++ catch handlers."
This is one of those lovely notes they put at the bottom of the help, where you probably would miss it if you didn't take time to read the whole set of documentation for the function. I almost missed it myself the first time I read through the text.
Thats it for reporting errors. Its simple enough that you can wrap the reporting itself into some macros of your own to simplify the process of filling out the CLSID and interface UUID parameters as well as loading messages from something like string resources or external stores.
I hope after reading through this series, you see that its simple to add rich error support to your COM objects if you spend some minimal time developing some coding support to make it easier. Although its a little more work than not doing it, the users of your COM objects, particularly in scripting languages or Visual Basic, will appreciate it.
Mark M. Baker is the Chief of Research & Development at BNA Software located
in Washington, D.C.
Do you have a Windows development question? Send it to [email protected].