Kim Gräsman, September 2003
This article assumes that you're familiar with COM and ATL
While COM error info works fine for passing simple error messages, it's a fairly restricted error reporting framework. What I miss the most is a mechanism for chaining error info, so as to not end up overwriting error info in multi-tiered COM applications.
There has been quite a bit of discussions on custom IErrorInfo implementations on various mailing lists, but I've never seen any actual results of the research, besides people saying "Hey, it works for me".
This article presents a custom implementation which is perfectly compatible with the existing one, as easy to use (if not easier), and which allows error info chaining.
From the aforementioned mailing lists, I have managed to gather a set of requirements for custom error info implementations:
IErrorInfo, but may implement other interfacesBased on these requirements, I have built a simple chained error info implementation, conceptually similar to Java's chained exceptions.
Listed below are the key characteristics of this implementation, and I will cover each of these in detail later
IErrorInfo interface pointerCreateChainedErrorInfo, superseding the standard CreateErrorInfoI implemented this as a simple ATL project.
The IErrorInfo implementation is very straightforward. It maintains a couple of BSTRs for source, description and helpfile, a GUID for the interface called through when the error occurred, and a DWORD for the help context. All these properties are read-only for all intents and purposes, they are only written once when the object is created.
Besides this standard implementation, we need another interface, to express the error info chain I have mentioned previously. It needs to maintain a pointer to an "inner" error info, but also needs to remember what the "inner" HRESULT was, since that is overridden in each step as well.
"Inner" in this context may need some further explanation. Many COM-based applications are layered, that is the client calls a facade object, which in turn may call another object, which probably uses ADO to communicate with a database, and then the result is transferred upwards through the layers back to the client.
Using the stock IErrorInfo techniques, if something fails, the client will only ever see the error information from the outer-most COM object, while error info for the two underlying tiers is lost. Some applications propagate the original HRESULT all the way to the client, to provide the debugging developer with at least an error code. Some will refrain from overwriting error information all the way up, and present the end-user with the error description as well. Yet others do overwrite error information at every tier, thereby making it next to impossible to debug the application, since all you have at the client machine is "An unexpected error occurred (0x80004005)."
Some applications have exquisite log frameworks for logging each and every failure HRESULT, which can be a good idea for debugging, but sooner or later becomes a nuisance, once clients start expecting failures, and handling them gracefully at their end. While the application looks fine to the end-user, there are hundreds of error log records generated for gracefully handled errors.
This is where chained error info comes to the rescue. By keeping track of, and keeping alive, the last known error info before setting new error info, we can build a chain of error info records. An interface for reading this information could look like this:
interface IChainedErrorInfo : IUnknown
{
HRESULT GetInnerErrorInfo(/*[out, retval]*/ IErrorInfo** ppErrorInfo);
HRESULT GetInnerHRESULT(/*[out, retval]*/ HRESULT* phResult);
};
By implementing both IErrorInfo and IChainedErrorInfo on a single COM coclass, we can piggy-back this extra data and functionality in the stock error info propagation framework.
In order to handle all this semi-automatically, we need an export from our library that handles the chaining behind the scenes, so the caller doesn't have to bother with connecting error info objects when reporting errors.
CreateChainedErrorInfo has the following signature:
HRESULT __stdcall CreateChainedErrorInfo( HRESULT hrInner, REFGUID guid,
LPCOLESTR pszSource, LPCOLESTR pszDescription,
LPCOLESTR pszHelpFile, DWORD dwHelpContext,
IErrorInfo** ppErrorInfo);
You'll note that compared to CreateErrorInfo, this function takes plenty of parameters. I designed it this way to keep usage more succinct, I've never been very fond of the create-populate-QI sequence for IErrorInfo.
CreateChainedErrorInfo does the following:
GetErrorInfo
hrInner and the existing IErrorInfo pointer, as well as the rest of the data members
IErrorInfo, and returns a pointer to that interface
See the source for details.
It's interesting to note here that the inner-most error info can actually be the stock implementation (or any other custom implementation, for that matter), since CreateChainedErrorInfo only ever handles IErrorInfo. This is good, because it allows us to pass that ADO error information from the inner-most tier, as well as chain other IChainedErrorInfo implementations.
To propagate error info, we can just use SetErrorInfo as usual and pass in the returned IErrorInfo pointer.
Rumor has it that the standard IErrorInfo implementation marshals by value, so naturally I wanted my own implementation to do so as well. Besides, from a round-trip standpoint it makes very much sense to marshal both these interfaces by value, since they are completely read-only.
For MBV to work properly, the implementation is marked as ThreadingModel='Both', which seems like a good idea, regardless, since this object will be used from any conceivable apartment type. Since it is read-only, as mentioned previously, thread synchronization is not an issue.
For your information, I have not taken any measures to preserve byte-ordering in the marshalled packets, so it might be a bad idea to use this library for cross-platform development. In case you have access to Windows NT on Alpha, feel free to try it, but at your own risk.
See the accompanying source code for more details on the implementation.
When a client receives a failure HRESULT from a COM object, it can now call GetErrorInfo to get detailed error information, as usual. If the client is smart, it can QueryInterface the returned IErrorInfo pointer for IChainedErrorInfo, and call GetInnerHRESULT to get the failed result from the inner object, and GetInnerErrorInfo to get an IErrorInfo pointer to the inner error information. This pattern can be applied recursively until QueryInterface fails, or until GetInnerErrorInfo returns a NULL pointer.
If the client was written two years ago, it can just skip the QueryInterface and make do with the censored error info from the outer-most COM object.
To make this process easier and more transparent, I built an ATL-style wrapper for these error info interfaces, called CComErrorInfo, which has facilities both for setting and getting error info.
Client-side usage in my proof-of-concept console application looks like this:
void DumpErrorInfo(HRESULT hr, const CComErrorInfo& cei, int nTier = 0)
{
if(cei != NULL)
{
printf("\nCurrent tier: %d\n-------------------------\n", nTier);
CComBSTR strSource, strDescription, strHelpFile;
DWORD dwHelpContext;
cei->GetDescription(&strDescription);
cei->GetSource(&strSource);
cei->GetHelpFile(&strHelpFile);
cei->GetHelpContext(&dwHelpContext);
printf("HRESULT: 0x%X\n", hr);
printf("Source: %ls\n", strSource);
printf("Description: %ls\n", strDescription);
printf("HelpFile: %ls\n", strHelpFile);
printf("HelpContext: %lu\n", dwHelpContext);
printf("-------------------------\n", nTier);
DumpErrorInfo(cei.InnerHRESULT(), cei.Inner(), ++nTier);
}
else if(FAILED(hr))
{
printf("\nCurrent tier: %d\n-------------------------\n", nTier);
printf("HRESULT: 0x%X\n", hr);
printf("-------------------------\n", nTier);
}
}
The routine calls itself recursively until the inner error info returned is NULL. This sample uses the library to its fullest, showing the full chain of error information, whereas a real implementation would probably want to show the sanitized outer-most error message, and log the rest to file. What to do with the error information is entirely up to the client.
I have packaged the presented solution as a mini-SDK, ready for public use. However, I realize that most people will probably want to use this idea as a starting point for even more elaborate error reporting, so the full source code, as well as the test/proof-of-concept code is available for download.
The files listed below are placed in the public domain — you are free to use them any way you see fit, as long as I am void of any responsibility for problems incurred by this usage.
| chainedei-sdk.zip | Chained Error Info SDK (.DLL, .LIB and .H) |
| chainedei-poc.zip | Chained Error Info proof-of-concept console app (includes SDK) |
| chainedei-src.zip | Chained Error Info source code |
I would love it if you reported any bugs to me, so I can improve the library where necessary.