There has just got to be an easier way to implement IDispatch::GetIDsOfNames and Invoke, right? The amount of code shown in Variation I for just one property and one method is obnoxious enough—now imagine a complex object with 30 methods and 50 properties! If you don't like 300-page switch statements, it's time to create a type library. In Beeper2 (CHAP14\BEEPER2), the files BEEP0000.ODL and BEEP0007.ODL describe the object's dispinterface in English and German, through use of an interface description. Here's the core of BEEP0000.ODL. (BEEP0007.ODL uses German strings.)
[attributes]
library BeeperTypeLibrary
{
§
[attributes]
interface IBeeper : IUnknown
{
//Properties
[propget, helpstring("The current sound")]
long Sound(void);
[propput]
void Sound([in] long lSound);
//Methods
[helpstring("Play the current sound")]
long Beep(void);
}
[attributes]
dispinterface DIBeeper
{
interface IBeeper;
}
§
}
In the ODL files, we define the object class, CLSID_Beeper, as implementing the dispinterface DIBeeper and the interface IBeeper. DIBeeper is defined as a dispinterface that obtains its methods and properties from IBeeper. IBeeper is defined as having a Sound (Ton) property and a Beep (Piep) method. The same property name is listed twice: once with propget and once with propput.
To see why we're doing this, let's look at what happens when we run the MKTYPLIB tool (described in Chapter 3) on one of these ODL files.
The most common command line for MKTYPLIB, which is used in the samples for this chapter, is as follows:
mktyplib /h <header>.H /l <errors>.LOG /o <library>.TLB <file>.ODL
You'll find the batch file MAKELIB in the BUILD directory of the sample code. MAKELIB lets you generate a header, a log, and a type library from a given ODL file; the files created will have the same name as the ODL file.
When you build Beeper2, its make file calls MKTYPLIB for BEEP0000.ODL with a special command-line switch, /h ibeeper.h, which instructs MKTYPLIB to create that header file.8 The IBEEPER.H output from compiling BEEP0000.ODL is shown in Listing 14-1 beginning on the following page. (This is a built file, so you won't find it on the companion CD until you've actually built Beeper2.) Because the ODL files define an interface and a dispinterface, the header ends up with two interface definitions. In addition, MKTYPLIB automatically turns whatever uuid attributes you defined into DECLARE_GUID statements so that you don't have to define them elsewhere.9
/* This header file machine-generated by mktyplib.exe */
/* Interface to type library: BeeperTypeLibrary */
#ifndef _BeeperTypeLibrary_H_
#define _BeeperTypeLibrary_H_
DEFINE_GUID(LIBID_BeeperTypeLibrary,0x0002115E,0x0000,0x0000,0xC0
,0x00,0x00,0x00,0x00,0x00,0x00,0x46);
DEFINE_GUID(IID_IBeeper,0x0002115C,0x0000,0x0000,0xC0,0x00,0x00
,0x00,0x00,0x00,0x00,0x46);
/* Definition of interface: IBeeper */
DECLARE_INTERFACE_(IBeeper, IUnknown)
{
#ifndef NO_BASEINTERFACE_FUNCS
/* IUnknown methods */
STDMETHOD(QueryInterface)(THIS_ REFIID riid,
LPVOID FAR* ppvObj) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
#endif
/* IBeeper methods */
STDMETHOD_(long, get_Sound)(THIS) PURE;
STDMETHOD_(void, put_Sound)(THIS_ long lSound) PURE;
STDMETHOD_(long, Beep)(THIS) PURE;
};
DEFINE_GUID(DIID_DIBeeper,0x0002115D,0x0000,0x0000,0xC0,0x00
,0x00,0x00,0x00,0x00,0x00,0x46);
/* Definition of dispatch interface: DIBeeper */
DECLARE_INTERFACE_(DIBeeper, IDispatch)
{
#ifndef NO_BASEINTERFACE_FUNCS
/* IUnknown methods */
STDMETHOD(QueryInterface)(THIS_ REFIID riid
, LPVOID FAR* ppvObj) PURE; STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
/* IDispatch methods */
STDMETHOD(GetTypeInfoCount)(THIS_ UINT FAR* pctinfo) PURE;
STDMETHOD(GetTypeInfo)(
THIS_
UINT itinfo,
LCID lcid,
ITypeInfo FAR* FAR* pptinfo) PURE;
STDMETHOD(GetIDsOfNames)(
THIS_
REFIID riid,
OLECHAR FAR* FAR* rgszNames,
UINT cNames,
LCID lcid,
DISPID FAR* rgdispid) PURE;
STDMETHOD(Invoke)(
THIS_
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS FAR* pdispparams,
VARIANT FAR* pvarResult,
EXCEPINFO FAR* pexcepinfo,
UINT FAR* puArgErr) PURE;
#endif
/* Capable of dispatching all methods of interface IBeeper */
};
DEFINE_GUID(CLSID_Beeper,0x0002115B,0x0000,0x0000,0xC0,0x00,0x00
,0x00,0x00,0x00,0x00,0x46);
class Beeper;
#endif
Listing 14-1
MKTYPLIB-generated header file from BEEP0000.ODL.
What we now have in the IBEEPER.H file is the definition of a dispinterface that is nothing more than IDispatch and the definition of a custom interface, IBeeper, which inherits from IUnknown. The functions in IBeeper, however, are a bit odd: where did those get_ and put_ prefixes come from? In the ODL file, the definition of IBeeper listed the Sound function twice: one had the propget attribute, and the other, propput. MKTYPLIB uses these attributes to prepend get_ or put_ to the function name to avoid name conflicts. Method names, of course, need no munging.
But now that we have this custom interface, what are we supposed to do with it? The answer requires that we first look at how we actually get our type information into memory and then look at some of the powerful benefits of OLE's ITypeInfo implementation.
We now have type information, so we can implement IDispatch::GetTypeInfoCount and IDispatch::GetTypeInfo, as shown in Beeper2's BEEPER.CPP file:
STDMETHODIMP CImpIDispatch::GetTypeInfoCount(UINT *pctInfo)
{
//We implement GetTypeInfo, so return 1.
*pctInfo=1;
return NOERROR;
}
STDMETHODIMP CImpIDispatch::GetTypeInfo(UINT itInfo, LCID lcid
, ITypeInfo **ppITypeInfo)
{
HRESULT hr;
ITypeLib *pITypeLib;
ITypeInfo **ppITI=NULL;
if (0!=itInfo)
return ResultFromScode(TYPE_E_ELEMENTNOTFOUND);
if (NULL==ppITypeInfo)
return ResultFromScode(E_POINTER);
*ppITypeInfo=NULL;
switch (PRIMARYLANGID(lcid))
{
case LANG_NEUTRAL:
case LANG_ENGLISH:
ppITI=&m_pITINeutral;
break;
case LANG_GERMAN:
ppITI=&m_pITIGerman;
break;
default:
return ResultFromScode(DISP_E_UNKNOWNLCID);
}
//Load a type lib if we don't have information already.
if (NULL==*ppITI)
{
hr=LoadRegTypeLib(LIBID_BeeperTypeLibrary, 1, 0
, PRIMARYLANGID(lcid), &pITypeLib);
if (FAILED(hr))
{
switch (PRIMARYLANGID(lcid))
{
case LANG_NEUTRAL:
case LANG_ENGLISH:
hr=LoadTypeLib(OLETEXT("BEEP0000.TLB"), &pITypeLib);
break;
case LANG_GERMAN:
hr=LoadTypeLib(OLETEXT("BEEP0007.TLB"), &pITypeLib);
break;
}
}
if (FAILED(hr))
return hr;
hr=pITypeLib->GetTypeInfoOfGuid(DIID_DIBeeper, ppITI);
pITypeLib->Release();
if (FAILED(hr))
return hr;
}
(*ppITI)->AddRef();
*ppITypeInfo=*ppITI;
return NOERROR;
}
This code is perhaps more convoluted than it really needs to be. It is written so that after we've loaded our type information once, we never have to load it again for this instance of the object. The CImpIDispatch class in Beeper2 (BEEPER.H) maintains two ITypeInfo pointers in m_pITINeutral and m_pITIGerman. The switch statement in GetTypeInfo is used to set ppITI to the appropriate CImpIDispatch variable (depending on the language), and if that variable is NULL, we have to load the type library.
The easiest way to load a type library is to call LoadRegTypeLib, which looks for the LIBID you specify under the TypeLib key in the registry and tries to find an LIBID that matches the version number (in our case, 1.0) and the LANGID you specify (9 for English or 7 for German). It also attempts to load the type library listed for either Win16 or Win32 (as appropriate for the environment). If the file listed is a TLB file, LoadRegTypeLib loads it directly. If an EXE10 or a DLL is listed, it extracts the file from that module's resources. Failing that, it attempts to open the file as a compound file and extract the type library from the "\006typelib" stream. In addition, LoadRegTypeLib first looks for an exact match to the LANGID you pass. If that fails, it looks for a match with only the primary LANGID (which we're starting with immediately in this code). If that fails, it looks for LANGID_NEUTRAL (0) as a last resort.
If LoadRegTypeLib does fail, we try to load our TLB files directly with LoadTypeLib as a backup. This function tries to locate the given type library file in the registered DIR key that you place under your type library registration alongside HELPDIR. (See Chapter 3 for a review.)
If LoadTypeLib loads a type library successfully, it automatically creates the proper registry entries for the type library. This is the other reason that using LoadTypeLib as a backup to LoadRegTypeLib is good practice. If we ever get to LoadTypeLib, the next call to LoadRegTypeLib will work.
After we load the type library, we have an ITypeLib pointer. That's not, however, what we need to return from IDispatch::GetTypeInfo; we need instead an ITypeInfo pointer that describes our dispinterface. To get this, we must call ITypeLib::GetTypeInfoOfGUID passing DIID_DIBeeper, the IID of our dispinterface. This returns the ITypeInfo, which we save in our own m_pITI* variable (and, therefore, we call AddRef) and return to the controller. Even though we call ITypeLib::Release, the type library is still loaded because we have an ITypeInfo pointer to the same structure; therefore, we don't need to worry about reloading it later.
After you retrieve an ITypeInfo pointer for your dispinterface's type information, the implementation of IDispatch::GetIDsOfNames becomes considerably easier. Take a look at Beeper2's GetIDsOfNames:
STDMETHODIMP CImpIDispatch::GetIDsOfNames(REFIID riid
, OLECHAR **rgszNames, UINT cNames, LCID lcid, DISPID *rgDispID)
{
HRESULT hr;
ITypeInfo *pTI;
if (IID_NULL!=riid)
return ResultFromScode(DISP_E_UNKNOWNINTERFACE);
hr=GetTypeInfo(0, lcid, &pTI);
if (SUCCEEDED(hr))
{
hr=DispGetIDsOfNames(pTI, rgszNames, cNames, rgDispID);
pTI->Release();
}
return hr;
}
What happened to matching strings to dispIDs? All that information is contained within our type information, and because we have an ITypeInfo pointer to that information (which GetIDsOfNames obtains from GetTypeInfo in the same IDispatch), we can actually ask ITypeInfo to do all the work for us. The OLE API function DispGetIDsOfNames takes an ITypeInfo pointer and the rest of the GetIDsOfNames parameters and performs all the necessary mapping. DispGetIDsOfNames is, however, nothing more than a trivial wrapper around ITypeInfo::GetIDsOfNames:
STDAPI DispGetIDsOfNames(ITypeInfo FAR* ptinfo
, OLECHAR FAR* FAR* rgszNames, unsigned int cNames
, DISPID FAR* rgdispid)
{
return ptinfo->GetIDsOfNames(rgszNames, cNames, rgdispid);
}
You could simply call the ITypeInfo function directly and skip the overhead if you wanted. Internally, ITypeInfo uses its own member functions to extract the names and attributes of the properties, methods, and arguments in the dispinterface, matching names to id values. It's very similar to what we did in Beeper1's manual implementation.
If ITypeInfo::GetIDsOfNames can do so much for name mapping, can it do as much for our implementation of Invoke? Absolutely. In fact, in the implementation of Invoke in Beeper1, almost all of the type coercion and argument extraction is generic enough that a central piece of code could perform those steps based on the type information. What a generic function such as GetIDsOfNames cannot do, of course, is interpret what to do with the properties and methods. Somehow we have to break out the specific dispinterface code from Invoke and structure it so that a central piece of type manipulation code can call it when necessary.
This is the reason why the header file generated by MKTYPLIB defined a custom interface—in our case, IBeeper. This interface describes exactly those operations that are specific to the dispinterface. Beeper2's definition of CBeeper in BEEPER.H actually inherits from the IBeeper interface in IBEEPER.H, and CBeeper's QueryInterface also responds to IID_IBeeper as well as DIID_DIBeeper. The implementation of the get_Sound, put_Sound, and Beep functions is found in BEEPER.CPP:
STDMETHODIMP_(long) CBeeper::get_Sound(void)
{
return m_lSound;
}
STDMETHODIMP_(void) CBeeper::put_Sound(long lSound)
{
if (MB_OK!=lSound && MB_ICONEXCLAMATION!=lSound
&& MB_ICONQUESTION!=lSound && MB_ICONHAND!=lSound
&& MB_ICONASTERISK!=lSound)
{
m_pImpIDispatch->Exception(EXCEPTION_INVALIDSOUND);
return;
}
m_lSound=lSound;
return;
}
STDMETHODIMP_(long) CBeeper::Beep(void)
{
MessageBeep((UINT)m_lSound);
return m_lSound;
}
This is fabulous news! These three simple functions are the only object-specific parts of the listing of IDispatch::Invoke that we saw with Beeper1. The rest of it can be handled in a central piece of code. That central piece is ITypeInfo::Invoke, which is trivially wrapped by the OLE API DispInvoke, the same as DispGetIDsOfNames. The question to answer is, how do we tie our custom IBeeper implementation to whatever ITypeInfo::Invoke does for us? To do that, let's look at Beeper2's 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;
if (IID_NULL!=riid)
return ResultFromScode(DISP_E_UNKNOWNINTERFACE);
hr=GetTypeInfo(0, lcid, &pTI);
if (FAILED(hr))
return hr;
m_wException=EXCEPTION_NONE;
//This is exactly what DispInvoke does, so skip the overhead.
hr=pTI->Invoke((IBeeper *)m_pObj, dispID, wFlags
, pDispParams, pVarResult, pExcepInfo, puArgErr);
if (EXCEPTION_NONE!=m_wException)
{
pExcepInfo->scode
=(SCODE)MAKELONG(m_wException, PRIMARYLANGID(lcid));
FillException(pExcepInfo);
hr=ResultFromScode(DISP_E_EXCEPTION);
}
pTI->Release();
return hr;
}
As we did for GetIDsOfNames, we use our own GetTypeInfo to retrieve the ITypeInfo pointer we want. When we call ITypeInfo::Invoke, the first argument is a pointer to the custom interface from which the dispinterface was defined. In our case, the m_pObj variable in CImpIDispatch is a backpointer to CBeeper. Casting it to (IBeeper *) gives us the correct IBeeper vtable pointer. After ITypeInfo::Invoke performs type coercion and argument extraction, it passes the necessary values to the CBeeper members that implement the Sound property and the Beep method.
Personally, I like the services that ITypeInfo provides: they allow you to implement your functionality as a clean custom interface and eliminate the need for all the grungy work of implementing Invoke yourself. Not every implementation of IDispatch, however, will necessarily use ITypeInfo. For example, an event sink that has to respond to an arbitrary event set cannot install a bunch of arbitrary custom interface functions in a vtable at run time simply to get ITypeInfo to invoke those functions. It's much easier to implement the event handling code inside Invoke manually. There are times and places for both Beeper1 and Beeper2 techniques, which is why I've shown each of them in this chapter.
You might have noticed that CBeeper::put_Sound performs the same argument validation in Beeper2 that it did in Beeper1. In Beeper1, we raised an exception when the wrong value was sent to the Sound property, which was easy because the exception occurred inside Invoke. But CBeeper::put_Sound doesn't have a direct way to return an exception code—the return value of the function is a void, as is required for a property put function. How, then, can it raise an exception?
In Beeper2, I've added a function named Exception to its CImpIDispatch class and a member variable named m_wException for the purpose of supporting some kind of exception model that can be used from within the custom interface implementation. Before calling ITypeInfo::Invoke, CImpIDispatch::Invoke clears exceptions by setting m_wException to EXCEPTION_NONE (0). If a function called from within ITypeInfo::Invoke wants to raise an exception, it calls CImpIDispatch::Exception with the exception code, and Exception just stuffs the exception code into m_wException. When ITypeInfo::Invoke returns, CImpIDispatch::Invoke checks for a nonzero m_wException. If CImpIDispatch::Invoke finds one, it calls our old friend FillException (the same function we have in Beeper1, except now we rely on HELPDIR) to fill the EXCEPINFO structure. Invoke then returns DISP_E_EXCEPTION. This type of mechanism is necessary because ITypeInfo::Invoke has no knowledge of the semantics of our dispinterface, only the syntax.
This solution works well only in a single-threaded operating system, however, or, more appropriately, with a single-threaded controller. This is perfectly acceptable for a Microsoft Windows 3.1 target, but it is not acceptable for a Microsoft Windows NT or Microsoft Windows 95 target, which offers preemptive multitasking. To support multiple threads, we have to maintain some sort of exception code inside CImpIDispatch on a per-thread basis. Fortunately, OLE has just the solution—error objects.
8 MKTYPLIB is very aggressive when you tell it to create a header with the /h switch: it will overwrite any existing file of that name without warning. |
9 The central INC\BOOKGUID.H file used for all the samples in this book redundantly defines these same GUIDs for use from samples such as Beeper1 and AutoCli (in Chapter 15) that don't use a MKTYPLIB-generated header. To prevent compiling errors, the GUIDs in BOOKGUID.H are conditionally excluded from compiling if the symbol GUIDS_FROM_TYPELIB is defined. This symbol is specific to these samples—it's not anything you'll find documented, and you'll |
10 Prior to OLE 2.02 (September 1994), LoadRegTypeLib doesn't work properly with an EXE. |