Page #54 (Revisiting C Code)

< BACK  NEXT >
[oR]

Implementing COM Objects

Once the interfaces have been designed and a coclass has been declared, the next step is to programmatically implement the support for the interfaces. In this section, we will revisit the code developed in the earlier section, and examine various aspects of implementation in detail. We will continue to use C++ for implementing the code as it is a widely used programming language.

The reason for the popularity of C++ language in implementing a COM server is perhaps because supporting multiple interfaces in a C++ class can easily be achieved by using the language s support for multiple inheritance.

Using Multiple Inheritance

We know that a COM object frequently has to support multiple interfaces. C++ language support for multiple inheritance makes it convenient to support multiple interfaces on a class. All that is needed is to declare each interface as a base class of the implementation class, as shown below:

 class CVcr : public IVideo, public ISVideo  {   ...  }; 

graphics/01icon01.gif

There are many other ways to support multiple interfaces. Multiple inheritance is by far the most commonly used technique.


Recall that an interface contains nothing but pure virtual methods. A class cannot be instantiated if it contains pure virtual methods. Therefore, we will need to declare all the pure virtual methods from interfaces IVideo and ISVideo, and their base interface, IUnknown, as concrete methods on the class, as follows:

 class CVcr : public IVideo, public ISVideo  { public:  // IUnknown interface    STDMETHOD(QueryInterface)(REFIID iid, void** pp);    STDMETHOD_(ULONG, AddRef)();    STDMETHOD_(ULONG, Release)();    // IVideo interface    STDMETHOD(GetSignalValue)(long* plVal);    // ISVideo interface    STDMETHOD(GetSVideoSignalValue)(long* plVal);    ...  }; 

Let s see how we can implement these methods.

Implementing the Root Interface

A COM object is always required to support the IUnknown interface. To recap, this interface has two functionalities:

  • Manage the lifetime of the object using a reference-counting mechanism.

  • Lets a client navigate from one interface to another via the QueryInterface method.

The SDK does not provide a default implementation for IUnknown. Instead, in order to ensure trouble-free interaction within COM components, the COM specifications precisely define rules for reference counting as well as for querying an interface. Based on these rules, the object implementer is free to implement the three IUnknown methods using whatever logic that makes sense for the specific object. The rules should be followed strictly.

Reference Counting Rules

In dealing with interface pointers, the server and the client both have some responsibilities towards the lifetime of the object. Here are the rules that apply:

Rule #1: Reference counting does not apply to a NULL interface pointer.

As a matter of fact, any method call on a NULL interface pointer will typically result in a program crash with an access violation error message.

Rule #2: AddRef and Release typically go together.

For every call to AddRef, there should be a call to Release at some point later.

Rule #3: If an interface pointer is obtained, either via a COM API or an interface method, the callee should call AddRef before returning the interface and the caller should call Release once done with the interface pointer.

Consider our previous TV client example. We called a COM API, CoCreateInstance, that returned an ISVideo interface pointer. In this case, it can be assumed that a successful return from this API would have incremented the reference count up. It is now the responsibility of the TV client to release it once it is done with the interface.

 ISVideo* pSVideoSource1 = NULL;  ::CoCreateInstance(CLSID_VCR, NULL, CLSCTX_ALL,  IID_ISVideo,    reinterpret_cast<void**>(&pSVideoSource1));  ...  pSVideoSource1->Release(); 

Releasing an interface pointer informs the server that the caller is not interested in using the same pointer anymore. Making any method calls on such an interface pointer may result in unexpected behavior (the underlying object could already have been deleted).

graphics/01icon02.gif

It is a good idea to set the pointer to NULL once it is released.

 pSVideoSource1->Release(); pSVideoSource1 = NULL; 


Rule #4: If an interface pointer is copied, the reference count should be incremented by calling AddRef .

Consider, for example, the following line of code:

 ISVideo* pSVideoNew = pSVideoSource1; 

As a new copy of ISVideo interface pointer is made, a call to AddRef should be in order, as shown here:

 pSVideoNew->AddRef(); 

Remember that both the variables, pSVideoSource1 and pSVideoNew, need to be released at some point.

graphics/01icon01.gif

In a large project, it is easy to forget calling Release on an object or calling AddRef when copying an object. Essentially, any one of the above four reference counting rules can be broken easily. ATL provides a template class, CComPtr, to help eliminate such programming mistakes. This class wraps the actual interface pointer and ensures that the rules of reference counting are maintained. The programmer would not need to explicitly call AddRef or Release on the object. We will see how to use this class later in the chapter.


When copying an interface pointer, there are special cases when the redundant calls to AddRef and Release can be optimized away. This requires special knowledge about the relationship between two or more interface pointers. One such example is when we know that an interface pointer will last longer than its copy. Examine the following code fragment:

 void UseSVideo(ISVideo* pSVideo)  {   long val;    VRESULT vr;    for(int i=0; i<10; i++) {     vr = pSVideo->GetSVideoSignalValue(&val);      if (V_FAILED(vr)) {       ReportError(vr);        continue;      }      cout << "Round: " << i << " - Value: " << val << endl;    }  }  int main(int argc, char* argv[])  {   ...    ISVideo* pSVideoSource1 = NULL;    ::CoCreateInstance(CLSID_VCR, NULL, CLSCTX_ALL, IID_ISVideo,      reinterpret_cast<void**>(&pSVideoSource1));    ...    UseSVideo(pSVideoSource1);    ...    pSVideoSource1->Release();    ...  } 

When function UseSVideo is called, interface pointer pSVideoSource1 is copied to pSVideo. However, variable pSVideo will get destroyed before pSVideoSource1 is released. In this case, there is no need to call AddRef and Release on variable pSVideo.

In the above case, the special relationship between the two memory locations was very obvious. In general, finding such relationships is not easy. Spending time to find such relationships may not be worth it. The benefits of removing calls to AddRef and Release are fairly insignificant. Moreover, removing these calls with an assumed relationship is a debugging nightmare when the assumption turns out to be incorrect. Not to mention how poorly it reflects on your company when the customer finds a crash.

graphics/01icon02.gif

When in doubt, even slightly, use AddRef and Release on an interface pointer copy.


Reference Count Implementation

We have seen reference count implementation in Chapter 1. This is just a remake in COM-style programming.

The class that implements the interfaces maintains a member variable of data type long, as follows:

 class CMyImpl : public IInterface1, IInterface2, ...  { public:    ...  private:    ...    long m_lRefCount;  }; 

The count has to be initialized to zero in the object construction:

 CMyImpl:: CMyImpl()  {   ...    m_lRefCount = 0;  } 

Method AddRef increments the count:

 STDMETHODIMP_(ULONG) CMyImpl::AddRef()  {   return ++m_lRefCount;  } 

Method Release decrements the count. If the count reaches zero, there are no more outstanding references to the object, implying that the object is not needed anymore. A typical implementation deletes itself.

 STDMETHODIMP_(ULONG) CMyImpl::Release()  {   ULONG lRetVal =  m_lRefCount;    if (0 == lRetVal) {     delete this;    }    return lRetVal;  } 

This reference counting implementation is so generic that one can easily write a C++ template to implement this functionality. As we will see later, Microsoft s ATL does just this (and much more).

Relationship Between Interfaces

COM uses a standard technique for visually representing objects. Figure 3.1 shows the representation for our VCR class.

Figure 3.1. Representation of an object.
graphics/03fig01.gif

Figure 3.1 adheres to COM s philosophy of separating interfaces from implementation and hiding implementation details of the object. All that is known for an object is the potential list of interfaces the object exposes. For the VCR object, they are IVideo, ISVideo, and IUnknown.

IUnknown method QueryInterface (generally referred to as QI in the COM community) is the means by which a client can obtain one interface from another on the same object. However, COM places certain requirements on the relationship between all the interfaces an object exposes through QueryInterface. As we will see shortly, these requirements simplify the client s use of an object.

graphics/01icon01.gif

As every object is expected to implement interface IUnknown, QueryInterface for IID_IUnknown should never fail on an object.


Interfaces are Symmetric. In order to obtain a specific interface, a client should not be concerned with which interface to acquire first.

For example, in our TV-VCR code, in order to obtain the ISVideo interface pointer, the TV client should be able to acquire any of the three interfaces first and then QI its way to the ISVideo interface.

To facilitate this, COM mandates that QI should be symmetric. That is, if interface IVideo is obtained through ISVideo, then interface ISVideo can be obtained through interface IVideo. This symmetry can be represented as follows:

 If QI(IX) ? IY then  QI (QI(IX) ? IY) ? IX 

Interfaces are Transitive. A client should not be forced to obtain all interfaces of an object in a specific sequence.

In our TV-VCR sample, the client should not be forced to obtain IUnknown, IVideo, and ISVideo in a specific order. Instead, all interfaces should be treated as peers. If this were not the case, the TV client would have to code specific QI sequences to obtain a specific interface pointer.

To facilitate this, COM mandates that QI should be transitive. That is, if interface ISVideo can be obtained from IVideo, and IVideo can be obtained from IUnknown, then ISVideo can be obtained directly from IUnknown. This transitivity can be represented as follows:

 If QI(QI(IX) ? IY) ? IZ then  QI(IX) ? IZ 

Interfaces are Reflexive. A client should be able to obtain the same interface from an existing interface pointer.

In our TV-VCR sample, if the client has a pointer to ISVideo, he or she should be able to QI this pointer to obtain additional ISVideo pointers.

To understand why this is important, consider the following code fragment:

 extern void UseSignal(IUnknown* pUnk);  int main()  {   ...    ISVideo* pSVideo = ObtainSVideoInterface();    UseSignal(pSVideo);    ...  } 

When a strongly typed interface (such as ISVideo) is passed as its base type parameter (such as IUnknown), the called function (UseSignal) loses information about the original type forever. It would be counterintuitive if the original type information could not be regained inside the function, as shown here:

 void UseSignal(IUnknown* pUnk)  {   ISVideo* pSVideo;    hr = pUnk->QueryInterface(IID_ISVideo, (void**)  &pSVideo);    ...  } 

To facilitate this, COM mandates that a QI request through an interface pointer must always succeed if the requested type matches the type of pointer to make the request. This can be represented as:

 QI(IX) ? IX 

Consistent Set of Interfaces. The set of interfaces supported by an object should not change during the lifetime of the object. If QI returns S_OK for interface IX, it must always return S_OK for interface IX during the lifetime of the object. If QI returns E_NOINTERFACE for interface IX, it must always return E_NOINTERFACE during the lifetime of the object. Recall that the lifetime of the object is governed by the number of outstanding references on the object. The object is alive as long as there is at least one outstanding interface pointer on the object.

If this consistency restriction is not maintained, one of the earlier specified rules will break down.

graphics/01icon01.gif

The consistency restriction is placed on the object (the instance of the class) and not on the class itself. Two different objects of the same class can individually use some state or temporal information to decide the set of interfaces each will support during its lifetime.

Also note that the supported set of interfaces is fixed only for the lifetime of the object, not forever. It is legal, for example, for the implementer to support additional interfaces in a new version of the COM server.


Unique Object Identity. A client may wish to determine if two interface pointers point to the same object.

To facilitate this, COM mandates that a QI for IUnknown should return the same pointer value for each request. A client can then determine if two interface pointers point to the same object identity, as shown in the following code fragment:

 bool IsItTheSameObject(ISVideo* pSVideo, IVideo* pVideo)  {   IUnknown* pUnk1 = NULL;    HRESULT hr = pSVideo->QueryInterface(IID_IUnknown,      (void**) &pUnk1);    _ASSERT (SUCCEEDED(hr));    IUnknown* pUnk2 = NULL;    hr = pVideo->QueryInterface(IID_IUnknown, (void**) &pUnk2);    _ASSERT (SUCCEEDED(hr));    bool bRetVal = (pUnk1 == pUnk2);    pUnk1->Release(); pUnk2->Release();    return bRetVal;  } 

The notion of object identity is a fundamental concept that is used by the COM remoting architecture to efficiently represent interface pointers on the network.

Now that we understand all the requirements placed on a QI implementation, let s reexamine our own implementation.

Typical QI Implementation

The following is the QI implementation for the TV-VCR sample:

 STDMETHODIMP CVcr::QueryInterface(REFIID iid, void** ppRetVal)  {   *ppRetVal = NULL;    if (IsEqualIID(iid, IID_IUnknown)) {     *ppRetVal = static_cast<IVideo*>(this);    }else    if (IsEqualIID(iid, IID_IVideo)) {     *ppRetVal = static_cast<IVideo*>(this);    }else    if (IsEqualIID(iid, IID_ISVideo)) {     *ppRetVal = static_cast<ISVideo*>(this);    }    if (NULL != (*ppRetVal)) {     AddRef();      return S_OK;    }    return E_NOINTERFACE;  } 

For each interface that is supported, the this pointer is statically cast to the interface type and returned.

Returning interface IUnknown is a special case. Simply static-casting the object to IUnknown, as shown below, will result in a compiler error:

 *ppRetVal = static_cast<IUnknown*>(this); 

The problem is that there are two paths to IUnknown in the vtbl layout, as both the interfaces, IVideo and ISVideo, are derived from IUnknown. With a static-cast such as the one above, the compiler does not know which path to choose. We can cast this " to IVideo specifically and help the compiler to pick one path.

graphics/01icon01.gif

Typecasting this to ISVideo would have worked as well, as the implementation of IUnknown is the same for both IVideo and ISVideo. However, what should not be done is typecasting to IVideo at one time and to ISVideo at another time during the lifetime of the object, as it breaks the object identity rule.


In order to isolate the logic of converting this to IUnknown, it is a good idea to define a method on the class to return the IUnknown pointer:

 IUnknown* CVcr::GetUnknown()  {   return static_cast<IVideo*>(this);  } 

Let s recap what we learned in this section.

A COM object has to follow certain rules for counting the references as well as maintaining the relationship between the interfaces it exposes. We discussed how to implement IUnknown methods that would obey these rules.

It is left as an exercise to the reader to prove that the DLL server that was implemented in the earlier section follows these rules.

The code that we wrote to implement QueryInterface can easily be converted to a template so that it can be used with any class implementation. This is what ATL does. Let s see how.

ATL is Our Friend

If you examine the code that we have developed so far, you will see that there is a lot of grunge work that you have to do in order to implement a COM object:

  • For every COM object, you have to implement IUnknown logic reference-counting as well as support for querying an interface.

  • For every COM object, you have to implement a class object (that itself is a COM object). You typically will have to support an IClassFactory interface for your class object.

  • You need to keep track of every COM object that has an outstanding reference using a global counter.

  • You have to implement the registration logic for the COM server.

All this work may very well defocus you from your main implementation logic.

With a little more thought, one can isolate this grunge work into a separate piece of code, then reuse this code for the next COM object or COM server to be implemented.

It turns out that ATL already does this for you. ATL is a set of template-based C++ classes with which you can easily create COM objects. The code has been written with efficiency and compactness in mind.

Let s see how we can leverage ATL in our code.

Standard Include Files

The core ATL logic is defined in two header files, atlbase.h and atlcom.h. File atlcom.h relies on a global variable, _Module, to be defined by the developer. This variable should be of type ATL class CComModule or its deriv-ative. As you may have guessed, class CComModule is the replacement for our earlier defined class CMyModule, and variable _Module is the replacement for our defined variable g_MyModule.

The standard header file for a project should contain the above two header files and the global variable _Module declared as external:

 // File StdAfx.h  #pragma once  #define WIN32_LEAN_AND_MEAN  #define _ATL_MIN_CRT  #define _USRDLL  #include <atlbase.h>  extern CComModule _Module;  #include <atlcom.h> 

The macro _ATL_MIN_CRT instructs ATL to minimize the use of the C run-time library. If this macro is defined, ATL defines its run-time functions such as malloc, free, operator new, and operator delete. It even defines run-time startup code _DLLMainCRTStartup for in-process servers and WinMainCRTStartup for out-of-process servers. [6] By default, ATL assumes the code being developed is for out-of-process servers. Macro _USRDLL instructs ATL to use _DLLMainCRTStartup as the startup.

[6] Out-of-process COM servers are supported as legacy components under COM+.

Though most of the code provided by ATL is template-based and is defined in header files, some supplementary code is defined in source files. One such source file is atlimpl.cpp. This file needs to be referenced just once in the project, typically done in a standard source file, StdAfx.cpp:

 // File StdAfx.cpp (source file that includes just the  //        standard includes)  #include "StdAfx.h"  #include <atlimpl.cpp> 
Registration Logic

Instead of hard-coding the registration information, as we did earlier, ATL requires that this information be moved to a text file in a script format that ATL understands. This file typically has .rgs extension. The following listing shows the registration script for our COM object:

 HKCR  {   NoRemove CLSID    {     ForceRemove {318B4AD3-06A7-11d3-9B58-0080C8E11F14}      {       InprocServer32 = s '%MODULE%'      }    }  } 

You should consult the ATL documentation for the format of the script. You should also read Craig McQueen s Using the ATL Registry Component [Mcq-98] for a good introduction. In the above text fragment, we are indicating that the InprocServer32 entry be added under the key HKEY_CLASSES_ROOT\CLSID\{OurCLSID}. The keyword ForceRemove indicates that the specified subkey should be deleted and recreated before adding any entries. The keyword NoRemove indicates that the entries should be added to the specified subkey without actually deleting the subkey first.

The definition is typically added as a resource into the resource file associated with the project, as follows:

 // File Resource.h  #define IDR_VCR 100  // File MyVcr.rc  #include "Resource.h"  IDR_VCR  REGISTRY DISCARDABLE  "Vcr.rgs" 

For more information on using resource files, check the SDK documentation.

A remaining piece of logic is to associate the resource IDR_VCR with the implementation class CVcr. Using macro DECLARE_REGISTRY_RESOURCEID does the trick, as shown here:

 // File vcr.h  #include "Resource.h"  ...  class CVcr :    ...  { public:    ...  DECLARE_REGISTRY_RESOURCEID(IDR_VCR)    ...  }; 

Let s revise our CVcr class to support interfaces IVideo and ISVideo using ATL.

Implementation Class Logic

ATL breaks the implementation for IUnknown into two distinct code pieces:

  • An internal piece that implements the core IUnknown logic: the methods are called InternalQueryInterface, InternalAddRef, and InternalRelease.

  • A wrapper code that just turns around and calls the internal code.

The internal piece is implemented in a class called CComObjectRoot. Any ATL-based COM implementation class needs to be derived from this class (or its variants such as CComObjectRootEx), as shown here:

 // File vcr.h  #include "Resource.h"  #include "Video.h"  class CVcr :    ...    public CComObjectRoot,    ...  {   ...  }; 

Finally, method InternalQueryInterface needs to be informed of the list of interfaces the class supports. The list is sandwiched between BEGIN_COM_MAP and END_COM_MAP macros. Each interface that is supported is specified using the COM_INTERFACE_ENTRY macro, as shown below:

 class CVcr :    public IVideo,    public ISVideo,    public CComObjectRoot,    ...  { public:    ...  BEGIN_COM_MAP(CVcr)    COM_INTERFACE_ENTRY(IVideo)    COM_INTERFACE_ENTRY(ISVideo)  END_COM_MAP()    ...  }; 

Note that interface IUnknown is implicitly assumed to be a supported interface and hence should not be specified in the interface map.

Class Object Support

Each implementation class has to be tied to a class object that will let us create an instance of the implementation class. If you recall, our old implementation defined a class CVcrClassObject (that supported method CreateInstance to create instances) for this purpose. ATL pushes instance creation logic to the implementation class itself. To use ATL s mechanism, each implementation class is required to have the class CComCoClass in its base class list. This class contains the instance creation logic.

The following code shows class CVcr revised to use ATL:

 // File vcr.h  #include "Resource.h"  #include "Video.h"  class CVcr :    public IVideo,    public ISVideo,    public CComObjectRoot,    public CComCoClass<CVcr, &CLSID_VCR>  { public:    CVcr();    ~CVcr();  DECLARE_REGISTRY_RESOURCEID(IDR_VCR)  BEGIN_COM_MAP(CVcr)    COM_INTERFACE_ENTRY(IVideo)    COM_INTERFACE_ENTRY(ISVideo)  END_COM_MAP()    // IVideo interface    STDMETHOD(GetSignalValue)(long* plVal);    // ISVideo interface    STDMETHOD(GetSVideoSignalValue)(long* plVal);  private:    long m_lCurValue;    int m_nCurCount;  }; 

Note that CVcr is still an abstract class in that it does not define IUnknown methods. Therefore, the following line of code will fail to compile:

 CVcr* pVcr = new CVcr; 

ATL defines the IUnknown methods in a template called CComObject. Not only does this class implement the logic of the IUnknown interface, but it also helps in keeping track of the number of outstanding objects (via the Lock and Unlock methods). Using this template, an instantiable class of CVcr can be declared as CComObject<CVcr>, as illustrated in the following line of code:

 CComObject<CVcr>* pVcr = new CComObject<CVcr>; 

The above line of code will not fail during compilation.

Of course, ATL suggests that the object be created in a slightly different way:

 CComObject<CVcr>* pVcr = CComObject<CVcr>::CreateInstance(); 

Why the second form of coding is better than the first one is left as an exercise for the readers (hint: check the ATL documentation for the FinalConstruct method).

Just one more bit left knowing that there could be many implementation classes in a COM server, ATL requires the classes be specified in a list so that when DllGetClassObject is called, ATL can search for the requested class object in the list.

Such a list has to be specified as a sandwich between BEGIN_OBJECT_MAP and END_OBJECT_MAP macros. Each coclass that is being exposed should be defined using the OBJECT_ENTRY macro, as shown below, for our VCR class:

 // File VcrDll.cpp  #include "StdAfx.h"  #include "Video.h"  #include "Video_i.c"  #include "Vcr.h"  BEGIN_OBJECT_MAP(ObjectMap)    OBJECT_ENTRY(CLSID_VCR, CVcr)  END_OBJECT_MAP() 

The map declared here should be passed as a parameter to the initialization method of the global instance, _Module, as shown here:

 // DLL Entry Point  extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance,    DWORD dwReason, LPVOID /*lpReserved*/)  {   if (DLL_PROCESS_ATTACH == dwReason) {     _Module.Init(ObjectMap, hInstance, NULL);    }else    if (DLL_PROCESS_DETACH == dwReason) {     _Module.Term();    }    return TRUE;  // ok  } 

That s it. By using ATL, we got rid of our class factory implementation code, our registration code, and our IUnknown implementation code without sacrificing any efficiency or flexibility. The revised code is much easier to read and maintain.

Is there ever a reason not to use ATL? There is no reason, unless you like inflicting pain on yourself and other team members. That said, however, it is important to understand the fundamentals of COM programming and what goes on under the hood of ATL. This chapter is here to explain the fun-damentals to you and will continue to use non-ATL code. However, I will demonstrate the use of ATL whenever appropriate.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

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