COM, ATL

ATL and Dual Interfaces

디버그정 2008. 8. 28. 00:44
ATL and Dual Interfaces
(Page 1 of 2)
George Shepherd and Scot Wingo
Microsoft's Active Template Library provides template implementations of many of the most common COM idioms. However, to work with the more advanced COMisms in ATL, you'll want to understand what ATL is doing behind the façade of wizardry.

Scot is a cofounder of Stingray Software. He can be contacted at mfc_faq@stingsoft.com. George is a senior computer scientist with DevelopMentor and can be contacted at 70023.1000@compuserve.com. They are the coauthors of MFC Internals (Addison-Wesley, 1996).


Writing a COM object involves banging out a lot of code, much of it necessary just to get a COM object working. Once you're familiar with the basic idea of COM, it seems as though much of the code could be factored out.

This is just what Microsoft's Active Template Library (ATL) does. ATL provides template implementations of most of the common COM idioms. Visual C++ even gives you a wizard for developing COM interfaces. For the most part, you can get away with using Visual C++'s wizard technology for writing simple COM classes, without understanding how ATL works. However, to work with the more advanced COMisms in ATL, you'll want to understand what ATL is doing behind the façade of wizardry.

For example, every COM object has to support the IUnknown interface. While IUnknown comprises just three functions -- QueryInterface, AddRef, and Release -- everybody who writes COM objects has to grind out similar code. IUnknown implementations tend to be fairly standard, and an IUnknown implementation makes a good candidate for code that should be factored out. ATL splits its IUnknown implementation among CComObjectRootBase, CComObjectRootEx, and CComObject.

ATL is flexible when it comes to implementing IUnknown. As far as AddRef and Release go, you can apply the correct code for your target threading model through templates. In addition, ATL provides a decent implementation of QueryInterface using interface maps. (They're just interface lookup tables.) ATL's version of QueryInterface actually turns out to be flexible, providing support for such esoteric COM class composition techniques as aggregation and tear-off interfaces. Another aspect of writing COM classes that turns out to be standard boilerplate-type code is implementing dual interfaces. This month, we'll look at ATL's dual-interface machinery to see how it works under the hood.

What's a Dual Interface?

COM is about getting software components to interoperate with each other, even though they may be written using different development environments. That's a tall order, given the variety of software-development tools. However, when it comes down to it, languages fall into two general categories -- compiled and interpreted. A compiled language like C++ tends to be static in terms of data types you can use in the program. To generate the correct code, the compiler needs to know about the program's data structures and the like. In other words, all the data types and functions need to be declared ahead of time. Not so with interpreted languages, which tend to be much more dynamic. The interpreter doesn't need to know about data types and function calls until it's necessary, then figures them out on the fly.

COM gets different pieces of software to cooperate by defining a specific binary layout. COM objects talk to one another by adhering to defined interfaces, which turn out to be function tables at the binary layout. C++ uses pure abstract base classes to conveniently define interfaces. Listing One for example, is a valid COM interface defined in C++.

All COM interfaces derive from a base interface named IUnknown, which allows clients of the interface to discover an object's additional functionality at run time. IUnknown also provides a means of lifetime control for the object by providing a reference-counting scheme for the object. Clients of ISomeInterface might use the interface as demonstrated in Listing Two.

COM interfaces are defined and published for the world to see. When a statically typed language like C++ encounters a pointer to a COM interface defined in a header file, the compiler understands the binary layout of the interface because it's defined in the header file. The compiler can generate the correct code for jumping to the right function in the table because it understands the entire picture.

Scripting environments, however, don't work this way. A scripting environment figures out how to make the function call at run time, not at compile time. In this case, the standard COM interface doesn't work. COM, therefore, defines a single well-known interface named IDispatch that scripting languages understand how to deal with. IDispatch is just another COM interface, which looks like Listing Three.

Rather than accessing functionality by understanding certain positions in a function table, clients of IDispatch access functionality by understanding how to call IDispatch::Invoke. To call Invoke, a client acquires a token representing the function it wants to call. (IDispatch::GetIDsOfNames is one way to acquire a token.) The client calls IDispatch::Invoke, passing the token representing the function. Arguments are passed using a self-describing data type known as VARIANTs. Notice that this is a dynamic situation -- clients can acquire the invocation tokens at run time given human readable names. Because Invoke uses VARIANTs (and not predetermined data types) as arguments, figuring out what kind of arguments to pass is also dynamic.

In its early days, Visual Basic required clients to call GetIDsOfNames to acquire the dispatch tokens before calling Invoke. These days, Visual Basic can figure out the dispatch tokens beforehand by consulting the type library. In addition, Visual Basic can even call regular COM interfaces and ignore the entire dynamic dispatching mechanism of IDispatch. However, there are still a number of environments that require IDispatch. One example is using VBScript inside a web page.

Before jumping into dual interfaces, take a minute to ponder IDispatch. The client has to do a lot of work before it can call Invoke. The client first has to acquire the invocation tokens. Then the client has to set up the VARIANT arguments. On the object side, the object has to decode all those VARIANT parameters, make sure they're correct, put them on some sort of stack frame, then make the function call. As you can imagine, this is complex and time-consuming. If you're writing a COM object and expect some of your clients to use scripting languages and other clients to use languages like C++, you've got a dilemma -- you have to include IDispatch or lock your scripting-language clients out. If you provide only IDispatch, then you make it inconvenient to access your object from C++. Of course, you can provide access through both IDispatch and through a custom interface, but that involves bookkeeping.

Dual interfaces handle this problem. A dual interface is simply IDispatch with functions pasted onto the end. For example, Listing Four is a valid dual interface. Because ISomeDualInterface derives from IDispatch, the first seven functions of ISomeDualInterface are those of IDispatch. Clients that understand only IDispatch (VBScript for instance) look at the interface as just another version of IDispatch and feed DISPIDs to the Invoke function in the hopes of invoking a function. Clients that understand vtable-style custom interfaces look at the entire interface and ignore the middle four functions (the IDispatch functions) and concentrate on the first three functions (IUnknown) and the last three functions (the ones that represent the interface's core functions). Figure 1 shows the vtable layout of ISomeDualInterface.

Raw C++ and IDispatch

IDispatch is a nasty interface, especially the Invoke function. To get Invoke working, the object has to unpack the list of VARIANTs, decode them, set them up into some appropriate execution context, perform the function, and package the result back into a VARIANT.

Even though IDispatch is fairly complex, it turns out to be pretty easy to implement -- even if you're using raw C++. We'll take a look at the most common way to implement IDispatch in raw C++ because that's the approach ATL uses. After understanding a raw implementation of IDispatch, ATL's approach seems obvious.

The most straightforward way to implement dual interfaces in C++ is to describe the interface in IDL, use the IDL to generate a type library, and then delegate calls to Invoke and GetIDsOfNames to the type library. Defining a dual interface in IDL looks something like Listing Five.

You can think of IDL as an attribute-extended version of C. The syntax is similar. However, IDL differs from C in that IDL is a purely declarative language. Notice in Listing Five that the IDL starts out with square braces. These square braces indicate that some attributes are to follow. The keyword object indicates that what follows is a COM interface. The uuid keyword names the interface. Finally, the dual keyword says the following interface is a dual interface. This keyword causes some additional information to be inserted into the registry when the type library is registered.

Next comes the interface. The IDL syntax indicates the interface derives from IDispatch. Also notice that each interface method has some attributes (as shown in the square braces) attached. In this case, the attributes are the DISPIDs for each function (that is, the tokens a client will need to use to invoke a method via IDispatch::Invoke).

Compiling this IDL file through the MIDL compiler yields a header file (useful for C++ implementations) and a type library -- a binary representation of the information included in the IDL file. Getting IDispatch to work is simply a matter of completing several more steps.

To get the dual interface working, first write a C++ class that includes IDispatch as one of the interfaces to implement. The constructor of the C++ class can use the API function LoadTypeLib to load the IDL-generated type library into memory. LoadTypeLib returns a COM interface named ITypeLib, which includes a function named GetTypeInfoOfGUID. Given the GUID of the dual interface, GetTypeInfoOfGUID returns an interface named ITypeInfo that represents the information about the dual interface. Then you can implement GetTypeInfo, Invoke, and GetIDsOfNames through the type information from the type library. Listing Six illustrates a dual interface in raw C++. Notice how the C++ implementation loads the type library straight away and uses ITypeInfo to do the nasty task of implementing Invoke and GetIDsOfNames.

ATL and IDispatch

ATL's implementation of IDispatch is similar to the approach just outlined. ATL's implementation of IDispatch lives in the class IDispatchImpl. Objects that want to implement a dual interface just include the IDispatchImpl template in the inheritance list as shown in Listing Seven.

In addition to including the IDispatchImpl template class in the inheritance list, the object includes entries for the dual interface and for IDispatch in the interface map so QueryInterface works properly; see Listing Eight. As you can see, the IDispatchImpl template class arguments include the dual interface itself, the GUID for the interface, and the GUID representing the type library holding all the information about the interface. In addition to these template arguments, the IDispatchImpl class has some optional parameters not illustrated. The template parameter list also includes room for a major and minor version of the type library. Finally, the last template parameter is a class for managing the type information. ATL provides a default class called CComTypeInfoHolder.

In Listing Six, CSomeObject called LoadTypeLib and ITypeLib::GetTypeInfoOfGuid in the constructor and held on to the ITypeInfo pointer for the life of the class. ATL does things a little differently by using the CComTypeInfoHolder to help manage the ITypeInfo pointer. CComTypeInfoHolder maintains an ITypeInfo pointer as a data member and wraps the critical IDispatch-related functions GetIDsOfNames and Invoke.

Clients acquire the dual interface by calling QueryInterface for IID_ISomeDualInterface (the client will also get this interface by calling QueryInterface for IDispatch). If the client calls Function1, Function2, or Function3 on the interface, the client accesses those functions directly (as it would for any other COM interface).

When a client calls IDispatch::Invoke, the call lands inside IDispatchImpl's Invoke function as you'd expect. From there, IDispatchImpl::Invoke delegates to the CComTypeInfoHolder to perform the invocation. Unlike Listing Six, the CComTypeInfoHolder doesn't call LoadTypeLib until an actual call to Invoke or GetIDsOfNames is made. CComTypeInfoHolder has a member function named GetTI, which consults the registry for the type information (using the GUID and any major/minor version numbers passed in as a template parameter). Then CComTypeInfoHolder calls ITypeLib::GetTypeInfo to get the information about the interface. At that point, the type information holder just delegates to the type information pointer. IDispatchImpl implements IDispatch::GetIDsOfNames in the same manner.

Multiple Dual Interfaces

What happens if you want to include two dual interfaces in a single ATL-based COM class? All dual interfaces begin with the seven functions of IDispatch. The problem occurs whenever the client calls QueryInterface for IID_IDispatch. As a developer, you need to choose which version of IDispatch to pass out. For example, consider the CDualTestOb class in Listing Nine again.

The interface map is where the QueryInterface for IID_IDispatch is specified. ATL has a specific macro for handling this situation. The macro is named COM_INTERFACE_ENTRY2. To get QueryInterface working correctly, all you need to do is decide which version of IDispatch the client should get when they ask for IDispatch like Listing Ten. In this case, a client asking for IDispatch gets a pointer to ISomeDualInterface (whose first seven functions include the IDispatch functions).

Conclusion

ATL's implementation of IDispatch is the same as most raw C++ implementations. The Zen of ATL matches the Zen of COM in that IDL is the starting point for the entire project. Given some valid IDL, the MIDL compiler can easily generate everything you need to get a solid implementation of IDispatch working. Since the information needed for implementing IDispatch::Invoke is in the type library, it's sensible to use the type library to implement IDispatch::Invoke. The main way ATL differs from raw C++ is that ATL manages the type information in a wrapper class called CComTypeInfoHolder.


Listing One

struct ISomeInterface : public IUnknown {   HRESULT SomeFunction();
   HRESULT AnotherFunction();
};
Back to Article

Listing Two

void UseSomeInterface(ISomeInterface* pSomeInterface) {   pSomeInterface->SomeFunction();
   pSomeInterface->AnotherFunction();
}
Back to Article

Listing Three

interface IDispatch : public IUnknown {   HRESULT GetTypeInfoCount(UINT FAR* pctinfo);
   HRESULT GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo);
   HRESULT GetIDsOfNames(REFIID riid, char FAR* FAR* rgszNames,
                         UINT cNames, LCID lcid, DISPID FAR* rgdispid);
   HRESULT Invoke(DISPID dispidMember, REFIID riid, LCID lcid,
                  WORD wFlags, DISPPARAMS FAR* pdispparams,
                  VARIANT FAR* pvarResult, EXCEPINFO FAR* pexcepinfo,
                  UINT FAR* puArgErr);
};
Back to Article

Listing Four

interface ISomeDualInterface : public IDispatch {   virtual void HRESULT Function1();
   virtual void HRESULT Function2();
   virtual void HRESULT Function3();
};
Back to Article

Listing Five

[   object,
   uuid(271165EE-2110-11D1-8CAA-FD10872CC837),
   dual
]
interface IDualTestOb : IDispatch
{
   [id(1)] HRESULT Function1();
   [id(2)] HRESULT Function2();
   [id(3)] HRESULT Function3();s
};
Back to Article

Listing Six

class CSomeObject : public ISomeDualInterface {  ITypeInfo *m_pTypeInfo;
public:
  CSomeObject(void) : m_pTypeInfo(0) {  
    ITypeLib *ptl = 0;
    if (SUCCEEDED(LoadTypeLib(g_wszTypeLib, &ptl))) {
      ptl->GetTypeInfoOfGuid(IID_ISomeDualInterface, &m_pTypeInfo);  
      ptl->Release();}
  }
  HRESULT QueryInterface(REFIID riid, void **ppv) {
    if (riid == IID_ISomeDualInterface) 
      *ppv = m_pTypeInfo ? (ISomeDualInterface*) this : 0;
    else if (riid == IID_IDispatch) 
      *ppv = m_pTypeInfo ? (DIFoo*) this : 0;
    else if (riid == IID_IUnknown) 
      *ppv = (DIFoo*) this;
    else *ppv = 0;
     :
  }
  HRESULT GetTypeInfoCount(UINT * pticount) 
  {
    *pticount = 1;
    return NOERROR;
  }
  HRESULT GetTypeInfo(UINT i, LCID lcid, ITypeInfo**ppti)
  {
    if (i != 0) return DISP_E_BADINDEX;
    (*ppti = m_pTypeInfo)->AddRef();
    return NOERROR;
  }
  HRESULT Invoke(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags,  
                 DISPPARAMS * pdispparams, VARIANT * pvarResult, 
                 EXCEPINFO * pei, UINT * puArgErr) {
    return m_pTypeInfo->Invoke((ISomeDualInterface*)this, dispid, wFlags, 
                                 pdispparams, pvarResult, pei, puArgErr);
  }  
  HRESULT GetIDsOfNames(REFIID riid, OLECHAR* rgszNames[],
                        UINT cNames, LCID lcid, DISPID rgdispid[]) {
   return m_pTypeInfo->GetIDsOfNames(rgszNames, cNames, rgdispid);
  }
   ...
// Implement the rest of IUnknown and the dual interface functions...
Back to Article

Listing Seven

class ATL_NO_VTABLE CDualTestOb :   public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CDualTestOb, &CLSID_DualTestOb>,
  public IDispatchImpl<ISomeDualInterface, &IID_ISomeDualInterface, 
                       &LIBID_DUALTESTLib> {
};
Back to Article

Listing Eight

BEGIN_COM_MAP(CDualTestOb)   COM_INTERFACE_ENTRY(ISomeDualInterface)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
Back to Article

Listing Nine

class ATL_NO_VTABLE CDualTestOb :   public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CDualTestOb, &CLSID_DualTestOb>,
  public IDispatchImpl<ISomeDualInterface, &IID_ISomeDualInterface, 
                       &LIBID_DUALTESTLib> 
  public IDispatchImpl<IAnotherDualInterface, &IID_IAnotherDualInterface, 
                       &LIBID_DUALTESTLib> {
};
Back to Article

Listing Ten

BEGIN_COM_MAP(CDualTestOb)   COM_INTERFACE_ENTRY(ISomeDualInterface)
   COM_INTERFACE_ENTRY(IAnotherDualInterface)
   COM_INTERFACE_ENTRY2(IDispatch, ISomeDualInterface)
END_COM_MAP()

Back to Article

DDJ