C++ COM編程-COM編程入門實踐


1. COM編程基礎
COM是一種規范,而不是實現。

當使用C++來實現時,COM組件就是一個C++類,而COM接口就是繼承至IUnknown的純虛類,COM組件就是實現相應COM接口的C++類。

COM規范規定,任何組件或接口都必須從IUnknown接口中繼承而來。IUnknown定義了3個重要函數,分別是QueryInterface、AddRef和Release。其中,QueryInterface負責組件對象上的接口查詢,AddRef用於增加引用計數,Release用於減少引用計數。引用計數是COM中的一個非常重要的概念,它很好地解決了組件對象地生命周期問題,即COM組件何時被銷毀,以及誰來銷毀地問題。

除了IUnknown接口外,還有另外一個重要地接口,即IClassFactory。COM組件實際上是一個C++類,對於組件地外部使用者來說,這個類名一般不可知,那么如何創建這個類地的例?由誰來創建?COM規范規定,每個組件都必須實現一個與之對應的類工廠(Class Factory)。類工廠也是一個COM組件,它實現了IClassFactory接口。在IClassFactory的接口函數CreateInstance中,才能使用new操作生成一個COM組件類對象實例。

COM組件有3種類型:
① 進程內組件(CLSCTX_INPROC_SERVER)
② 本地進程組件(CLSCTX_LOCAL_SERVER)
③ 遠程組件(CLSCTX_REMOTE_SERVER)

在接口成員函數中,字符串變量必須用Unicode字符指針,這是COM規范的要求。

2. COM組件開發
實現一個COM組件,需要完成以下工作:

COM組件接口
COM組件實現類
COM組件創建工廠
COM組件注冊與取消注冊
本文以一個例子作為說明,COM組件提供了一個SayHello的接口函數,將“Hello COM”打印輸出。

2.1 創建COM組件接口
COM組件接口是一個繼承IUnknown的抽象類:

 1 // IComTest.h
 2 #pragma once
 3 #include <Unknwn.h>
 4 // interface id,COM組件接口唯一標識
 5 static const WCHAR* IID_IComTestStr = L"{213D1B15-9BBA-414A-BAB6-CA5B6CEF0006}";  6 static const GUID IID_IComTest = { 0x213D1B15, 0x9BBA, 0x414A, { 0xBA, 0xB6, 0xCA, 0x5B, 0x6C, 0xEF, 0x00, 0x06 } };  7 class IComTest :public IUnknown  8 {  9 public: 10     virtual int _stdcall SayHello() = 0; 11 };

2.2 創建COM組件實現類

COM組件類是一個實現了相應COM組件接口的C++類,注意:一個COM組件可以同時實現多個COM接口。

 1 // ComTest.h
 2 #pragma once
 3 #include "IComTest.h"
 4 // class id,COM組件唯一標識
 5 static const WCHAR* CLSID_CComTestStr = L"{4046FA83-57F0-4475-9381-8818BFC50DDF}";  6 static const GUID CLSID_CComTest = { 0x4046FA83, 0x57F0, 0x4475, { 0x93, 0x81, 0x88, 0x18, 0xBF, 0xC5, 0x0D, 0xDF } };  7 
 8 class CComTest :public IComTest  9 { 10 public: 11  CComTest(); 12     ~CComTest(); 13 
14     // 實現IUnknown接口 15     // 查找接口 16     // riid : 輸入參數,接口id 17     // ppvObject : 輸出參數,返回相應的接口
18     virtual HRESULT _stdcall QueryInterface(const IID &riid, void ** ppvObject); 19     // 增加引用計數
20     virtual ULONG _stdcall AddRef(); 21     // 減少引用計數
22     virtual ULONG _stdcall Release(); 23     virtual int _stdcall SayHello(); 24 
25 protected: 26     //引用計數
27  ULONG m_RefCount; 28     //全局創建對象個數
29     static ULONG g_ObjNum; 30 };
 1 // ComTest.cpp
 2 #include "ComTest.h"
 3 #include <stdio.h>
 4 
 5 ULONG CComTest::g_ObjNum = 0;  6 
 7 CComTest::CComTest()  8 {  9     m_RefCount = 0; 10     g_ObjNum++;//對象個數+1
11 } 12 
13 CComTest::~CComTest() 14 { 15     g_ObjNum--;//對象個數-1
16 } 17 
18 HRESULT _stdcall CComTest::QueryInterface(const IID &riid, void **ppvObject) 19 { 20     // 通過接口id判斷返回的接口類型
21     if (IID_IUnknown == riid){ 22         *ppvObject = this; 23         ((IUnknown*)(*ppvObject))->AddRef(); 24  } 25     else if (IID_IComTest == riid){ 26         *ppvObject = (IComTest*)this; 27         ((IComTest*)(*ppvObject))->AddRef(); 28  } 29     else{ 30         *ppvObject = NULL; 31         return E_NOINTERFACE; 32  } 33     return S_OK; 34 } 35 
36 ULONG _stdcall CComTest::AddRef() 37 { 38     m_RefCount++; 39     return m_RefCount; 40 } 41 
42 ULONG _stdcall CComTest::Release() 43 { 44     m_RefCount--; 45     if (0 == m_RefCount){ 46         delete this; 47         return 0; 48  } 49     return m_RefCount; 50 } 51 
52 int _stdcall CComTest::SayHello() 53 { 54     printf("hello COM\r\n"); 55     return 666; 56 }

2.3 COM組件創建工廠
對於組件地外部使用者來說,這個COM組件的類名一般不可知,那么如何創建這個類地實例?由誰來創建?COM規范規定,每個組件都必須實現一個與之對應的類工廠(Class Factory)。類工廠也是一個COM組件,它實現了IClassFactory接口。在IClassFactory的接口函數CreateInstance中,才能使用new操作生成一個COM組件類對象實例。

 1 // ComTestFactory.h
 2 #pragma once
 3 #include <Unknwn.h>
 4 
 5 class CComTestFactory : public IClassFactory  6 {  7 public:  8  CComTestFactory();  9     ~CComTestFactory(); 10 
11     // 實現IUnknown接口 
12     virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject); 13     virtual ULONG _stdcall AddRef(); 14     virtual ULONG _stdcall Release(); 15 
16     // 實現IClassFactory接口 
17     virtual HRESULT _stdcall CreateInstance(IUnknown *pUnkOuter, const IID& riid, void **ppvObject); 18     virtual HRESULT _stdcall LockServer(BOOL fLock); 19 
20 protected: 21     ULONG m_RefCount;//引用計數
22     static ULONG g_ObjNum;//全局創建對象個數
23 };
 1 // ComTestFactory.cpp
 2 #include "ComTestFactory.h"
 3 #include "ComTest.h"
 4 
 5 ULONG CComTestFactory::g_ObjNum = 0;  6 
 7 CComTestFactory::CComTestFactory()  8 {  9     m_RefCount = 0; 10     g_ObjNum++; 11 } 12 
13 CComTestFactory::~CComTestFactory() 14 { 15     g_ObjNum--; 16 } 17 
18 // 查詢指定接口
19 HRESULT _stdcall CComTestFactory::QueryInterface(const IID &riid, void **ppvObject) 20 { 21     if (IID_IUnknown == riid){ 22         *ppvObject = (IUnknown*)this; 23         ((IUnknown*)(*ppvObject))->AddRef(); 24  } 25     else if (IID_IClassFactory == riid){ 26         *ppvObject = (IClassFactory*)this; 27         ((IClassFactory*)(*ppvObject))->AddRef(); 28  } 29     else{ 30         *ppvObject = NULL; 31         return E_NOINTERFACE; 32  } 33     return S_OK; 34 } 35 
36 ULONG _stdcall CComTestFactory::AddRef() 37 { 38     m_RefCount++; 39     return m_RefCount; 40 } 41 
42 ULONG _stdcall CComTestFactory::Release() 43 { 44     m_RefCount--; 45     if (0 == m_RefCount){ 46         delete this; 47         return 0; 48  } 49     return m_RefCount; 50 } 51 
52 // 創建COM對象,並返回指定接口
53 HRESULT _stdcall CComTestFactory::CreateInstance(IUnknown *pUnkOuter, const IID &riid, void **ppvObject) 54 { 55     if (NULL != pUnkOuter){ 56         return CLASS_E_NOAGGREGATION; 57  } 58     HRESULT hr = E_OUTOFMEMORY; 59     //ComClass::Init();
60     CComTest* pObj = new CComTest(); 61     if (NULL == pObj){ 62         return hr; 63  } 64     hr = pObj->QueryInterface(riid, ppvObject); 65     if (S_OK != hr){ 66         delete pObj; 67  } 68     return hr; 69 } 70 
71 HRESULT _stdcall CComTestFactory::LockServer(BOOL fLock) 72 { 73     return NOERROR; 74 }

2.4 COM組件的注冊
COM組件需要使用regsvr32工具注冊到系統才能被調用,然而COM組件是如何被regsvr32注冊的?一個典型的自注冊COM組件需要提供4個必需的導出函數:

DllGetClassObject:用於獲得類工廠指針
DllCanUnloadNow:系統空閑時會調用這個函數,以確定是否可以卸載COM組件
DllRegisterServer:將COM組件注冊到注冊表中
DllUnregisterServer:刪除注冊表中的COM組件的注冊信息
DLL還有一個可選的入口函數DllMain,可用於初始化和釋放全局變量

DllMain:DLL的入口函數,在LoadLibrary和FreeLibrary時都會調用

1 // ComTestExport.h
2 #include <windows.h> 
3 
4 extern "C" HRESULT _stdcall DllRegisterServer(); 5 extern "C" HRESULT _stdcall DllUnregisterServer(); 6 extern "C" HRESULT _stdcall DllCanUnloadNow(); 7 extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv);
 1 // ComTestExport.cpp
 2 #include "ComTestExport.h"
 3 #include "ComTestFactory.h"
 4 #include "ComTest.h"
 5 
 6 #include <iostream>
 7 
 8 HMODULE g_hModule;  //dll進程實例句柄 
 9 ULONG g_num;        //組件中ComTest對象的個數,用於判斷是否可以卸載本組建,如值為0則可以卸載 
 10 
 11 int myReg(LPCWSTR lpPath)   //將本組件的信息寫入注冊表,包括CLSID、所在路徑lpPath、ProgID 
 12 {  13  HKEY thk, tclsidk;  14 
 15     //打開鍵HKEY_CLASSES_ROOT\CLSID,創建新鍵為ComTest的CLSID,  16     //在該鍵下創建鍵InprocServer32,並將本組件(dll)所在路徑lpPath寫為該鍵的默認值 
 17     if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk)){  18 
 19         printf("RegOpenKey ok\r\n");  20 
 21         if (ERROR_SUCCESS == RegCreateKey(thk, CLSID_CComTestStr, &tclsidk)){  22 
 23             wprintf(L"RegCreateKey %s ok\r\n", CLSID_CComTestStr);  24 
 25  HKEY tinps32k, tprogidk;  26             if (ERROR_SUCCESS == RegCreateKey(tclsidk, L"InprocServer32", &tinps32k)){  27 
 28                 printf("RegCreateKey InprocServer32 ok\r\n");  29 
 30                 if (ERROR_SUCCESS == RegSetValue(tinps32k, NULL, REG_SZ, lpPath, wcslen(lpPath) * 2)){  31  }  32  RegCloseKey(tinps32k);  33  }  34  RegCloseKey(tclsidk);  35  }  36  RegCloseKey(thk);  37  }  38     //在鍵HKEY_CLASSES_ROOT下創建新鍵為COMCTL.CComTest,  39     //在該鍵下創建子鍵,並將CCompTest的CLSID寫為該鍵的默認值 
 40     if (ERROR_SUCCESS == RegCreateKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest", &thk)){  41         if (ERROR_SUCCESS == RegCreateKey(thk, L"CLSID", &tclsidk)){  42             if (ERROR_SUCCESS == RegSetValue(tclsidk,  43  NULL,  44  REG_SZ,  45  CLSID_CComTestStr,  46                 wcslen(CLSID_CComTestStr) * 2)){  47  }  48  }  49  }  50     //這樣的話一個客戶端程序如果想要使用本組件,首先可以以COMCTL.CComTest為參數調用CLSIDFromProgID函數  51     //來獲取CCompTest的CLSID,再以這個CLSID為參數調用CoCreateInstance創建COM對象 
 52     return 0;  53 }  54 
 55 extern "C" HRESULT _stdcall DllRegisterServer()  56 {  57     WCHAR szModule[1024];  58     //獲取本組件(dll)所在路徑 
 59     DWORD dwResult = GetModuleFileName(g_hModule, szModule, 1024);  60     if (0 == dwResult){  61         return -1;  62  }  63     MessageBox(NULL, szModule, L"", MB_OK);  64     //將路徑等信息寫入注冊表 
 65  myReg(szModule);  66     return 0;  67 }  68 
 69 int myDelKey(HKEY hk, LPCWSTR lp)  70 {  71     if (ERROR_SUCCESS == RegDeleteKey(hk, lp)){  72  }  73     return 0;  74 }  75 
 76 //刪除注冊時寫入注冊表的信息 
 77 int myDel()  78 {  79  HKEY thk;  80     if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk)){  81         myDelKey(thk, L"{4046FA83-57F0-4475-9381-8818BFC50DDF}\\InprocServer32");  82  myDelKey(thk, CLSID_CComTestStr);  83 
 84  RegCloseKey(thk);  85  }  86     if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest", &thk)){  87         myDelKey(thk, L"CLSID");  88  }  89     myDelKey(HKEY_CLASSES_ROOT, L"COMCTL.CComTest");  90     return 0;  91 }  92 
 93 extern "C" HRESULT _stdcall DllUnregisterServer()  94 {  95     //刪除注冊時寫入注冊表的信息 
 96  myDel();  97     return 0;  98 }  99 
100 // 用於判斷是否可以卸載本組建, 由CoFreeUnusedLibraries函數調用 
101 extern "C" HRESULT _stdcall DllCanUnloadNow() 102 { 103     //如果對象個數為0,則可以卸載 
104     if (0 == g_num){ 105         return S_OK; 106  } 107     else{ 108         return S_FALSE; 109  } 110 } 111 
112 //用於創建類廠並返回所需接口,由CoGetClassObject函數調用 
113 extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv) 114 { 115  LPOLESTR szCLSID; 116     StringFromCLSID(rclsid, &szCLSID);     //將其轉化為字符串形式用來輸出 
117     wprintf(L"rclsid CLSID \"%s\"\n", szCLSID); 118 
119  szCLSID; 120     StringFromCLSID(riid, &szCLSID);     //將其轉化為字符串形式用來輸出 
121     wprintf(L"riid CLSID \"%s\"\n", szCLSID); 122 
123     if (CLSID_CComTest == rclsid){ 124         CComTestFactory* pFactory = new CComTestFactory();//創建類廠對象 
125         if (NULL == pFactory){ 126             return E_OUTOFMEMORY; 127  } 128         HRESULT result = pFactory->QueryInterface(riid, ppv);//獲取所需接口 
129         return result; 130  } 131     else{ 132         return CLASS_E_CLASSNOTAVAILABLE; 133  } 134 } 135 
136 // 提供DLL入口;對於動態鏈接庫,DllMain是一個可選的入口函數,在COM組件中是必須有的
137 BOOL APIENTRY DllMain(HMODULE hModule, 138  DWORD ul_reason_for_call, 139  LPVOID lpReserved 140  ) 141 { 142     //獲取進程實例句柄,用於獲取本組件(dll)路徑 
143     g_hModule = hModule; 144     switch (ul_reason_for_call) 145  { 146     case DLL_PROCESS_ATTACH: 147     case DLL_THREAD_ATTACH: 148     case DLL_THREAD_DETACH: 149     case DLL_PROCESS_DETACH: 150         break; 151  } 152     return TRUE; 153 }

導出文件(Source.def):

1 LIBRARY "ComTest_Server"
2 EXPORTS 3 DllCanUnloadNow 4 DllGetClassObject 5 DllUnregisterServer 6 DllRegisterServer

生成完后,使用regsvr32注冊到系統中:

> regsvr32 ComTest_Server.dll

3. COM組件使用

COM組件的使用包括:

  1. 如何創建COM組件
  2. 如何得到組件對象上的接口以及如何調用接口方法
  3. 如何管理組件對象(需熟悉COM的引用計數機制)

下面的代碼是最一般的步驟:

 1 CoInitialize(NULL);    // COM庫初始化  2 // ...
 3 IUnknow *pUnk = NULL;  4 IObject *pObj = NULL;  5 // 創建組件對象,CLSID_XXX為COM組件類的GUID(class id),返回默認IID_IUnknown接口
 6 HRESULT hr = CoCreateInstance(CLSID_XXX,NULL,CLSCTX_INPROC_SERVER,NULL,IID_IUnknown,(void **)&pUnk);  7 if(S_OK == hr)  8 {  9     // 獲取接口,IID_XXX為組件接口的GUID(interface id)
10     hr = pUnk->QueryInterface(IID_XXX,(void **)&pObj); 11     if(S_OK == hr) 12  { 13         // 調用接口方法
14         pObj->DoXXX(); 15  } 16     // 釋放組件對象
17     pUnk->Release(); 18 } 19 //... 20 // 釋放COM庫
21 CoUninitialize();

下面我們編寫一個客戶端,調用之前寫的COM組件服務:

 1 #include "IComTest.h"
 2 #include "ComTest.h"
 3 #include <stdio.h>
 4 
 5 int main()  6 {  7     // 初始化COM庫
 8  CoInitialize(NULL);  9 
10     IComTest *pComTest = NULL; 11  HRESULT hResult; 12 
13     // 創建進程內COM組件,返回指定接口
14     hResult = CoCreateInstance(CLSID_CComTest, NULL, CLSCTX_INPROC_SERVER, IID_IComTest, (void **)&pComTest); 15     if (S_OK == hResult) 16  { 17         // 調用接口方法
18         printf("%d\r\n", pComTest->SayHello()); 19         // 釋放組件
20         pComTest->Release(); 21  } 22     // 釋放COM庫
23  CoUninitialize(); 24 
25     return 0; 26 }

上面的例子和一般步驟不一致,少了QueryInterface,是因為默認返回的就是指定的接口,下面按一般步驟再實現一次:

 1 #include "IComTest.h"
 2 #include "ComTest.h"
 3 #include <stdio.h>
 4 
 5 int main()  6 {  7     // 初始化COM庫
 8  CoInitialize(NULL);  9 
10     IUnknown *pUnk = NULL; 11     IComTest *pComTest = NULL; 12  HRESULT hResult; 13 
14     // 創建COM組件,返回默認接口
15     hResult = CoCreateInstance(CLSID_CComTest, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk); 16     if (S_OK == hResult) 17  { 18         // 查詢接口
19         hResult = pUnk->QueryInterface(IID_IComTest, (void **)&pComTest); 20         if (S_OK == hResult) 21  { 22             // 調用接口方法
23             printf("%d\r\n", pComTest->SayHello()); 24  } 25         // 釋放組件
26         pComTest->Release(); 27  } 28 
29     // 釋放COM庫
30  CoUninitialize(); 31     return 0; 32 }

是直接創建COM組件並獲取接口,還是先創建COM組件得到默認接口再查詢其他的接口,需要具體問題具體分析。

4.COM組件運行機制

一個COM組件從編寫到最終可以被調用,整個運行流程是怎樣的?或者我們再考慮簡單一點,COM組件是如何被調用的?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM