Com 组件技术在Windows平台上应用十分广泛,虽然历史久远,但是仍然具有旺盛的生命力。下面提供一个最基础的Com组件供参考学习。
(详细资料见资源文件https://download.csdn.net/download/yuanshenqiang/90532743)
一 组件类
// Interface.h #pragma once #include <Unknwn.h> static const WCHAR* IID_ISayHelloStr = L"{213D1B15-9BBA-414A-BAB6-CA5B6CEF0006}"; // PowerShell生成GUID指令: '{'+[guid]::NewGuid().ToString().ToUpper()+'}' static const GUID IID_ISayHello = { 0x213D1B15, 0x9BBA, 0x414A, { 0xBA, 0xB6, 0xCA, 0x5B, 0x6C, 0xEF, 0x00, 0x06 } }; // SayHello.h #include "Interface.h" class INotifyCallback { public: STDMETHOD(OnCallback) () PURE; }; class SayHelloInterface:public IUnknown { public: virtual int _stdcall SayHello() = 0; virtual int _stdcall SetCallback(INotifyCallback* cb) = 0; }; class CSayHello :public SayHelloInterface { public: CSayHello(); ~CSayHello(); // 实现IUnknown接口 // 查找接口 // riid : 输入参数,接口id // ppvObject : 输出参数,返回相应的接口 HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject) override; // 增加引用计数 ULONG _stdcall AddRef() override; // 减少引用计数 ULONG _stdcall Release() override; int _stdcall SayHello() override; int _stdcall SetCallback(INotifyCallback* cb); //全局创建对象个数 static ULONG g_ObjNum; protected: //引用计数 ULONG m_RefCount; INotifyCallback* m_Cb; }; // SayHello.cpp --组件类实现 #include "SayHello.h" #include <stdio.h> ULONG CSayHello::g_ObjNum = 0; CSayHello::CSayHello() { m_RefCount = 0; InterlockedIncrement(&g_ObjNum);//对象个数+1 } CSayHello::~CSayHello() { InterlockedDecrement(&g_ObjNum);//对象个数-1 } HRESULT _stdcall CSayHello::QueryInterface(const IID& riid, void** ppvObject) { // 通过接口id判断返回的接口类型 if (IID_IUnknown == riid) { *ppvObject = static_cast<IUnknown*>(this); ((IUnknown*)(*ppvObject))->AddRef(); } else if (IID_ISayHello == riid) { *ppvObject = static_caast<SayHelloInterface*>(this); ((SayHelloInterface*)(*ppvObject))->AddRef(); } else { *ppvObject = NULL; return E_NOINTERFACE; } return S_OK; } ULONG _stdcall CSayHello::AddRef() { return InterlockedIncrement(&m_RefCount); } ULONG _stdcall CSayHello::Release() { ULONG refCount = InterlockedDecrement(&m_RefCount); if (0 >= refCount) { delete this; } return refCount; } int _stdcall CSayHello::SayHello() { printf("hello COM\r\n"); if(m_Cb != NULL) m_Cb->OnCallback(); return 0; } int _stdcall CSayHello::SetCallback(INotifyCallback* cb) { m_Cb = cb; return 0; }二 工厂类
//ComFactory.h -- 工厂类声明 #pragma once #include <Unknwn.h> class ComFactory:public IClassFactory { public: ComFactory(); ~ComFactory(); // 实现IUnknown接口 HRESULT _stdcall QueryInterface(const IID& riid, void** ppvObject) override; ULONG _stdcall AddRef() override; ULONG _stdcall Release() override; // 实现IClassFactory接口 HRESULT _stdcall CreateInstance(IUnknown* pUnkOuter, const IID& riid, void** ppvObject) override; HRESULT _stdcall LockServer(BOOL fLock) override; protected: ULONG m_RefCount;//引用计数 static ULONG g_ObjNum;//全局创建对象个数 }; //ComFactory.cpp -- 工厂类实现 #include "ComFactory.h" #include "SayHello.h" ULONG ComFactory::g_ObjNum = 0; ComFactory::ComFactory() { m_RefCount = 0; InterlockedIncrement(&g_ObjNum); } ComFactory::~ComFactory() { InterlockedDecrement(&g_ObjNum); } // 查询指定接口 HRESULT _stdcall ComFactory::QueryInterface(const IID& riid, void** ppvObject) { if (IID_IUnknown == riid) { *ppvObject = static_cast<IUnknown*>(this); ((IUnknown*)(*ppvObject))->AddRef(); } else if (IID_IClassFactory == riid) { *ppvObject = static_cast<IClassFactory*>(this); ((IClassFactory*)(*ppvObject))->AddRef(); } else { *ppvObject = NULL; return E_NOINTERFACE; } return S_OK; } ULONG _stdcall ComFactory::AddRef() { return InterlockedIncrement(&m_RefCount); } ULONG _stdcall ComFactory::Release() { ULONG refCount = InterlockedDecrement(&m_RefCount); if (0 >= refCount) { delete this; } return refCount; } // 创建COM对象,并返回指定接口 HRESULT _stdcall ComFactory::CreateInstance(IUnknown* pUnkOuter, const IID& riid, void** ppvObject) { if (NULL != pUnkOuter) return CLASS_E_NOAGGREGATION; HRESULT hr = E_OUTOFMEMORY; CSayHello* pObj = new CSayHello(); if (NULL == pObj) { return hr; } hr = pObj->QueryInterface(riid, ppvObject); if (S_OK != hr) { delete pObj; } return hr; } HRESULT _stdcall ComFactory::LockServer(BOOL fLock) { return NOERROR; }三 导出模块
// Server.h -- 导出模块声明 #pragma once #include <windows.h> static const WCHAR* CLSID_CSayHelloStr = L"{4046FA83-57F0-4475-9381-8818BFC50DDF}"; static const GUID CLSID_CSayHello = { 0x4046FA83, 0x57F0, 0x4475, { 0x93, 0x81, 0x88, 0x18, 0xBF, 0xC5, 0x0D, 0xDF } }; extern "C" HRESULT __stdcall DllRegisterServer(); extern "C" HRESULT __stdcall DllUnregisterServer(); extern "C" HRESULT __stdcall DllCanUnloadNow(void); extern "C" HRESULT __stdcall DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Out_ LPVOID * ppv); // Server.cpp -- 导出模块实现 #include "SayHello.h" #include "ComFactory.h" #include <iostream> #include <strsafe.h> #include "Server.h" HMODULE g_hModule; //dll进程实例句柄 ULONG g_num; //组件中Com对象的个数,用于判断是否可以卸载本组建,如值为0则可以卸载 LSTATUS myReg(LPCWSTR lpPath) //将本组件的信息写入注册表,包括CLSID、所在路径lpPath、ProgID { HKEY thk = NULL, tclsidk = NULL; //打开键HKEY_CLASSES_ROOT\CLSID,创建新键为ComTest的CLSID, //在该键下创建键InprocServer32,并将本组件(dll)所在路径lpPath写为该键的默认值 LSTATUS status = RegOpenKeyW(HKEY_CLASSES_ROOT, L"CLSID", &thk); if (ERROR_SUCCESS == status) { status = ERROR_SUCCESS == status?RegCreateKeyExW( thk, // 父键句柄 CLSID_CSayHelloStr,//子键名称 0, // 保留,必须为 0 nullptr, // 类名(通常为 nullptr) REG_OPTION_NON_VOLATILE, // 选项(非易失性) KEY_ALL_ACCESS, // 访问权限 nullptr, // 安全属性(通常为 nullptr) &tclsidk, // 返回的子键句柄 nullptr): status; HKEY tinps32k = NULL; status = ERROR_SUCCESS == status?RegCreateKeyExW( tclsidk, L"InprocServer32", 0, // 保留,必须为 0 nullptr, // 类名(通常为 nullptr) REG_OPTION_NON_VOLATILE, // 选项(非易失性) KEY_ALL_ACCESS, // 访问权限 nullptr, // 安全属性(通常为 nullptr) &tinps32k, // 返回的子键句柄 nullptr):status; status = ERROR_SUCCESS == status?RegSetValueW(tinps32k, NULL, REG_SZ, lpPath, wcslen(lpPath)): status; RegCloseKey(tinps32k); RegCloseKey(tclsidk); RegCloseKey(thk); } //在键HKEY_CLASSES_ROOT下创建新键为COMCTL.CComTest, //在该键下创建子键,并将CCompTest的CLSID写为该键的默认值 HKEY tprogk = NULL, tprogidk = NULL; status = status == ERROR_SUCCESS ? RegCreateKeyW(HKEY_CLASSES_ROOT, L"COMCTL.CSayHello", &tprogk) : status; status = status == ERROR_SUCCESS ? RegCreateKeyW(tprogk, L"CLSID", &tprogidk) : status; status = status == ERROR_SUCCESS ? RegSetValueW(tprogidk, NULL, REG_SZ, CLSID_CSayHelloStr, wcslen(CLSID_CSayHelloStr)) : status; RegCloseKey(tprogk); RegCloseKey(tprogidk); //这样的话一个客户端程序如果想要使用本组件,首先可以以COMCTL.CSayHello为参数调用CLSIDFromProgID函数 //来获取CSayHello的CLSID,再以这个CLSID为参数调用CoCreateInstance创建COM对象 return status; } extern "C" HRESULT __stdcall DllRegisterServer() { WCHAR szModule[1024]; //获取本组件(dll)所在路径 DWORD dwResult = GetModuleFileNameW(g_hModule, szModule, 1024); if (0 == dwResult) { return -1; } //将路径等信息写入注册表 return myReg(szModule); } int myDelKey(HKEY hk, LPCWSTR lp) { if (ERROR_SUCCESS == RegDeleteKeyW(hk, lp)) { } return 0; } //删除注册时写入注册表的信息 int myDel() { HKEY thk; if (ERROR_SUCCESS == RegOpenKeyW(HKEY_CLASSES_ROOT, L"CLSID", &thk)) { wchar_t szKey[256]; // 删除 CLSID 项 StringCchPrintfW(szKey, ARRAYSIZE(szKey), L"%s\\InprocServer32", CLSID_CSayHelloStr); myDelKey(thk, szKey); myDelKey(thk, CLSID_CSayHelloStr); RegCloseKey(thk); } if (ERROR_SUCCESS == RegOpenKeyW(HKEY_CLASSES_ROOT, L"COMCTL.CSayHello", &thk)) { myDelKey(thk, L"CLSID"); } myDelKey(HKEY_CLASSES_ROOT, L"COMCTL.CSayHello"); return 0; } extern "C" HRESULT __stdcall DllUnregisterServer() { //删除注册时写入注册表的信息 myDel(); return 0; } // 用于判断是否可以卸载本组建, 由CoFreeUnusedLibraries函数调用 extern "C" HRESULT __stdcall DllCanUnloadNow(void) { //如果对象个数为0,则可以卸载 if (0 == CSayHello::g_ObjNum) { 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_CSayHello == rclsid) { ComFactory* pFactory = new ComFactory();//创建工厂对象 if (NULL == pFactory) return E_OUTOFMEMORY; HRESULT result = pFactory->QueryInterface(riid, ppv);//获取工厂对象(内部校验一下) if (S_OK != result) { //pFactory->Release(); delete pFactory; } return result; } else { return CLASS_E_CLASSNOTAVAILABLE; } } // 提供DLL入口;对于动态链接库,DllMain是一个可选的入口函数,在COM组件中是必须有的 extern "C" 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; }导出符号定义:
// Module.def LIBRARY SayHello DESCRIPTION "SayHello Com DLL" EXPORTS DllMain @1 DllRegisterServer @2 DllUnregisterServer @3 DllCanUnloadNow @4 DllGetClassObject @5四 客户端调用(用作com组件)
#include <iostream> #include "CSayHelloModule.h" #include "SayHello.h" #include <stdio.h> //#pragma comment(lib,"SayHello.lib") class MyCB :public INotifyCallback { public: STDMETHOD(OnCallback) () { printf("Notify Triggered.\n"); return S_OK; } }; ////Com 接口可传递的参数: 基本类型[char,int,float...](不包括基本类型数组和指针),POD结构体,IUnknown类型指针, //// BSTR(带长度前缀的字符串), SAFEARRAY(可变长数组),VARIANT(变体类型) 【#include <atlsafe.h>】 ////BSTR,SAFEARRAY,VARIANT需要管理生命周期,为了简化可直接使用ATL封装的相关类,它们使用RAII技术进行自动管理 //CComBSTR bstrVal(L"World"); //CComSafeArray<long> arr(5); //CComVariant varStr(L"Hello"); //CComVariant varArr(arr); // //bstrVal.m_str;//BSTR //arr.m_psa;//SAFEARRAY //SAFEARRAY n; //varStr.bstrVal;//BSTR, varStr.vt == VT_ARRAY| VT_I4; //varArr.parray;//SAFEARRAY, varArr.vt == VT_BSTR; //VARIANT* pVar1 = &varStr;//VARIANT //如果COM组件想提供Automation接口给JS/VB/Python等脚本语言使用,则接口参数要求更加严格,只能有基本类型,IDispatch*,BSTR,SAFEARRAY,VARIANT int main() { // 初始化COM库 CoInitialize(NULL); SayHelloInterface* pComTest = NULL; HRESULT hResult = ERROR_SUCCESS; hResult = DllRegisterServer();//要求管理员权限。可以在安装软件的时候,拿到管理员权限,执行命令:regsvr32 SayHello.dll // 创建进程内COM组件,返回指定接口 if(hResult == ERROR_SUCCESS) { hResult = CoCreateInstance(CLSID_CSayHello, NULL, CLSCTX_INPROC_SERVER, IID_ISayHello, (void**)&pComTest); //CoCreateInstance等效以下步骤 //IClassFactory* pFac = NULL; //CoGetClassObject(CLSID_CSayHello,CLSCTX_INPROC_SERVER,NULL,IID_IClassFactory,(void**)&pFac);//会去加载dll库 //pFac->CreateInstance(NULL, IID_ISayHello, (void**)&pComTest); //pFac->Release(); } if (S_OK == hResult) { // 调用接口方法 MyCB cb = {}; pComTest->SetCallback(&cb); printf("%d\r\n", pComTest->SayHello()); // 释放组件 pComTest->Release(); } else printf("Error Occurs: %d\r\n", hResult); // 释放COM库 DllUnregisterServer();//要求管理员权限。等价于:regsvr32 /u SayHello.dll CoUninitialize(); system("pause"); return 0; }五. 客户端调用(用作普通组件)
用作普通组件时,可以脱离windows系统,变成一种通用的组件模型。
#include <iostream> #include "CSayHelloModule.h" #include "SayHello.h" #include <stdio.h> #pragma comment(lib,"SayHello.lib") int main() { IClassFactory* pComFac = NULL; ICalc* pComTest = NULL; HRESULT hResult = 0; hResult = DllGetClassObject(CLSID_CSayHello, IID_IClassFactory,(void**)&pComFac); if (S_OK == hResult) { pComFac->CreateInstance(NULL, IID_ISayHello, (void**)&pComTest); // 调用接口方法 printf("Read Ret: %d\r\n", pComTest->SayHello()); pComTest->Release(); pComFac->Release(); } else printf("Error Occurs: %d\r\n", hResult); system("pause"); return 0; }六. Reg Free Com
上面小节4中,需要借助注册表存储GUID 和组件路径作为元数据,但是这种做法会污染操作系统注册表,在安装软件时需要获取管理员权限,将元数据信息写入注册表,对于一些敏感的用户,这种操作是不被允许的。WinXp以后,可以借助文件清单(.manifest)来管理COM组件,当调用CoCreateInstance时,会先从文件清单中寻找Com组件,如果找不到就去注册表中找,同时兼容传统借助注册表实现的com 组件应用,无需做代码上的改动,很方便。需要组件提供方除了提供动态库dll外,同时提供minifest 文件,放到exe同级目录,并在客户端的manifest文件中将所有依赖的COM组件信息列出来,如下方截图。(需要注意的是,如果客户端依赖了COM组件A,而COM组件A依赖了其他的COM组件B,则客户端的manifest文件中,除了列出COM组件A的信息,也要列出COM组件B)。如果选择了隐式嵌入,客户端只会有exe生成,否则还会有一个xxx.exe.manifest。(隐式嵌入的话可以使用: "C:\Program Files (x86)\Windows Kits\10\bin\x64\mt.exe" -inputresource:xxx.exe;#1 -out:exported.manifest 去查看嵌入的清单信息)