Variation III: Exceptions Through Error Objects

A mechanism such as the one used in Beeper2 isn't suitable in a multiple-thread environment. To address this problem, the 32-bit versions of OLE support error objects, standard multithreaded devices for raising exceptions. These devices are also known to the 32-bit versions of ITypeInfo::Invoke, so you don't need your own postprocessing after that Invoke returns. We'll use the Beeper3 sample (CHAP14\BEEPER3) to demonstrate how to use error objects.

Each error object is basically an EXCEPINFO structure and an IID (of the dispinterface or interface raising the exception) wrapped in the interfaces ICreateErrorInfo and IErrorInfo. The member functions in ICreateErrorInfo—for example, SetHelpFile—store a value in the underlying EXCEPINFO structure. The functions in IErrorInfo—for example, GetHelpFile—retrieve those values, as illustrated in Figure 14-6.

Figure 14-6.

An error object wraps an EXCEPINFO structure and an IID.

To raise an exception using error objects, you perform the following steps:

Before calling ITypeInfo::Invoke, call the OLE API function SetErrorInfo(0L, NULL) to clear the error object for the current thread. OLE manages a list of error objects on a per-thread basis through this function.

When an exception occurs from within a custom interface function called from ITypeInfo::Invoke, call CreateErrorInfo(&pICreateErrorInfo) to obtain the ICreateErrorInfo pointer for a new error object.

Call ICreateErrorInfo functions to set your exception information in the object.

Call pICreateErrorInfo->QueryInterface(IID_IErrorInfo &pIErrorInfo) to obtain an IErrorInfo pointer that you pass to SetErrorInfo(0L, pIErrorInfo). (This function takes an IErrorInfo pointer, not an ICreateErrorInfo pointer.) This assigns the error object to the current thread, which releases any previous object.

Release both your IErrorInfo and your ICreateErrorInfo pointer. SetErrorInfo will call AddRef as required to hold on to the pointer.

Return from your custom interface function. ITypeInfo::Invoke will then call GetErrorInfo to retrieve the IErrorInfo pointer for the current thread. If you do not raise an exception, this returns S_FALSE, and Invoke proceeds normally. Otherwise, Invoke extracts the information in the error object into the EXCEPINFO structure that you were given in your own IDispatch::Invoke and returns DISP_E_EXCEPTION.

In Beeper3's IDispatch implementation (found in BEEPER3\BEEPER.CPP), we can follow this procedure. First comes the Beeper's IDispatch::Invoke:


STDMETHODIMP CImpIDispatch::Invoke(DISPID dispID, REFIID riid
, LCID lcid, unsigned short wFlags, DISPPARAMS *pDispParams
, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
HRESULT hr;
ITypeInfo *pTI;

[Code to check for errors and obtain pTI omitted]

//Clear exceptions.
SetErrorInfo(0L, NULL);

hr=pTI->Invoke((IBeeper *)m_pObj, dispID, wFlags
, pDispParams, pVarResult, pExcepInfo, puArgErr);

pTI->Release();
return hr;
}

The implementation of CBeeper::put_Sound is the same in Beeper3 as in Beeper2, but the implementation of CImpIDispatch is much different because it implements the remaining steps in the preceding list:


void CImpIDispatch::Exception(WORD wException)
{
HRESULT hr;
ICreateErrorInfo *pICreateErr;
BOOL fSuccess;
LPTSTR psz;
LPOLESTR pszHelp;
UINT idsSource;
UINT idsException;
DWORD dwHelpID;

[Other code omitted]

//Not much we can do if this fails.
if (FAILED(CreateErrorInfo(&pICreateErr)))
return;

psz=(LPTSTR)malloc(1024*sizeof(TCHAR));

if (NULL==psz)
{
pICreateErr->Release();
return;
}

fSuccess=FALSE;

switch (wException)
{
case EXCEPTION_INVALIDSOUND:
pICreateErr->SetGUID(DIID_DIBeeper);
dwHelpID=HID_SOUND_PROPERTY_LIMITATIONS;

pszHelp=OLETEXT("beep0000.hlp");
idsSource=IDS_0_EXCEPTIONSOURCE;
idsException=IDS_0_EXCEPTIONINVALIDSOUND;

switch (langID)
{
case LANG_GERMAN:
idsSource=IDS_7_EXCEPTIONSOURCE;
idsException=IDS_7_EXCEPTIONINVALIDSOUND;
pszHelp=OLETEXT("beep0007.hlp");
break;

case LANG_ENGLISH:
case LANG_NEUTRAL:
default:
break;
}

fSuccess=TRUE;
break;

default:
break;
}


if (fSuccess)
{
IErrorInfo *pIErr;

pICreateErr->SetHelpFile(pszHelp);
pICreateErr->SetHelpContext(dwHelpID);

LoadString(g_hInst, idsSource, psz, 1024);
pICreateErr->SetSource(psz);

LoadString(g_hInst, idsDescri, psz, 1024);
pICreateErr->SetDescription(psz);

hr=pICreateErr->QueryInterface(IID_IErrorInfo
, (PPVOID)&pIErr);

if (SUCCEEDED(hr))
{
SetErrorInfo(0L, pIErr);
pIErr->Release();
}
}

free(psz);

pICreateErr->Release();
return;
}

This is all quite similar to the exception filling functions we've already seen. The only difference is in how we set the EXCEPINFO fields.

Localized Error Objects

If you have a sharp eye, you might have caught an ambiguity in the implementation of the Exception function. Where does that little langID value come from? It's not listed anywhere, yet I use it to determine the language in which to report the error information.

The source of langID is the lcid parameter in CImpIDispatch::Invoke—that's the only function that receives it during an invocation. Somehow we have to get that value from Invoke to Exception. We could use a member variable in CImpIDispatch, but that would make our whole implementation unsuitable for multiple threads—which is exactly what we are trying to avoid by doing this fancy error object stuff in the first place!

Because error objects are used primarily on Win32 platforms, we can take advantage of what is known as thread-local storage (TLS). TLS allows us to attach a pointer to a thread so that the system maintains one pointer per thread, just as OLE maintains one error object per thread. In order to use TLS, we have to obtain a TLS index with the Win32 API function TlsAlloc. This occurs, along with the reverse step, TlsFree, in the Beeper DLL's entry point found in DBEEPER.CPP:


DWORD       g_dwTLS;       //For thread-local storage

BOOL WINAPI LibMain32(HINSTANCE hInstance, ULONG ulReason
, LPVOID pvReserved)
{
g_hInst=hInstance;

if (DLL_PROCESS_DETACH==ulReason)
{
TlsFree(g_dwTLS);
return TRUE;
}
else
{
if (DLL_PROCESS_ATTACH!=ulReason)
return TRUE;

g_dwTLS=TlsAlloc();
}

return TRUE;
}

The TLS index is stored in the global variable g_dwTLS, which is declared as an extern in Beeper3's BEEPER.CPP file. This gives us the means to assign a pointer to the thread by calling TlsSetValue and to retrieve that pointer elsewhere by calling TlsGetValue. Both functions take the index as the first parameter. So to communicate the LANGID from Invoke to Exception, Invoke declares a LANGID on the stack (which is already thread-local), saves the language from lcid in it, and assigns its pointer to the thread:


STDMETHODIMP CImpIDispatch::Invoke(...)
{
§
LANGID langID=PRIMARYLANGID(lcid);

TlsSetValue(g_dwTLS, &langID);
§
}

Inside Exception, we can then retrieve the same value that is currently on Invoke's stack, which is where we get the right langID value:


void CImpIDispatch::Exception(WORD wException)
{
§
LANGID langID=LANG_NEUTRAL;
LANGID *pLangID;

pLangID=(LANGID *)TlsGetValue(g_dwTLS);

if (NULL!=pLangID)
langID=*pLangID;

§
}

No magic—just straightforward multithreaded Win32 programming!

The ISupportErrorInfo Interface

One final piece of the error object framework that I have not mentioned yet is the ISupportErrorInfo interface, which lets an object say, "Hey, I support error objects!" You implement this interface on the same object as IDispatch and your custom interface.11 It has a single function (besides IUnknown) named InterfaceSupportsErrorInfo. This function gives the consumer of a thread's error object—for example, ITypeInfo::Invoke or a controller itself—a way to check whether the interface that's being called uses error objects to report exceptions. In other words, you can use this function to determine whether GetErrorInfo will return something meaningful or just plain trash—such as an exception raised long ago by something else in the same thread. ITypeInfo::Invoke should call QueryInterface for IID_ISupportErrorInfo through your custom interface pointer (the first argument to ITypeInfo::Invoke, remember?) and then call InterfaceSupportsErrorInfo with the dispinterface IID. If either step fails, Invoke should not call GetErrorInfo.12

In any case, Beeper3 implements this interface on its CBeeper object using the class CImpISupportErrorInfo—which is about as degenerate as you can get. The real core of this interface—its singular interesting member function—is very simple to implement:


STDMETHODIMP CImpISupportErrorInfo::InterfaceSupportsErrorInfo
(REFIID riid)
{
if (DIID_DIBeeper==riid)
return NOERROR;

return ResultFromScode(S_FALSE);
}

11 Older documentation is very misleading on this point. It mentions that this interface is put on the same object as IErrorInfo, but you don't implement that interface on your automation object! It should say that the interface goes alongside IDispatch.