Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Undocumented Corner


Dr. Dobb's Journal August 1997: ATL and the <i>IUnknown</i> Interface

ATL and the IUnknown Interface

Scot is a cofounder of Stingray Software. He can be contacted at [email protected]. George is a senior computer scientist with DevelopMentor and can be contacted at [email protected]. They are the coauthors of MFC Internals (Addison-Wesley, 1996).


COM is like a beautiful, ever-blossoming flower whose seed is the IUnknown interface. Once you grasp fully the intent behind the IUnknown interface, you understand how one of the most important features of COM lies in its extensibility, or the ability of a client to widen the connection to an object arbitrarily through QueryInterface. As an object implementor, this gives you tremendous flexibility for pinning as much or as little functionality on an object as you'd like. In addition to QueryInterface, IUnknown's AddRef and Release functions are a reasonable way to control an object's lifetime.

Unfortunately, the complex morass of details necessary to get COM objects up and running often overshadows the fundamental beauty of COM. For example, implementing COM classes involves writing a great deal of code that remains the same from one class implementation to another. IUnknown implementations are generally the same for every COM class you encounter -- the main difference between them is the interfaces exposed by each class. But just as you no longer need to understand assembly language to get software working these days, pretty soon, you'll no longer need to understand all the nuances of IUnknown and COM to get your COM-based software up and running. We're not quite there yet, but the Active Template Library (ATL) from Microsoft represents a great first step in that direction.

This month we'll take a look at the heart of ATL, including its support for multithreading and its various implementations of IUnknown.

ATL Basics

If you've experimented at all with ATL, you've seen how it simplifies the process of implementing COM classes. The tool support is very good -- it's almost as easy to develop COM classes using the new version of Visual C++ as it is to create MFC-based programs. Just use AppWizard to create a new ATL-based class. However, instead of using ClassWizard (as you would to handle messages and add dialog-box member variables), ATL employs ClassView for adding new functions to the interfaces, then simply fills them in within the C++ code generated by ClassView. AppWizard generates all the necessary code for implementing your class, including an implementation of IUnknown, a server module to house your COM class, and a class object that implements IClassFactory.

Writing COM objects this way is certainly more convenient than most other methods. But just what is happening when you press those buttons? Understanding how ATL works is important if you want to extend your ATL-based COM classes and servers much beyond what AppWizard and ClassView provide. For example, ATL provides support for advanced interface techniques like tear-off interfaces. Unfortunately, there's no Wizard option for implementing a tear-off interface. Even though ATL supports it, you've got to do a little work by hand. Understanding how ATL implements IUnknown is very helpful in this situation.

Let's start by looking at a minimal COM class. Using the Insert|New ATL Object menu to generate an ATL-based COM class yields the class in Listing One While this is ordinary vanilla C++ source code, it does differ from normal C++ source code for implementing a COM object in several ways. For example, while many COM class implementations derive strictly from COM interfaces, this COM class derives from several templates. In addition, this C++ class uses several odd-looking macros. As we examine the code, we'll look at ATL's implementation of IUnknown, as well as a few other interesting topics like a technique for managing vtable bloat and an uncommon use for templates. We'll begin by looking at the first symbol in the wizard-generated code ATL_NO_VTABLE.

Managing VTABLE Bloat

COM interfaces are easily expressed in C++ as pure abstract base classes. Writing COM classes using multiple inheritance (there are other ways to write COM classes) is merely a matter of adding the COM interface base classes to your inheritance list and implementing the union of all the functions. Of course, this means that the memory footprint of your COM class will include four bytes of vptr overhead for each interface implemented by your class. That's not a big deal if you have only a few interfaces and your C++ class hierarchy isn't that deep, but more interfaces and a deeper hierarchy will add overhead. ATL provides a way to cut down on some of this overhead. ATL defines the symbol #define ATL_NO_VTABLE __declspec(novtable).

Using ATL_NO_VTABLE prevents an object's vtable pointer (vptr) from being initialized in the constructor. That way the linker eliminates the vtable and all the functions pointed to by the vtable for that class. This can lower the size of your COM object somewhat, provided the most-derived class does not use the novtable declspec. You'll notice the size difference in classes with large derivation lists. There's one caveat: Calling virtual functions from the constructor of any object that uses this declspec is unsafe (because the vptr is uninitialized).

The next line in the class shows that CDefault derives from CComObjectRootEx. This is where we get to ATL's version of IUnknown.

ATL's IUnknown: CComObjectRootEx

While CComObjectRootEx isn't quite at the top of the ATL hierarchy, it's pretty close. The actual base class for COM objects in ATL is a class named CComObjectRootBase (both these classes are found in ATLCOM.H). Looking at CComObjectRootBase reveals the code you might expect for a C++-based COM class. CComObjectRootBase includes a DWORD member named m_dwRef for reference counting. You'll also see OuterAddRef, OuterRelease, and OuterQueryInterface to support COM aggregation and tear-off interfaces. Looking at CComObjectRootEx reveals InternalAddRef, InternalRelease, and InternalQueryInterface for performing the regular native reference counting and QueryInterface mechanisms for class instances with object identity.

Notice that Listing One shows CDefault derived from CComObjectRootEx and that CComObjectRoot is a parameterized template class. Take a moment to examine Listing Two, which shows the definition of CComObjectRootEx.

CComObjectRootEx is a template class that varies given the kind of threading model class passed in as the template parameter. In fact, ATL supports several threading models: Single Threaded Apartments (STAs), Multi Threaded Apartments (MTAs), and Free Threading. ATL includes three preprocessor symbols for selecting the various default threading models for your project.

Defining the preprocessor symbol _ATL_SINGLE_THREADED changes the default threading model to support only one STA-based thread. This option is useful for out-of-process servers that don't create any extra threads. Because the server supports only one thread, ATL's global state may remain unprotected by critical sections, so the server is more efficient. However, the downside is that your server may support only one thread. Defining _ATL_APARTMENT_THREADED for the preprocessor causes the default threading model to support multiple STA-based threads. This is useful for apartment model in-process servers (servers supporting the "ThreadingModel=Apartment" key). Because a server employing this threading model can support multiple threads, ATL protects its global state using critical sections. Finally, defining the _ATL_FREE_ THREADED preprocessor symbol creates servers compatible with any threading environment. That is, ATL protects its global state using critical sections and each object in the server will have its own critical sections to maintain data safety.

These preprocessor symbols merely determine which threading class to plug into CComObjectRootEx as a template parameter. ATL provides three threading model classes. The classes provide support for the most efficient yet thread-safe behavior for COM classes within each of the three aforementioned contexts. The three classes are CComMultiThreadModel, CComSingleThreadModel, and CComMultiThreadModelNoCS. Listing Three shows these three classes.

Notice that each of these classes exports two static functions, Increment and Decrement, and various aliases for critical sections.

CComMultiThreadModel and CComMultiThreadModelNoCS both implement Increment and Decrement using the thread safe Win32 InterlockedIncrement and InterlockedDecrement functions. CComSingleThreadModel implements Increment and Decrement using the more conventional ++ and --operators.

In addition to implementing incrementing and decrementing differently, the three threading models manage critical sections differently. ATL provides wrappers for two critical sections -- a CComCriticalSection (which is a plain wrapper around the Win32 critical section API) and CComAutoCriticalSection (same as CComCriticalSection, but it automatically initializes and cleans up critical sections). ATL also defines a fake critical section that has the same binary signature as the other critical section classes, but which doesn't do anything. As you can see from the class definitions, CComMultiThreadModel uses real critical sections, while CComMultiThreadModelNoCS and CComSingleThreadModel use the no-op fake critical sections.

So now the minimal ATL class definition makes a bit more sense. Take another look at Listings One and Two. CComObjectRootEx takes a thread model class whenever you define it. CDefault is defined using the CComSingleThreadModel class, so it uses the CComSingleThreadModel methods for incrementing and decrementing as well as the fake no-op critical sections. Thus CDefault uses the most efficient behavior, as it doesn't need to worry about protecting data. However, you're not stuck with that model. For example, if you want to make CDefault safe for any threading environment, simply redefine CDefault to derive from CComObjectRootEx using CComMultiThreadModel as the template parameter. AddRef and Release calls are automatically mapped to the correct Increment and Decrement functions.

So in ATL, the class you use when declaring CComObjectRootEx in your hierarchy list determines which version of AddRef and Release to use, as well as whether to use real or no-op critical sections for data protection. That takes care of IUnknown and reference counting in ATL. The next part of IUnknown to tackle is QueryInterface.

ATL and QueryInterface

It looks as though ATL took a cue from MFC for implementing QueryInterface. ATL uses a lookup table for implementing QueryInterface just like MFC's version. Take a look at the middle of CDefault's definition -- you'll see a construct based on macros called the "interface map." ATL's interface maps constitute its QueryInterface mechanism.

QueryInterface is used by clients to arbitrarily widen the connection to an object. That is, when a client needs a new interface, it calls QueryInterface through an existing interface. The object then looks at the name of the requested interface and compares that name to all the interfaces implemented by the object. If the object implements the interface, the object hands the interface back to the client. Otherwise, QueryInterface returns an error indicating no interface was found.

Traditional QueryInterface implementations usually consist of a long if-then statement. For example, a standard implementation of QueryInterface for a multiple-inheritance COM class might look like the one in Listing Four. As you'll see, ATL uses a lookup table instead of the conventional if-then statement.

ATL's lookup table begins with a macro named BEGIN_COM_MAP. Listing Five shows BEGIN_COM_MAP in the raw. Each class that uses ATL for implementing IUnknown specifies an interface map to provide to InternalQueryInterface. ATL's interface maps consist of structures containing interface ID (GUID)/DWORD/function pointer tuples. Listing Six shows the type named _ATL_INTMAP_ENTRY, which contains these tuples.

The first field is the interface ID (a GUID) and the second field indicates what action to take when the interface is queried. There are three ways to interpret the third member. If pFunc is equal to the constant _ATL_SIMPLEMAPENTRY (the value 1), then dw is an offset into the object. If pFunc is non-NULL but not equal to 1, then pFunc indicates a function to be called when the interface is queried. If pFunc is NULL, then the entry indicates the end of the QueryInterface lookup table.

Notice that CDefault uses COM_INTERFACE_ENTRY. This is the interface map entry for regular interfaces. Listing Seven is the raw macro. COM_INTERFACE_ENTRY fills the _ATL_INTMAP_ENTRY structure with the interface's GUID (notice the token pasting operator -- ##). In addition, notice how offsetofclass casts the this pointer to the right kind of interface and fills the dw member with that value. Finally, COM_INTERFACE_ENTRY fills the last field with _ATL_SIMPLEMAPENTRY to indicate that dw points to an offset into the class.

For example, the interface map for CDefault looks like Listing Eight after the preprocessor is done with it. If you look near the middle of Listing Five, you'll see that CComObjectRootEx's implementation of InternalQueryInterface uses the _GetEntries function as the second parameter. CComObjectRootEx::InternalQueryInterface uses a global ATL function named AtlInternalQueryInterface to look up the interface in the map. AtlInternalQueryInterface simply walks the map trying to find the interface.

In addition to COM_INTERFACE_ENTRY, ATL includes 16 other macros for implementing other composition techniques ranging from tear-off interfaces to COM aggregation.

Conclusion

ATL is handy because it does away with some of the drudgery of repeatedly implementing IUnknown. Just as MFC gives you a way out of writing the same GetMessage..DispatchMessage loop and gigantic switch statement, ATL provides all the COM boilerplate code you need to implement efficient and appropriately thread-safe COM classes

This month, we looked at ATL's implementation of IUnknown and saw that ATL uses templates to provide versions of AddRef and Release with varying degrees of thread safety. In addition, we saw that ATL uses a QueryInterface mechanism similar to the one MFC uses. However, ATL's lookup table is much more flexible than MFC's because you can compose your COM classes using a wide variety of techniques. In the coming months, we'll examine such topics as ATL's support for Class Objects (the classes that implement IClassFactory), ATL's tear off interfaces, and ATL and aggregation.


Listing One

class ATL_NO_VTABLE CDefault :    public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CDefault, &CLSID_Default>,
   public IDispatchImpl<IDefault, &IID_IDefault, &LIBID_ATLTESTLib>
{
public:
   CDefault() {
   }
DECLARE_REGISTRY_RESOURCEID(IDR_DEFAULT)
BEGIN_COM_MAP(CDefault)
   COM_INTERFACE_ENTRY(IDefault)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
// IDefault
public:
};

Back to Article

Listing Two

template <class ThreadModel>class CComObjectRootEx : public CComObjectRootBase
{
public:
   typedef ThreadModel _ThreadModel;
   typedef _ThreadModel::AutoCriticalSection _CritSec;


</p>
   ULONG InternalAddRef() {
      return _ThreadModel::Increment(&m_dwRef);
   }
   ULONG InternalRelease() {
      return _ThreadModel::Decrement(&m_dwRef);
   }
   void Lock() {m_critsec.Lock();}
   void Unlock() {m_critsec.Unlock();}
private:
   _CritSec m_critsec;
};

Back to Article

Listing Three

class CComMultiThreadModelNoCS{
public:
    static ULONG WINAPI Increment(LPLONG p) {return InterlockedIncrement(p);}
    static ULONG WINAPI Decrement(LPLONG p) {return InterlockedDecrement(p);}
    typedef CComFakeCriticalSection AutoCriticalSection;
    typedef CComFakeCriticalSection CriticalSection;
    typedef CComMultiThreadModelNoCS ThreadModelNoCS;
};
class CComMultiThreadModel
{
public:
    static ULONG WINAPI Increment(LPLONG p) {return InterlockedIncrement(p);}
    static ULONG WINAPI Decrement(LPLONG p) {return InterlockedDecrement(p);}
    typedef CComAutoCriticalSection AutoCriticalSection;
    typedef CComCriticalSection CriticalSection;
    typedef CComMultiThreadModelNoCS ThreadModelNoCS;
};
class CComSingleThreadModel
{
public:
    static ULONG WINAPI Increment(LPLONG p) {return ++(*p);}
    static ULONG WINAPI Decrement(LPLONG p) {return --(*p);}
    typedef CComFakeCriticalSection AutoCriticalSection;
    typedef CComFakeCriticalSection CriticalSection;
    typedef CComSingleThreadModel ThreadModelNoCS;
};

Back to Article

Listing Four

Class CDefault : public IDispatch,                        IDefault {
   HRESULT QueryInterface(RIID riid, 
                          void** ppv) {
      if(riid == IID_IDispatch)
         *ppv = (IDispatch*) this;
      else if(riid == IID_IDefault)
         *ppv = (IDefault*) this;
      else {
         *ppv = 0;
         return E_NOINTERFACE;
      }
      ((IUnknown*)(*ppv))->AddRef();
      return NOERROR;
   } 
   // AddRef, Release, and other functions
}; 

Back to Article

Listing Five

#define BEGIN_COM_MAP(x) public:   typedef x _ComMapClass; 
  static HRESULT WINAPI _Cache(void* pv,REFIID iid,void** ppvObject,DWORD dw){
     _ComMapClass* p = (_ComMapClass*)pv;
      p->Lock();
      HRESULT hRes = 
         CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);
      p->Unlock();
      return hRes;
   }
   IUnknown* GetUnknown() { 
      _ASSERTE(_GetEntries()[0].pFunc == _ATL_SIMPLEMAPENTRY); 
      return (IUnknown*)((int)this+_GetEntries()->dw); 
   }
   HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject) { 
      return InternalQueryInterface(this, _GetEntries(), iid, ppvObject); 
   } 
   const static _ATL_INTMAP_ENTRY* WINAPI _GetEntries() { 
      static const _ATL_INTMAP_ENTRY _entries[] = { 
                        DEBUG_QI_ENTRY(x)
   ...
   #define END_COM_MAP()   {NULL, 0, 0}};\
    return _entries;}

Back to Article

Listing Six

struct _ATL_INTMAP_ENTRY {   const IID* piid;       
   DWORD dw;
   _ATL_CREATORARGFUNC* pFunc; 
}

Back to Article

Listing Seven

#define offsetofclass(base,derived)((DWORD)(static_cast<base*>((derived*)8))-8)#define COM_INTERFACE_ENTRY(x)\
    {&IID_##x, \
    offsetofclass(x, _ComMapClass), \
    _ATL_SIMPLEMAPENTRY},

Back to Article

Listing Eight

const static _ATL_INTMAP_ENTRY* __stdcall _GetEntries() {   static const _ATL_INTMAP_ENTRY _entries[] = { 
    {&IID_IDefault, 
    ((DWORD)(static_cast<IDefault*>((_ComMapClass*)8))-8),
    ((_ATL_CREATORARGFUNC*)1)},
    {&IID_IDispatch, 
    ((DWORD)(static_cast<IDispatch*>((_ComMapClass*)8))-8), 
    ((_ATL_CREATORARGFUNC*)1)},
    {0, 0, 0}
  }; 
  return _entries;
}

Back to Article

DDJ


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.