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組件的使用包括:
- 如何創建COM組件
- 如何得到組件對象上的接口以及如何調用接口方法
- 如何管理組件對象(需熟悉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組件是如何被調用的?