Page #52 (Chapter 3. Components)

< BACK  NEXT >
[oR]

From C++ to COM A Quick Tour

In Chapter 1, we defined non-COM interfaces IVideo and ISVideo using C++. In Chapter 2, we turned them into COM-style interfaces. Now, we will use the non-COM C++ implementation from Chapter 1 and turn it to COM-based C++ code. As you will see, programming in COM is not that difficult. After all, we did lay the foundation of COM in Chapter 1.

Implementing Interfaces

Let s begin by converting the definition of class CVcr from our non-COM implementation. The following code fragment shows the class as defined at the end of Chapter 1:

 // File vcr.h  #include "Video.h"  class CVcr : public IVideo, public ISVideo  { public:    CVcr(void);    ~CVcr();    // IGeneral interface    VRESULT _stdcall Probe(char* pszType, IGeneral** ppRetVal);    void _stdcall AddReference();    void _stdcall Delete();    // IVideo interface    VRESULT _stdcall GetSignalValue(long* pRetVal);    // ISVideo interface    VRESULT _stdcall GetSVideoSignalValue(long* pRetVal);  private:    long m_lCurValue;    int m_nCurCount;    long m_lRefCount;  }; 

To COMify this class, we need to do the following:

  • convert methods to use COM-style return values and calling conventions

  • replace IGeneral method declarations with corresponding IUnknown method declarations.

Use COM Calling Conventions

To use COM-style calling conventions, we can use COM SDK-defined macros that were discussed in the previous chapter. Following are two simple rules to help you with the macro usage:

Rule 1: If the interface method returns an HRESULT (as almost all methods do), use the STDMETHOD(FuncName) macro.

For example, consider the following IVideo method:

 HRESULT GetSignalValue([out, retval] long* val); 

This interface method will have the following C++ style method declaration:

 STDMETHOD(GetSignalValue)(long* pRetVal); 

Rule 2: If the interface method returns a type other than HRESULT, use the

 STDMETHOD_(Type, FuncName) macro. 

For example, consider the AddRef method for interface IUnknown:

 ULONG AddRef(); 

This interface method will have the following C++ style method declaration:

 STDMETHOD_(ULONG, AddRef)(); 
Declare IUnknown Methods

We will replace the IGeneral methods with the corresponding IUnknown methods. The equivalent methods for Probe, AddReference, and Delete are QueryInterface, AddRef, and Release, respectively.

 STDMETHOD(QueryInterface)(REFIID iid, void** pp);  STDMETHOD_(ULONG, AddRef)();  STDMETHOD_(ULONG, Release)(); 

After making the necessary changes, the following is the revised C++ class definition:

 // File vcr.h  #include "Video.h"  class CVcr : public IVideo, public ISVideo  { public:    CVcr(void);    ~CVcr();    // 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);  private:    long m_lCurValue;    int m_nCurCount;    long m_lRefCount;  }; 

graphics/01icon01.gif

Even though the IDL interface definition does not allow multiple inheritance, there is no restriction on the implementation to support it, as can be seen in the above code.


Method Implementation

Let s now turn the non-COM implementation of C++ class methods from Chapter 1 into a COM-based C++ implementation.

To indicate COM-style implementation for a method, we use STDMETHODIMP macros discussed in Chapter 2. The usage rules are similar to those for STDMETHOD.

Also, as the return values are of type HRESULT now, we need to change our return code definitions to HRESULTs. The SDK defines many standard HRESULT values, along with their explanation, in the header file winerror.h. The ones that are currently of interest to us are S_OK and E_UNEXPECTED. The value S_OK indicates that the function call succeeded. Value E_UNEXPECTED indicates a failure in the code. For now, I will use E_UNEXPECTED to also indicate an internal error. Later in the chapter, I will show you how to use custom HRESULTs.

Except for the method QueryInterface, converting the rest of the methods is fairly straightforward. The following code shows the final implementation of these methods:

 // File vcr.cpp  #include "Vcr.h"  CVcr:: CVcr()  {   m_lCurValue = 5;    m_nCurCount = 0;    m_lRefCount = 0;  }  CVcr::~CVcr()  { }  STDMETHODIMP_(ULONG) CVcr::AddRef()  {   return (++m_lRefCount);  }  STDMETHODIMP_(ULONG) CVcr::Release()  {   ULONG lRetVal = ( m_lRefCount);    if (0 == lRetVal) {     delete this;    }    return lRetVal;  }  STDMETHODIMP CVcr::GetSignalValue(long* pRetVal)  {   if (m_nCurCount >= 5 || m_nCurCount < 0) {     return E_UNEXPECTED;    }    m_nCurCount++;    if (5 == m_nCurCount ) {     m_lCurValue = 5;      m_nCurCount = 1;    }    long lReturnValue = m_lCurValue;    m_lCurValue += 10;    *pRetVal = lReturnValue;    return S_OK;  }  STDMETHODIMP CVcr::GetSVideoSignalValue(long* pRetVal)  {   if (m_nCurCount >= 5 || m_nCurCount < 0) {     return E_UNEXPECTED;    }    m_nCurCount++;    if (5 == m_nCurCount ) {     m_lCurValue = 5;      m_nCurCount = 1;    }    long lReturnValue = m_lCurValue;    m_lCurValue += 20;    *pRetVal = lReturnValue;    return S_OK;  } 

If you compare this code to the one in Chapter 1, the only difference you will see is that of method signatures and return values. The rest of the code is identical.

The last method to be converted is Probe. The following code fragment shows the non-COM implementation as it appeared in Chapter 1:

 VRESULT CVcr::Probe(char* pszType, IGeneral** ppRetVal)  {   *ppRetVal = NULL;    if (!stricmp(pszType, "general")) {     *ppRetVal = static_cast<IVideo*>(this);    }else    if (!stricmp(pszType, "video")) {     *ppRetVal = static_cast<IVideo*>(this);    }else    if (!stricmp(pszType, "svideo")) {     *ppRetVal = static_cast<ISVideo*>(this);    }    if (NULL != (*ppRetVal)) {     AddReference();      return V_OK;    }    return V_NOINTERFACE;  } 

Converting this method to the IUnknown method, QueryInterface, requires a few changes:

  • The string comparison stricmp has to be replaced by interface ID comparison. The SDK provides an API, IsEqualIID, to compare two GUIDs.

  • If the requested interface is not found, the return value has to be indicated as E_NOINTERFACE, another standard HRESULT value defined in winerror.h.

The revised implementation is shown below:

 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;  } 

We just finished implementing the class that supports the interfaces. What remains to be done is adding a mechanism that would let the client instantiate this class and obtain the requested interface pointer.

Instantiation Logic

If you recall from Chapter 1, we defined a function, CreateVCR, that enabled the client to obtain an instance of CVcr. The following is the code fragment taken from Chapter 1:

 VRESULT _stdcall CreateVCR(char* pszType, IGeneral** ppRetVal)  {   *ppRetVal = NULL;    CVcr* pVcr = new CVcr;    if (NULL == pVcr) {     return V_OUTOFMEMORY;    }    VRESULT vr = pVcr->Probe(pszType, ppRetVal);    if (V_FAILED(vr)) {     delete pVcr;    }    return vr;  } 

In order to make this function available outside the DLL, recall that it was declared as an export function in the module definition file, vcr.def:

 ; File VCR.DEF  LIBRARY   VCR.DLL  EXPORTS    CreateVCR 

COM employs a similar technique to let the client access an object from the server. In its case, however, it mandates that the export function be called DllGetClassObject. Following is its prototype:

 STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv); 

The second and third parameters correspond to the two parameters we used for CreateVCR parameter riid specifies the interface that the client is interested in, and the interface pointer is returned in parameter ppv (as with IUnknown::QueryInterface, we are still stuck with LPVOID* as return type for legacy reasons). What s the first parameter for? As we will see later, a DLL can host multiple classes. The first parameter represents the CLSID of the class the client is interested in. For our implementation, this CLSID should match the GUID specified in the coclass definition of the IDL file Video.idl. If there is no match, then the implementation needs to return another standard HRESULT error code CLASS_E_CLASSNOTAVAILABLE to the client.

If the instance creation fails because of limited memory resources, COM requires that another standard HRESULT error code, E_OUTOFMEMORY, be used as a return value.

Here is our new implementation:

 STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid,    LPVOID* ppv)  {   if (!IsEqualIID(rclsid, CLSID_VCR)) {     return CLASS_E_CLASSNOTAVAILABLE;    }    CVcr* pVCR = new CVcr;    if (NULL == pVCR) {     return E_OUTOFMEMORY;    }    HRESULT hr = pVCR->QueryInterface(riid, ppv);    if (FAILED(hr)) {     delete pVCR;    }    return hr;  } 

graphics/01icon01.gif

In the above implementation, each call to DllGetClassObject returns a new instance. Though this is considered illegal under COM, let s ignore it for the moment. We will revisit this issue in the next section.


Where are the variables CLSID_VCR, IID_IVideo, and IID_ISVideo defined? If you recall from Chapter 2, when the IDL file Video.idl is compiled using MIDL, two of the generated files were Video.h. and Video_i.c. The first file defines these variables as external constants and the second file specifies the actual storage. File Video.h needs to be included in any C++ source module that refers to either of these constants or to any of the interfaces defined in the IDL file. File Video_i.c should be included just once in some source module.

 #include "Vcr.h"  #include "Video_i.c" 

Failure to include Video_i.c will result in the linker reporting unresolved external symbols when the object files are linked to create the final executable. If it is included more than once, the linker will complain about duplicate symbol definitions.

graphics/01icon01.gif

The version of MIDL that is shipped with Visual C++ 6.0 or the SDK is capable of generating code that eliminates the need for using Video_i.c in the implementation. The generated output contains another form of declaration for GUIDs that make it possible to obtain the GUID using __uuidof, a keyword provided by Visual C++ compiler. If you make the following changes in the previous code, you can remove the inclusion of Video_i.c from the source code.

1. Replace references to IID_IVideo and IID_ISVideo __uuidof(IVideo) and __uuidof(ISVideo), respectively.

2. Replace references to CLSID_VCR with __uuidof(VCR).

This technique works if your reference is limited to interfaces and coclasses. The current version of MIDL does not generate a similar declaration for type librar y ID (LIBID). Therefore, the __uuidof keyword cannot be used with LIBIDs.


The last thing that remains is the creation of a module-definition file. We need to export DllGetClassObject. The difference to be noted is that this function, and some other functions that we will discuss later, are being exported only to be used by the COM infrastructure. Client programs access such a function indirectly (through COM). Therefore, such a function should be declared as PRIVATE.

 ; Module-Definition File Vcr.Def  LIBRARY   "MyVcr.DLL"  EXPORTS    DllGetClassObject    PRIVATE 

graphics/01icon01.gif

The keyword PRIVATE ensures that the function can only be called through the Win32 API GetProcAddress. If the caller attempts to call this function directly, the linker will not be able to resolve the function symbol.


This concludes our move from non-COM C++ to COM C++. Compile and link the code, and we ve got our DLL-based COM server for VCR.

Now let s convert our TV client code to use the COM mechanism. As it turns out, this part is even easier.

The TV Client

The following is the main logic of the TV client code as it appeared at the end of Chapter 1:

 int main(int argc, char* argv[])  {   ISVideo* pSVideo = NULL;    VRESULT vr = CreateInstance("vcr.dll",      "svideo", reinterpret_cast<IGeneral**>(&pSVideo));    if (V_FAILED(vr)) {     ReportError(vr);      return 1;    }    UseSVideo(pSVideo);    pSVideo->Delete();    return 0;  } 

If you recall, function CreateInstance went through LoadLibrary, GetProcAddress, and CreateVCR calls to fetch the needed interface pointer. COM SDK provides this same functionality in an API known as CoGetClassObject. The following is its prototype:

 STDAPI CoGetClassObject(REFCLSID rclsid, DWORD dwClsContext,    COSERVERINFO* pServerInfo, REFIID riid, LPVOID* ppv); 

Most of the parameters to CoGetClassObject are equivalent in meaning to the CreateInstance function:

  • Parameter rclsid identifies the class object to be loaded (as opposed to the name of the DLL in the CreateInstance function).

  • Parameter riid identifies the interface that the client is interested in.

  • Parameter ppv is the return value. Similar to QueryInterface, this function returns a void* for legacy reasons.

The two parameters that do not have any equivalence in function CreateInstance are dwClsContext and pServerInfo.

  • Parameter dwClsContext indicates the type of COM server to load. So far, we have been developing DLL-based COM servers. As we will see later, a COM server could be set to run in the same process space as the client or as a separate process. Since we do not care about the server type at this point, we will specify CLSCTX_ALL for dwClsContext, indicating that we can deal with any type of server.

  • Parameter pServerInfo can be used to specify the machine where the COM server should load and execute. For now, we will specify a NULL value, indicating that we are interested in obtaining the server from the local machine.

The other obvious changes are replacing data type VRESULT with HRESULT and function ReportError with its equivalent function, DumpError, which we implemented in Chapter 2. The following is our revised code:

 int main(int argc, char* argv[])  {   ISVideo* pSVideo = NULL;    HRESULT hr = ::CoGetClassObject(CLSID_VCR, CLSCTX_ALL, NULL,      IID_ISVideo, reinterpret_cast<void**>(&pSVideo));    if (FAILED(hr)) {     DumpError(hr);      return 1;    }    UseSVideo(pSVideo);    pSVideo->Release();    return 0;  } 

What remains to be done is the initialization of the COM system. In order to use COM, the SDK mandates that a client initialize the COM system before making any COM-related calls and reset the system back to its uninitialized state after all the COM-related calls have been made. It defines the following APIs for this purpose:

 HRESULT CoInitialize(LPVOID pvReserved);     // initialize COM  HRESULT CoUninitialize();                    // uninitialize COM 

The parameter to CoInitialize is reserved for future use. It should be set to NULL for now.

If a client forgets to call CoInitialize, most other COM calls (including CoGetClassObject) will return the error code CO_E_NOTINITIALIZED (0x800401F0).

If CoUninitialize is not called, some resources that the COM system had acquired will not be freed, causing a resource leakage in the client s process.

With these two changes in place, the revised client code is as follows:

 int main(int argc, char* argv[])  {   ::CoInitialize(NULL);    ISVideo* pSVideo = NULL;    HRESULT hr = ::CoGetClassObject(CLSID_VCR, CLSCTX_ALL, NULL,      IID_ISVideo, reinterpret_cast<void**>(&pSVideo));    if (FAILED(hr)) {     DumpError(hr);      ::CoUninitialize();      return 1;    }    UseSVideo(pSVideo);    pSVideo->Release();    ::CoUninitialize();    return 0;  } 

Compile and link the code.

 cl tv.cpp ole32.lib. 

graphics/01icon01.gif

Many COM APIs, including CoInitialize, CoUninitialize, and CoGetClassObject, are implemented in the SDK library ole32.lib. This library should always be linked with the client code.


Let s run the TV executable and see what happens.

Here is the output of our run:

 Class not registered   0x80040154 

What happened was that the call to CoGetClassObject failed. The COM infrastructure doesn t know where the implementation for class CLSID_VCR is.

We know the implementation is in MyVcr.dll because we were the ones to implement it. However, unless we provide this information to the COM infrastructure, it will never know the whereabouts of the server.

Let s look at how COM expects us to provide this information.

The COM+ Catalog

The configuration information of a COM class, including the file name of the DLL that implements the COM class, has to be stored in some persistent database so that, when CoCreateInstance is called, the COM system can load the DLL on the client s behalf. This database is referred to as the COM+ Catalog. COM+ provides a component to manage the catalog, appropriately called the Catalog Manager. Windows 2000 provides a configuration management visual tool called the Component Services Snap-in that internally communicates with the Catalog Manager.

The process of adding COM class-specific entries to the catalog is referred to as registering the class. The reverse process, that is, removing the class specific information, is called unregistering the class.

We will learn more about the Catalog Manager in Chapter 5. For this chapter, the point of interest is that the Catalog Manager mandates that a Windows registry key, HKEY_CLASSES_ROOT\CLSID\{<CLSID>}\InprocServer32, be used to store DLL file name information. The default value for this subkey contains the full path to the DLL to be loaded.

Ideally, a COM component should be registered using the Catalog Manager API. However, many legacy components bypass the Catalog Manager and insert the needed registry keys by some other means (the concept of Catalog Manager was not available prior to Windows 2000). Such a component is considered a non-configured component. For our discussion in this chapter, it is sufficient for us to use a non-configured component.

The registry entries can be modified either programmatically or through the OS-provided tool called regedit.exe. Using regedit.exe, one can modify the entries either through the user-interface or through an ASCII file containing the information in a specific format.

We will add the needed entries for our COM server using regedit.exe for now. Later in the chapter we will see how to do this programmatically.

The following text shows the needed entries in a format that regedit.exe can use:

 REGEDIT4  [HKEY_CLASSES_ROOT\CLSID\{318B4AD3-06A7-11d3-9B58- 0080C8E11F14}\InprocServer32]  @="D:\\BookExamples\\Chapter04-ComServer\\01\\MyVcr.dll" 

graphics/01icon01.gif

The first entry in the registry file, REGEDIT4, specifies that the syntax of the file should follow the Windows convention.


Add this information to the registry using the tool regedit.exe, as follows:

 regedit.exe MyVcr.reg 

Once this information is registered, the COM infrastructure now has enough information to know which DLL file to load for CLSID_VCR. If you run the TV client program once again, it will work this time.

Let s recap.

  • We converted the C++ implementation from Chapter 1 into COM-style implementation.

  • We replaced function CreateVCR with function DllGetClassObject.

  • In the client code, we replaced the call to CreateInstance with a call to CoGetClassObject.

  • We added COM initialization/uninitialization logic in the client code.

  • We registered the COM server information.

In our drive to quickly convert the plain C++ code to COM-style C++ code, we left a few questions unanswered:

  • How can we house multiple classes in a DLL?

  • Why did we say that our implementation of DllGetClassObject is not legal under COM?

It s time to revisit our C++ code and fill in the missing gaps.


< 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