一杯清酒邀明月
天下本无事,庸人扰之而烦耳。

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组件是如何被调用的?

posted on 2021-08-16 09:59  一杯清酒邀明月  阅读(3383)  评论(0编辑  收藏  举报