The De Facto COM Interface: IUnknown

 < Free Open Study > 



So far in this chapter, we have thrown around the terms IUnknown, COM object, and COM class quite liberally, without providing clear definitions. Let's fix these omissions right now:

  • A COM class is the definition of a UDT implementing at least IUnknown. Often termed a coclass.

  • A COM object is an instance of a coclass.

  • IUnknown is the standard COM interface from which all others ultimately derive. Thus, the first three members of any COM interface will be the three inherited methods of IUnknown. Here is a streamlined version, which is fully defined in <unknwn.h>:

    // The core COM interface. interface IUnknown {      virtual HRESULT QueryInterface(REFIID riid, void** ppv) = 0;      virtual ULONG AddRef() = 0;      virtual ULONG Release() = 0; }; 

The actual definition of IUnknown is wrapped in a number of COM macros, allowing for the correct expansion on various platforms (also found in <unknwn.h> is the C language equivalent of IUnknown). We will examine these COM macros later, but will ignore them for now so they don't cloud the fact that IUnknown is just like any other interface seen thus far: a collection of pure virtual functions.

IUnknown provides two services to every COM object. First, QueryInterface() allows a client to gain a pointer to any interface implemented on the object (including IUnknown itself) from another valid interface pointer. Second, the AddRef() and Release() methods provide a mechanism to control the lifetime management of the object. We will drill down into the details of these methods momentarily.

When creating COM classes in C++, you may support COM interfaces using either MI or nested classes. In this respect, things seem much like our C3DRect class of Chapter 2. In COM, however, we also must account for IUnknown. Thus, if we derive our custom IDraw interface from IUnknown, and implement IDraw in a coclass named CoHexagon, we would need to implement a total of four methods:

// IDraw derives from IUnknown, // therefore CoHexagon must implement four methods. class CoHexagon : public IDraw { public:      // Constructor and destructor...      // IUnknown.      HRESULT QueryInterface(REFIID riid, void** ppv);      ULONG AddRef();      ULONG Release();      // IDraw.      void Draw(); };

COM Object Lifetime Management Using AddRef() and Release()

In COM programming, the client and coclass share the burden of the object's lifetime management. Every COM object maintains an internal reference count, representing the number of outstanding interface pointers currently in use. When this reference count drops to exactly zero, the object deallocates itself from memory. This joint venture of the object's lifetime management is critical, as a single object may be used by numerous clients at a single time. If client A is finished using a given object, and were to directly delete the COM object from memory, that would more than likely deeply offend the other active clients. This possibility is avoided altogether in COM by the use of an object's reference counter. The AddRef() and Release() methods of IUnknown are the two methods necessary to control the memory allocation of a COM object.

So when are AddRef() and Release() called? When a client asks for a given interface pointer through QueryInterface(), the COM object will call AddRef() if and only if it implements the interface in question. When a client is finished using that interface pointer, the client must call Release() to inform the object it has one less client. Implementing AddRef() and Release() is very simple, and extremely boilerplate. If you are not worried about thread safety, just increment or decrement the private reference counter (which of course you should set to zero in the class constructor):

// AddRef() is used to increase the internal reference count of the object. ULONG CoHexagon::AddRef()      {      return ++m_refCount;          // Private unsigned long defined in CoHexagon. }

In the implementation of Release(), check the reference counter for the final release, and delete the object:

// Release() is called to decrement the internal reference count on the object. // If (and only if) the final reference to the object has been released, the coclass // removes itself from memory. ULONG CoHexagon::Release() {      --m_refCount;      // Did I lose my final connection?      if(m_refCount == 0)                {           // Final Release! Remove coclass from memory.           delete this;           return 0;      }      return m_refCount; }

Notice that both AddRef() and Release() return the current number of outstanding interface pointers to the client. Never use this value for any purpose other than general debugging. The COM specification does not state that this returned reference count is a perfect reflection of the object's number of clients. Although a client can examine this return value to get a general feel of the object in use, it should never use this value in production code.

If you are concerned with a thread-safe implementation of AddRef() and Release(), you should make calls to the Win32 functions InterlockedIncrement() and InterlockedDecre- ment(), rather than the C++ increment and decrement operators. Here is a thread-safe implementation of AddRef() and Release():

// The increment operator (++) is not thread safe. // InterlockedIncrement() will safely increment a variable by one. ULONG CoHexagon::AddRef()      {      return InterlockedIncrement(&m_refCount); } ULONG CoHexagon::Release() {      InterlockedDecrement(&m_refCount);      // Did I lose my final connection?      if(m_refCount == 0)                {           // Final Release! Remove coclass from memory.           delete this;           return 0;      }      return m_refCount; }

We have much more to say about thread safety and your COM objects in Chapter 7. The point here is to understand that AddRef() and Release() are the two methods of IUnknown that control the lifetime management of each and every COM object.

Understanding AddRef() and Release() Rules

Now that we understand the functionality of AddRef() and Release(), we need to understand the exact circumstances in which to call them. Proper reference counting can be broken down into the following small set of rules:

  • A COM object calls AddRef() on itself whenever it successfully hands out an interface pointer to the client, typically during a QueryInterface() invocation. We will see this rule in action in the next topic section.

  • When a client has successfully received an interface pointer (through a COM library call or some interface method), the client must assume that AddRef() has been called by the object, and must call Release() when finished with the acquired pointer:

    // Assume the following method returns an interface pointer. IDraw* pDraw = NULL; if (GoGetIDraw(&pDraw))   // Success? The object has been AddRef-ed. {      pDraw -> Draw();      pDraw -> Release();  // If this is the only reference, the object is now                           // destroyed. }

  • If a client (or server for that matter) makes a copy of an interface pointer, the client should call AddRef() on that copy explicitly:

    // Assume we have fetched some interfaces and now set one equal to the other. pIFaceTwo = pIFaceOne; pIFaceTwo -> AddRef(); 

  • Functions that take interface pointers as parameters should AddRef() and Release() the interface during the method invocation:

    // This method draws any IDraw compatible object. HRESULT DrawMe(IDraw* pDrawingObject) {      pDrawingObject -> AddRef();          // We are using this object.      pDrawingObject -> Draw();      pDrawingObject -> Release();          // All done. }

At times, these calls may seem redundant, although technically correct. You can minimize AddRef() and Release() calls under a few circumstances; however, with these rules fixed in your head, you can be assured objects are deleted from memory as efficiently as possible. It is far better to add in explicit AddRef() and Release() calls (even if things seem a bit redundant) rather than have objects hanging around in memory longer than necessary.

In COM, the objects we activate with COM library calls are responsible for removing themselves from memory when they are no longer wanted. This is in contrast to our Shapes lab from the previous chapter where we indirectly deallocated our C3DRect using a custom API call (DestroyThe3DRect()). This is not the case in COM. As long as you abide by the AddRef() and Release() rules, you can rest assured that your objects take care of themselves.

Handing Out Interface Pointers with QueryInterface()

The final method of IUnknown, QueryInterface(), provides the functionality that allows a client to request an interface pointer from the object from an existing interface pointer. Recall the custom Rect API method, GetInterfaceFrom3DRect(). If the object supported the requested INTERFACEID, we received (through the void** parameter) a pointer to the given interface:

// Our custom rectangle API function mimicked QueryInterface() but // this method was global, and was not tightly bound to the object it operated on. bool GetInterfaceFrom3DRect(INTERFACEID iid, void** iFacePtr) {      ...      if(iid == IDRAW)          // They want access to IDraw.      {           *iFacePtr = (IDraw*)theRect;           return true;      }      ... }

A standard implementation of QueryInterface() mimics our hand-rolled API almost exactly. Rather than sending in an arbitrary numerical identifier, we send in the GUID of the interface we are requesting by reference. As the object itself is performing the cast, we make use of the this pointer rather than a named object (such as our global C3DRect). Finally, rather than returning a simple Boolean, we return the de facto HRESULT return type. Here is the implementation of QueryInterface() for CoHexagon (take note of the AddRef() call, if the object supports the requested interface):

// QueryInterface() is implemented by each and every coclass, and provides // a way to return an interface pointer to the client. HRESULT CoHexagon::QueryInterface(REFIID riid, void** ppv) {      // Which interface does the client want?      if(riid == IID_IUnknown)          // Human readable version of GUID           *ppv = (IUnknown*)this;           else if(riid == IID_IDraw)           *ppv = (IDraw*)this;         // Generalized "*ppv = (IDraw*)theRect"      if(*ppv)                          // Did we have the IID?      {           // Must call AddRef() if we hand out an interface.           ((IUnknown*)(*ppv))->AddRef();           return S_OK;      }      *ppv = NULL;           // NULL pointer if we do not have the interface.      return E_NOINTERFACE;  // Standard return if we don't support the IID. } 

Note 

Recall every standard interface already has a GUID assigned to it. As well, every standard interface has a human readable constant, using the IID_ prefix convention (for example, IID_IUnknown, IID_IMalloc, IID_IClassFactory, and so on).

Implementing QueryInterface(), like maintaining an object reference count, is also a very mechanical process. Basically, each interface supported on the object requires an additional two lines of code:

// Each interface supported by a COM object entails these two lines of code. if(riid == interface ID)                    // If you want this...      *ppv = (interface name*)this;          //...you get this.

When implementing QI, remember the basics of reference counting: If an object hands off an interface pointer to the client, the object must AddRef() itself. Every implementation of QI will have code the equivalent of the following after all IIDs have been checked (if we do not support the IID, return E_NOINTERFACE):

// Be sure to AddRef() the object if you have found the requested interface. if(*ppv) {      ((IUnknown*)(*ppv))->AddRef();     // We have another active client.      return S_OK;          // Informs the client we supported the requested IID. }

Implementing QueryInterface() Using static_cast<>

As an alternative to traditional C++ casting, you may find other implementations of QueryInterface() making use of static_cast<>. One advantage of this approach is that compile-time errors will be generated if an illegal cast is attempted. Functionally, however, each approach yields the same result: returning a portion of the object to the interested client (i.e., casting the this pointer).

If we were to rewrite CoHexagon's QueryInterface() implementation using static_cast<>, we would see the following:

// QueryInterface() à la static_cast<> HRESULT CoHexagon::QueryInterface(REFIID riid, void** ppv) {      if(riid == IID_IUnknown)           *ppv = static_cast<IUnknown*>(this);      else if(riid == IID_IDraw)           *ppv = static_cast<IDraw*>(this);       if(*ppv)           {           ((IUnknown*)(*ppv))->AddRef();           return S_OK;      }      *ppv = NULL;      return E_NOINTERFACE; } 

IUnknown Establishes an Object's Identity

You have seen that this single COM interface is responsible for the basic riggings of a COM class: object lifetime and handing out interface pointers. QueryInterface() has been defined by the COM specification with the following requirement in mind:

If a client has some interface pointer, the client must be able to navigate to any other interface on the object from that pointer.

In other words, QueryInterface() must ensure the rules of reflexivity, symmetry, and transitivity. This breaks down to the following set of guidelines:

  • Reflexivity: If a client has an interface pointer, pA, a client must be able to query pA for itself.

  • Symmetry:  If a client can navigate from an interface pointer, pA, to another interface pointer, pB, the client must be able to navigate from pB back to pA.

  • Transitivity:  If a client can navigate from pA to pB, and from pB to pC, the client must be able to return from pC to pA.

What these rules mean to a client is simple: "From a valid interface pointer I can get to any other interface on the object."

In order to ensure your coclasses support each of these rules, be sure each custom interface derives from an IUnknown-derived interface, and in the implementation of QueryInterface() include a provision for each and every interface on the object. Be extra careful to ensure that if your coclass supports a versioned interface (IDraw, IDraw2), a provision for each version is supported in your QueryInterface() logic, not a single test for the most derived interface.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net