Introduction
One of the most frustrating scenarios I have as a developer is getting a complaint from a user about a bug without knowing how to reproduce it. A powerful Java contribution, followed suit by C#, was having full fledged exceptions that maintain much data on the circumstances in which they had occurred. This article presents a similar exception base class for C++ whose main features are:
- Having a stack trace of the calls that threw the exception.
- The ability to be propagated between client and server processes when using DCOM, while maintaining the stack trace of both the client and the server.
- Convenient methods to prepend verbal description on the context in which the exception occurred.
JException (J stands for Java, whose exceptions' built-in stack trace inspired me to make this implementation) is written using the Visual Studio 2008 (VC9) compiler; however, the concept can be implemented by previous versions of Visual Studio and other compilers as well.
Running the demo
The demo shows two cases: in the first, an exception is thrown and caught in the same process. The second invokes a server, and an exception thrown by it is handled by the client. The best way to execute the demo is by running the Start.bat batch file, which also kills the server afterwards. The program's output goes both to standard and debug outputs. To view it, either have Debug View for Windows running, or simply invoke the batch file from a command window, or shell. A code snippet from the demo:
void Func3()
{
throw JException("Demo of a failure. Error %d", 67);
}
void Func2()
{
Func3();
}
void DoSomething()
{
Func2();
}
void ActOnTheServer(IDemoDCOM* p_demoCOM)
{
COM_ACTION(p_demoCOM->DoSomethingRemote());
}
int _tmain(int argc, _TCHAR* argv[])
{
try
{
Trace("[Info] An example of throwing an exception " +
"within the same process and thread\n");
DoSomething();
}
catch (JException& e)
{
e.PrependToCause("Failed to Do Something due to: ");
Trace("[Error] %s", e.GetCause().c_str());
Trace("[Error] Exception stack trace: \n%s\n", e.GetStackTrace().c_str());
}
try
{
Trace("[Info] An example of handling an exception " +
"that is thrown by a remote server\n");
IDemoDCOM* p_demoCOM = CreateDemoProxy("localhost");
ActOnTheServer(p_demoCOM);
p_demoCOM->Release();
CoUninitialize();
}
catch (const JException& e)
{
Trace("[Error] %s", e.AsString().c_str());
}
return 0;
}
And that's the output:
[Info] An example of throwing an exception within the same process and thread
[Error] Failed to Do Something due to: Demo of a failure. Error 67
[Error] Exception stack trace:
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (17): Func3
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (23): Func2
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (28): DoSomething
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (42): main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
[Info] An example of handling an exception that is thrown by a remote server
Attempt to create an instance of DemoServer on host: localhost
[Error] Demo of a failure on the server side
f:\exceptions\sandbox\demoserver\demodcom.cpp (12): ServerFunc3
f:\exceptions\sandbox\demoserver\demodcom.cpp (14): ServerFunc2
f:\exceptions\sandbox\demoserver\demodcom.cpp (16): SomeApplicationLogic
f:\exceptions\sandbox\demoserver\demodcom.cpp (26): CDemoDCOM::DoSomethingRemote
77E799F4 (RPCRT4): (filename not available): CheckVerificationTrailer
77EF421A (RPCRT4): (filename not available): NdrStubCall2
77EF5EA5 (RPCRT4): (filename not available): NdrCStdStubBuffer2_Release
771329AF (OLEAUT32): (filename not available): DllGetClassObject
77600C15 (ole32): (filename not available): StgGetIFillLockBytesOnFile
77600BBF (ole32): (filename not available): StgGetIFillLockBytesOnFile
7752AD31 (ole32): (filename not available): CoRevokeClassObject
7752AC56 (ole32): (filename not available): CoRevokeClassObject
7752B771 (ole32): (filename not available): DcomChannelSetHResult
77600E1F (ole32): (filename not available): StgGetIFillLockBytesOnFile
77602DF3 (ole32): (filename not available): WdtpInterfacePointer_UserMarshal
77600DD6 (ole32): (filename not available): StgGetIFillLockBytesOnFile
7752B7AB (ole32): (filename not available): DcomChannelSetHResult
7752B5E1 (ole32): (filename not available): DcomChannelSetHResult
7E418734 (USER32): (filename not available): GetDC
7E418816 (USER32): (filename not available): GetDC
7E4189CD (USER32): (filename not available): GetWindowLongW
7E4196C7 (USER32): (filename not available): DispatchMessageA
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3534):
ATL::CAtlExeModuleT<CDemoServerModule>::RunMessageLoop
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3552):
ATL::CAtlExeModuleT<CDemoServerModule>::Run
c:\program files\microsoft visual studio 9.0\vc\atlmfc\include\atlbase.h (3364):
ATL::CAtlExeModuleT<CDemoServerModule>::WinMain
f:\exceptions\sandbox\demoserver\demoserver.cpp (26): WinMain
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (578): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): WinMainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
-------------------------
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (33): ActOnTheServer
f:\exceptions\sandbox\exceptiondemo\exceptiondemo.cpp (56): main
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (586): __tmainCRTStartup
f:\dd\vctools\crt_bld\self_x86\crt\src\crtexe.c (403): mainCRTStartup
7C817077 (kernel32): (filename not available): RegisterWaitForInputIdle
Embedding the Stack Trace in Exceptions
We notify unexpected conditions by throwing exceptions. For example, suppose we write a library that deals with images, and one of its methods DoSomething(image)
checks for a non empty image prior to doing its thing:
if (image.GetSize() == 0)
throw ("Failed to Do Something, since image is empty");
Assuming our image library is useful, it will be used extensively by our team, and after having our software product debugged and tested, it will be delivered as part of our product to our clients. There, we may get an unpleasant surprise: a client finds a path that causes an empty image to be passed as an argument to DoSomething
which fails. Sounds familiar? DoSomething
throws an exception, and then what? Somewhere the exception is caught, and probably an error message is displayed to the customer. However, often the client will not be able to analyze what he did wrong or how to fix it. He complains, and we try to figure out the stack trace of calls to DoSomething
. Unfortunately, there's no way. Note that when this happens to you in a debug environment, there's no problem: VC breaks for you and you can examine the stack. However, it doesn’t work that way in a runtime environment, either in Alpha, Beta, or at the customer site. Assuming we have some sort of logging, this is where JException becomes helpful. This exception, in its constructor, records the stack trace and maintains it for future investigations. In a typical usage, the exception's cause is displayed to the user and the stack trace is written to the log file.
try
{
DoSomething(image);
...
} catch (const JException& e)
{
AfxMessageBox( e.GetCause(), MB_ICONEXCLAMATION);
Trace(e.GetStackTrace());
}
Propagating Exceptions between Processes with DCOM
The “default” inter-process communication mechanism in Windows based C++ is DCOM. One of its drawbacks, compared to other protocols (say CORBA), is that exceptions that are thrown on the server side can't reach the client. DCOM uses an IErrorInfo
mechanism which is limited to transferring strings from the server to the client in case of errors. However, with JException, we can use this feature to transfer the exception information from the server to the client, as follows:
- Serializing the exception to an XML string;
- Passing the string to the client as
IErrorInfo
; - Reconstructing an equivalent exception on the client side;
- Throwing the reconstructed exception.
The stack trace of the re-thrown exception can now be combined from that of the server and that of the client (textually separated). This gives the programmer the whole picture on the sequence of events that raised the exception.
Usage
Each invocation of a server should catch any exception that was thrown by it and set up the IErrorInfo
interface with the serialized exception. That should be done, of course, on the server side by enclosing the business logic code with a try catch
clause like this:
STDMETHODIMP CServer::DoSomething(const long aParameter)
{
try
{
... }
catch (const JException& e)
{
return CComCoClass::Error(e.ToXml());
}
return S_OK;
}
The opposite should be done on the client side. This code handles both exceptions that were thrown by the server and errors that are inherent to DCOM itself, like communication failure. The latter are also converted to JException.
HRESULT hr;
if ( ( hr = server->DoSomething(anArgument) ) < 0 )
{
USES_CONVERSION;
CComPtr<IErrorInfo> pError = NULL;
JException e;
if ( GetErrorInfo( 0, &pError ) == S_OK && pError != NULL ) {
CComBSTR strError;
pError->GetDescription( &strError );
e = JException::FromXml( strError );
} else {
string cause = TranslateCOMException( hr );
e = JException( cause );
}
throw e;
}
This code should be repeated on each invocation of the server. It can't be converted to a function, since each of the server's functions might have a different signature. Instead, we can use a macro. The file errorHandling.h contains a COM_ACTION
macro, which does exactly this. When using this macro, the code becomes:
COM_ACTION(server->DoSomething(anArgument))
Implementation
Stack Trace
JException generates the stack trace on construction time using a slightly modified version of StackWalker64 (contributed by Jochen Kalmbach). The modifications make its output more suitable for this usage, and allows for a parameter that drops the first given number of frames. It is used to drop the construction of JException itself from the stack trace. It is also useful when reconstructing an exception on the client side from XML. Parsing and reconstructing by themselves are not part of the exception's circumstances, and therefore are dropped. The concept can be adapted to platforms other than Windows, if a stack trace capability is available (for example, pstack for Linux).
Propagation of Exceptions through DCOM
A simple XML (contributed by Dr. Ir. Frank Vanden Berghen) is used for serializing and parsing. It can be easily replaced with any other parser to one's taste.
Deriving from JException
This paragraph is only applicable for DCOM users. To reproduce the appropriate exception from the IErrorInfo
string, a static method JException::FromXml
is used, which in turn uses ExceptionFactory
. An exception that derives from JException
should override GetClassName()
to return a string uniquely identifying its type. This string can then be used by ExceptionFactory
to instantiate the appropriate JException
derived type. Another restrain on exceptions that derive from JException
is that they should have a protected
or private
constructor with a signature: SomeException(const int skipFrames, const string& cause, const string& preStackTrace)
to be used solely by ExceptionFactory
. As an example, see TimeoutException
. Projects that do not use DCOM (and therefore do not use FromXML
) can ignore these requirements.
A Few More Goodies with JException
- A base constructor takes a variable number of arguments to format the cause easily in a
printf
manner. For example, one can:
throw JException("Given value: %d is out of range. "
"Value should be between %d and %d", value, min, max);
One can prepend text to the cause. This is useful to describe the “high-level” context in which an exception has occurred, when the exception is originally thrown from a “low-level” method. For example, a GetByte()
method of some communication class might throw an exception with the cause: "Timeout while attempting to get byte". This is all we can provide at the low level, but at a higher level, it would be useful to catch the exception, provide some more information on the context, and rethrow it as in this example:
try
{
b = socket.GetByte();
} catch (JException& e)
{
e.PrependToCause("Failed to read daily report "
"from transactions server %s due to: ", serverName);
throw e;
}
The elaborated cause: "Failed to read daily report from transaction server Server15 due to: Timeout while attempting to get byte", is much more comprehensive than merely "Timeout while attempting to get byte".
For compatibility reasons, JException
inherits from std::exception
, which merely provides a what()
method to get the cause as C string rather than std::string
.
Summary
JException maintains the stack trace of calls and a description of the cause, which are useful for analyzing the circumstances in which the exception has occurred. It also allows exceptions to cross the DCOM boundary, which lacks exception handling capabilities.
Revision History