实现一个COM组件,需要完成以下工作:
- COM组件接口
- COM组件实现类
- COM组件创建工厂
- COM组件注册与反注册
本文以一个例子作为说明,COM组件实现了一个矩形类,提供了两个接口,IAbstructShape和IDisplay。 IAbstructShape接口提供了shapeType接口函数,返回矩形类的类型。IDisplay接口提供了draw接口函数,将在屏幕上绘制出矩形。
一、COM组件的实现
代码目录结构如下
|
|--include
|--IAbstructShape.h
|--IDisplay.h
|--CRectangle.h
|--CRectangle.cpp
|--CRectangleFactory.h
|--CRectangleFactory.cpp
|--CRectangleExport.h
|--CRectangleExport.cpp
|--CRectangle.def
1、IAbstructShape接口
COM的所有接口都应该继承自IUnknown,因此IAbstructShape接口也是如此。
// IAbstructShape.h
#pragma once
#include <Unknwn.h>
// interface id,COM组件接口唯一标识
// {455C1480-C5C7-4EEB-B88F-CC91D2606CE5}
static const WCHAR* IID_IAbstructShapeStr = L"{455C1480-C5C7-4EEB-B88F-CC91D2606CE5}";
static const GUID IID_IAbstructShape =
{ 0x455c1480, 0xc5c7, 0x4eeb, { 0xb8, 0x8f, 0xcc, 0x91, 0xd2, 0x60, 0x6c, 0xe5 } };
class IAbstructShape : public IUnknown
{
public:
virtual int _stdcall shapeType() = 0;
};
对于COM的每一个接口,都有惟一的GUID标志。GUID的生成可以使用VS自带的GUID生成工具。
2、IDisplay接口
// IDisplay.h
#pragma once
#include <Unknwn.h>
// interface id,COM组件接口唯一标识
// {E3780F3F-875A-4A77-B66E-DAC8119938AC}
static const WCHAR* IID_IDisplayStr = L"{E3780F3F-875A-4A77-B66E-DAC8119938AC}";
static const GUID IID_IDisplay =
{ 0xe3780f3f, 0x875a, 0x4a77, { 0xb6, 0x6e, 0xda, 0xc8, 0x11, 0x99, 0x38, 0xac } };
class IDisplay : public IUnknown
{
public:
virtual int _stdcall draw() = 0;
};
3、COM组件类 -- CRectangle类实现
COM组件类是一个实现了相应COM组件接口的C++类,注意:一个COM组件可以同时实现多个COM接口。
CRectangle就实现了IAbstructShape和IDisplay两个接口,当然,IUnknown接口是每一个COM组件类必须要实现的。
// CRectangle.h
#pragma once
#include "include/IAbstructShape.h"
#include "include/IDisplay.h"
// class id,COM组件唯一标识
// {C1271670-DD25-4365-B0A6-3FABBF4F0177}
static const WCHAR* CLSID_CRectanleStr = L"{C1271670-DD25-4365-B0A6-3FABBF4F0177}";
static const GUID CLSID_CRectanle =
{ 0xc1271670, 0xdd25, 0x4365, { 0xb0, 0xa6, 0x3f, 0xab, 0xbf, 0x4f, 0x1, 0x77 } };
class CRectangle :
public IAbstructShape,
public IDisplay
{
public:
CRectangle();
~CRectangle();
// 实现IUnknown接口
// riid : 输入参数,接口id
// ppvObject : 输出参数,返回相应的接口
virtual HRESULT _stdcall QueryInterface(const IID &riid, void ** ppvObject) override;
virtual ULONG _stdcall AddRef() override;
virtual ULONG _stdcall Release() override;
// 实现IAbstructShape接口
virtual int _stdcall shapeType() override;
// 实现IDisplay接口
virtual int _stdcall draw() override;
protected:
//引用计数
ULONG m_RefCount;
//全局创建对象个数
static ULONG g_ObjNum;
};
// CRectangle.cpp
#include <stdio.h>
#include "CRectangle.h"
ULONG CRectangle::g_ObjNum = 0;
CRectangle::CRectangle()
{
m_RefCount = 0;
g_ObjNum++;//对象个数+1
}
CRectangle::~CRectangle()
{
g_ObjNum--;//对象个数-1
}
HRESULT _stdcall CRectangle::QueryInterface(const IID &riid, void **ppvObject)
{
// 通过接口id判断返回的接口类型
if (IID_IUnknown == riid)
{
*ppvObject = this;
((IUnknown*)(*ppvObject))->AddRef();
}
else if (IID_IAbstructShape == riid)
{
*ppvObject = (IAbstructShape*)this;
((IAbstructShape*)(*ppvObject))->AddRef();
}
else if (IID_IDisplay == riid)
{
*ppvObject = (IDisplay*)this;
((IDisplay*)(*ppvObject))->AddRef();
}
else
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
ULONG _stdcall CRectangle::AddRef()
{
m_RefCount++;
return m_RefCount;
}
ULONG _stdcall CRectangle::Release()
{
m_RefCount--;
if (0 == m_RefCount)
{
delete this;
return 0;
}
return m_RefCount;
}
int _stdcall CRectangle::shapeType()
{
return 0x1001;
}
int _stdcall CRectangle::draw()
{
printf("begin draw a Rectangle...\r\n\r\n");
printf("-----------------\r\n");
printf("| |\r\n");
printf("| |\r\n");
printf("-----------------\r\n\r\n");
printf("end draw! Successful!!!\r\n");
return 0;
}
4、 COM组件创建工厂
对于组件外部的使用者(客户端)来说,COM组件的类名一般是不可知,那么如何创建这个类的实例?由谁来创建?COM规范规定,每个组件都必须实现一个与之对应的类工厂(Class Factory)。类工厂也是一个COM组件,它实现了IClassFactory接口。在IClassFactory的接口函数CreateInstance中,才能使用new操作符实例化一个COM组件类的对象。
// CRectangleFactory.h
#pragma once
#include <Unknwn.h>
class CRectangleFactory : public IClassFactory
{
public:
CRectangleFactory();
~CRectangleFactory();
// 实现IUnknown接口
virtual HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject);
virtual ULONG _stdcall AddRef();
virtual ULONG _stdcall Release();
// 实现IClassFactory接口
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnkOuter, const IID& riid, void **ppvObject);
virtual HRESULT _stdcall LockServer(BOOL fLock);
protected:
ULONG m_RefCount;//引用计数
static ULONG g_ObjNum;//全局创建对象个数
};
// CRectangleFactory.cpp
#include "CRectangleFactory.h"
#include "CRectangle.h"
ULONG CRectangleFactory::g_ObjNum = 0;
CRectangleFactory::CRectangleFactory()
{
m_RefCount = 0;
g_ObjNum++;
}
CRectangleFactory::~CRectangleFactory()
{
g_ObjNum--;
}
// 查询指定接口
HRESULT _stdcall CRectangleFactory::QueryInterface(const IID &riid, void **ppvObject)
{
if (IID_IUnknown == riid)
{
*ppvObject = (IUnknown*)this;
((IUnknown*)(*ppvObject))->AddRef();
}
else if (IID_IClassFactory == riid)
{
*ppvObject = (IClassFactory*)this;
((IClassFactory*)(*ppvObject))->AddRef();
}
else
{
*ppvObject = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
ULONG _stdcall CRectangleFactory::AddRef()
{
m_RefCount++;
return m_RefCount;
}
ULONG _stdcall CRectangleFactory::Release()
{
m_RefCount--;
if (0 == m_RefCount)
{
delete this;
return 0;
}
return m_RefCount;
}
// 创建COM对象,并返回指定接口
HRESULT _stdcall CRectangleFactory::CreateInstance(IUnknown *pUnkOuter, const IID &riid, void **ppvObject)
{
if (NULL != pUnkOuter)
{
return CLASS_E_NOAGGREGATION;
}
HRESULT hr = E_OUTOFMEMORY;
//ComClass::Init();
CRectangle* pObj = new CRectangle();
if (NULL == pObj)
{
return hr;
}
hr = pObj->QueryInterface(riid, ppvObject);
if (S_OK != hr)
{
delete pObj;
}
return hr;
}
HRESULT _stdcall CRectangleFactory::LockServer(BOOL fLock)
{
return NOERROR;
}
5、 COM组件的注册
COM组件需要使用regsvr32工具注册到系统才能被系统自动调用,然而COM组件是如何被regsvr32注册的?一个典型的自注册COM组件需要提供4个必需的导出函数:
- DllGetClassObject:用于获得类工厂指针
- DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载COM组件
- DllRegisterServer:将COM组件注册到注册表中
- DllUnregisterServer:删除注册表中COM组件的注册信息
// CRectangleExport.h
#include <windows.h>
extern "C" HRESULT _stdcall DllRegisterServer();
extern "C" HRESULT _stdcall DllUnregisterServer();
extern "C" HRESULT _stdcall DllCanUnloadNow();
extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv);
// CRectangleExport.cpp
#include <stdio.h>
#include "CRectangle.h"
#include "CRectangleFactory.h"
#include "CRectangleExport.h"
HMODULE g_hModule; //dll进程实例句柄
ULONG g_num; //组件中ComTest对象的个数,用于判断是否可以卸载本组建,如值为0则可以卸载
int RegCRectangle(LPCWSTR lpPath) //将本组件的信息写入注册表,包括CLSID、所在路径lpPath、ProgID
{
int iRet = 1;
HKEY thk;
HKEY tclsidk;
//打开键HKEY_CLASSES_ROOT\CLSID,创建新键为CRectangle的CLSID,
//在该键下创建键InprocServer32,并将本组件(dll)所在路径lpPath写为该键的默认值
if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk))
{
printf("RegOpenKey ok\r\n");
if (ERROR_SUCCESS == RegCreateKey(thk, CLSID_CRectanleStr, &tclsidk))
{
wprintf(L"RegCreateKey %s ok\r\n", CLSID_CRectanleStr);
HKEY tinps32k, tprogidk;
if (ERROR_SUCCESS == RegCreateKey(tclsidk, L"InprocServer32", &tinps32k))
{
printf("RegCreateKey InprocServer32 ok\r\n");
if (ERROR_SUCCESS == RegSetValue(tinps32k, NULL, REG_SZ, lpPath, wcslen(lpPath) * 2))
{
iRet = 0;
}
RegCloseKey(tinps32k);
}
RegCloseKey(tclsidk);
}
RegCloseKey(thk);
}
//在键HKEY_CLASSES_ROOT下创建新键为COMCTL.CRectangle,
//在该键下创建子键,并将CRectangle的CLSID写为该键的默认值
if (ERROR_SUCCESS == RegCreateKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle", &thk))
{
if (ERROR_SUCCESS == RegCreateKey(thk, L"CLSID", &tclsidk))
{
if (ERROR_SUCCESS == RegSetValue(tclsidk,
NULL,
REG_SZ,
CLSID_CRectanleStr,
wcslen(CLSID_CRectanleStr) * 2))
{
printf("RegCreateKey COMCTL.CRectangle ok\r\n");
}
}
}
//这样的话一个客户端程序如果想要使用本组件,首先可以以COMCTL.CRectangle为参数调用CLSIDFromProgID函数
//来获取CRectangle的CLSID,再以这个CLSID为参数调用CoCreateInstance创建COM对象
return iRet;
}
extern "C" HRESULT _stdcall DllRegisterServer()
{
WCHAR szModule[1024];
//获取本组件(dll)所在路径
DWORD dwResult = GetModuleFileName(g_hModule, szModule, 1024);
if (0 == dwResult)
{
printf("Get file name failed!!!\r\n");
return -1;
}
//将路径等信息写入注册表
if (0 == RegCRectangle(szModule))
MessageBox(NULL, szModule, L"CRectangle", MB_OK);
else
MessageBox(NULL, szModule, L"CRectangle", MB_OK | MB_ICONERROR);
return 0;
}
int DelRegKey(HKEY hk, LPCWSTR lp)
{
if (ERROR_SUCCESS == RegDeleteKey(hk, lp))
{
return 0;
}
return 1;
}
//删除注册时写入注册表的信息
int UnRegCRectangle()
{
HKEY thk;
if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk))
{
DelRegKey(thk, L"{C1271670-DD25-4365-B0A6-3FABBF4F0177}\\InprocServer32");
DelRegKey(thk, CLSID_CRectanleStr);
RegCloseKey(thk);
}
if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle", &thk))
{
DelRegKey(thk, L"CLSID");
}
DelRegKey(HKEY_CLASSES_ROOT, L"COMCTL.CRectangle");
return 0;
}
extern "C" HRESULT _stdcall DllUnregisterServer()
{
//删除注册时写入注册表的信息
UnRegCRectangle();
return 0;
}
// 用于判断是否可以卸载本组建, 由CoFreeUnusedLibraries函数调用
extern "C" HRESULT _stdcall DllCanUnloadNow()
{
//如果对象个数为0,则可以卸载
if (0 == g_num)
{
return S_OK;
}
else
{
return S_FALSE;
}
}
//用于创建类厂并返回所需接口,由CoGetClassObject函数调用
extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR* ppv)
{
LPOLESTR szCLSID;
StringFromCLSID(rclsid, &szCLSID); //将其转化为字符串形式用来输出
wprintf(L"rclsid CLSID \"%s\"\n", szCLSID);
szCLSID;
StringFromCLSID(riid, &szCLSID); //将其转化为字符串形式用来输出
wprintf(L"riid CLSID \"%s\"\n", szCLSID);
if (CLSID_CRectanle == rclsid)
{
CRectangleFactory* pFactory = new CRectangleFactory();//创建类厂对象
if (NULL == pFactory)
{
return E_OUTOFMEMORY;
}
HRESULT result = pFactory->QueryInterface(riid, ppv);//获取所需接口
return result;
}
else
{
return CLASS_E_CLASSNOTAVAILABLE;
}
}
// 提供DLL入口;对于动态链接库,DllMain是一个可选的入口函数,在COM组件中是必须有的
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//获取进程实例句柄,用于获取本组件(dll)路径
g_hModule = hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
定义一个导出文件(CRectangle.def),方便导出函数。
LIBRARY "CRectangle"
EXPORTS
DllCanUnloadNow
DllGetClassObject
DllUnregisterServer
DllRegisterServer
在VS中,还需要在linker中将导出文件名加上,否则不会导出任何函数
编译成功后,在Debug/Release目录下,会生成CRectangle.dll文件,这个就是COM组件。使用regsvr32命令注册到到系统中。在命令行中输入以下命令。
会两次弹框,第一个弹框是CRectangleExport中自己弹框提示的,第二个是regsvr32命令弹框的。
查看注册表。可以看到正确生成相关信息。
在Root目录下,生成了COMCTL.CRectangle节点,其子节点CLSID的值就是CRectangle的classid。
在Root/wow6432node/CLSID下面,生成了对应的GUID节点,其子节点InprocServer32的值就是COM组件在磁盘上的目录路径。
注意:若是32位系统,就早Root/CLSID下面,若是64位系统,就在Root/wow6432node/CLSID路径下。
至此,CRectangle这个COM组件就完成了,现在可以使用了。
二、COM组件的使用
COM组件的使用包括:
- 如何创建COM组件
- 如何得到组件对象上的接口以及如何调用接口方法
- 如何管理组件对象(需熟悉COM的引用计数机制)
#include <stdio.h>
#include "CRectangle.h"
int main()
{
// 初始化COM库
CoInitialize(NULL);
IUnknown *pUnk = NULL;
// 创建COM组件,返回默认接口
HRESULT hResult = CoCreateInstance(CLSID_CRectanle, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
if (S_OK == hResult)
{
// 查询IAbstructShape接口
IAbstructShape *pAbstructShape = NULL;
hResult = pUnk->QueryInterface(IID_IAbstructShape, (void **)&pAbstructShape);
if (S_OK == hResult)
{
// 调用接口方法
printf("shapetype:%d\r\n", pAbstructShape->shapeType());
}
// 释放组件
pAbstructShape->Release();
// 查询IDisplay接口
IDisplay *pDisplay = NULL;
hResult = pUnk->QueryInterface(IID_IDisplay, (void **)&pDisplay);
if (S_OK == hResult)
{
// 调用接口方法
pDisplay->draw();
}
// 释放组件
pDisplay->Release();
}
// 释放COM库
CoUninitialize();
return 0;
}
运行效果: