实现一个COM组件,需要完成以下工作:
本文以一个例子作为说明,COM组件实现了一个矩形类,提供了两个接口,IAbstructShape和IDisplay。 IAbstructShape接口提供了shapeType接口函数,返回矩形类的类型。IDisplay接口提供了draw接口函数,将在屏幕上绘制出矩形。
代码目录结构如下
| |–include |–IAbstructShape.h |–IDisplay.h |–CRectangle.h |–CRectangle.cpp |–CRectangleFactory.h |–CRectangleFactory.cpp |–CRectangleExport.h |–CRectangleExport.cpp |–CRectangle.def
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生成工具。
// 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; };
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; }
对于组件外部的使用者(客户端)来说,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; }
COM组件需要使用regsvr32工具注册到系统才能被系统自动调用,然而COM组件是如何被regsvr32注册的?一个典型的自注册COM组件需要提供4个必需的导出函数:
// 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"EXPORTSDllCanUnloadNowDllGetClassObjectDllUnregisterServerDllRegisterServer
在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组件的使用包括:
#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; }
运行效果:
参考链接: COM编程(2)– 第一个COM组件 UUID、CLSID、IID的获取