大家好,欢迎来到IT知识分享网。
一、COM介绍
com组件是Component Object Model(组件对象模型)的缩写。为了减少重复造轮子,人们在开发过程中提出了“接口”这一概念,将部分通用的功能进行抽象,由具体的实例实现具体的操作。com的核心思想也是类似的,不过与C++的接口不同的是,它的重用是在源码级别的,会受到由于不同语言、不同环境各种影响;而com组件是各种各样的二进制可执行文件,它与平台、语言无关,通过com库提供的接口,即可对com组件进行调用。下面我们就来学习如何使用本地com服务器的com组件,并在第二阶段尝试着自己注册一个二进制Dll文件并调用它。
二、com的一些基本元素、概念
1.coclass(组件对象类):包含一个或多个接口的代码,被包含在二进制文件。
2.COM服务器:包含了一个或多个coclass的二进制文件。
3.GUID:全球唯一标识符,是128位的二进制。这是独立于com开发的标示方法,这也意味着任何编程语言都可对它进行处理,为每一个接口和coclass分配一个GUID,可以避免名字冲突。
4.HRESULT:com接口被调用时的返回值。
5.所有的COM接口都直接或间接地继承IUnKnown,这个接口提供了内存管理及接口查询(此处后文再重写QueryInterface()函数时会有介绍)的功能。
6.com组件的内存管理采用引用计数的方式,即对当前调用的接口及对象都保存一个count值,当值为0时,可以用IUnKnown的Release()函数将其从内容中释放,每当有一个新指针获取接口和对象时,该值++。
三、com对象的基本使用
1.使用前的初始化
和某些高级语言在定义变量时需要初始化一样,在使用com库前,我们也需要对其进行初始化,这里要用到其自带的CoInitialize()函数(objbase.h库中),它有一个保留参数,使用时传入NULL即可。
2.调用com库的接口
使用CoCreateInstance()函数,原型如下:
HRESULT CoCreateInstance( [in] REFCLSID rclsid, [in] LPUNKNOWN pUnkOuter, [in] DWORD dwClsContext, [in] REFIID riid, [out] LPVOID *ppv );
第一个参数为实例对象所调用coclass的GUID;第二个参数如果为NULL,则指示对象未作为聚合的一部分创建。 如果为非NULL,则指向聚合对象的 IUnknown 接口的指针 (控制 IUnknown) ;第三个参数用于管理创建的新对象与将在其中运行的上下文。具体可查阅CLSCTX;第四个参数是调用该coclass的接口的GUID;第五个是实例化对象的指针(作为返回值)。
3.创建好实例后,调用接口内的函数。
其实在具体实现上,和调用类的函数没有什么两样,后面会用示例展示。
4.实例指针的释放
和C++使用new开辟空间后需使用delete进行空间释放一样;函数运行完成后,调用的com组件若不释放,仍会占用内存。此时可以调用Release()函数对其进行释放。(注:所有com接口都继承IUNKNOWN)。
5.COM库的关闭
程序结束时,需使用与初始化对应(类似于C++中类的析构函数)的函数CoUninitialize(),函数作用是关闭当前线程上的 COM 库,卸载线程加载的所有 DLL,释放线程维护的任何其他资源,并强制关闭线程上的所有 RPC 连接。
四、使用com组件示例
下面是使用一些com组件实现用对话框展示当前打开文件的路径的案例,借鉴自文章。
总体的步骤如下:打开文件对话框,判断是否打开文件,若打开则记录文件路径,用对话框将获取的路径展示。用到的组件主要有:IFileOpenDialog、IShellItem、MessageBow,依次组件的作用为:打开文件对话框并打开文件、获取文件路径、将路径信息展示至对话框中,具体作用可可通过上述链接进行查看。
下面代码的意思是实例化一个调用IFileOpenDialog接口的指针,其中CLSID_FileOpenDialog结合IID_IFileOpenDialog是设定好的。
IFileOpenDialog *pFileOpen; hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast<void>(&pFileOpen));
如何判断操作有没有成功呢?这里用hr接收这个返回值,并用宏SUCCEED()对其进行判断。若操作成功,则可用实例指针进行下一步操作,操作模式为:
hr = //使用接口 if (SUCCEEDED(hr)){ //之前的操作成功,下一步操作 } else{ //某些错误发生,对错误进行处理 }
2.在这个实例中,若调用成功,则需要展示出文件选择的对话框,展示出对话框后,需判断是否打开文件,一个个的嵌套条件语句。代码如下:
if (SUCCEEDED(hr)) { // 打开文件选择对话框 hr = pFileOpen->Show(NULL); if (SUCCEEDED(hr)) { //下面选择文件 IShellItem *pItem; //获取行为结果,即是否选择文件 hr = pFileOpen->GetResult(&pItem); if (SUCCEEDED(hr)) { //储存文件路径的字符串 PWSTR pszFilePath; //调用IShellItem接口的GetDisplayName获取路径名 hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); //用MessageBox,将获取的结果展示到对话框 if (SUCCEEDED(hr)) { //设置对话框的格式 MessageBox(NULL, pszFilePath, L"File Path", MB_OK); //注意,前面我们用pszFilePath指向了函数返回的存放打开文件路径的指针,在使用完后需要手动释放 CoTaskMemFree(pszFilePath); } //释放pItem实例 pItem->Release(); } } //释放pFileOpen实例 pFileOpen->Release(); }
此外:这里本人使用的IDE是VScode,gcc版本为gcc version 8.1.0 (x86_64-win32-seh-rev0, Built by MinGW-W64 project),我直接编译是不成功的,提示“’IID_IFileOpenDialog’ was not declared in this scope”,原因在MinGW会将NTDDT_VERSION的值设置为0x0是MinGW会将NTDDT_VERSION的值设置为0x0MinGW会将NTDDT_VERSION的值设置为0x0,需将其修改,参考文章;若出现提示“undefined reference to _imp__CoInitializeundefined reference to _imp__CoInitialize”,则为未链接ole32库,需在编译时手动链接,参考文章,在下文注释里有。
整体代码如下:
#define NTDDI_VERSION 0x0A000006 //NTDDI_WIN10_RS5 #define _WIN32_WINNT 0x0A00 // _WIN32_WINNT_WIN10, the _WIN32_WINNT macro must also be defined when defining NTDDI_VERSION #include <windows.h> #include <shobjidl.h> #include <iostream> #include <stdlib.h> /* g++ -static-libgcc -static-libstdc++ test.cpp -o test.exe -lole32 -luuid */ int main(){ //初始化com库 HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (SUCCEEDED(hr)) { // 创建调用IOpenDialog接口的实例,pFileOpen,是返回的实例指针 IFileOpenDialog *pFileOpen; hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast<void>(&pFileOpen)); if (SUCCEEDED(hr)) { // 打开文件选择对话框 hr = pFileOpen->Show(NULL); if (SUCCEEDED(hr)) { //下面选择文件 IShellItem *pItem; //获取行为结果,即是否选择文件 hr = pFileOpen->GetResult(&pItem); if (SUCCEEDED(hr)) { //储存文件路径的字符串 PWSTR pszFilePath; //调用IShellItem接口的GetDisplayName获取路径名 hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath); //用MessageBox,将获取的结果展示到对话框 if (SUCCEEDED(hr)) { //设置对话框的格式 MessageBox(NULL, pszFilePath, L"File Path", MB_OK); //注意,前面我们用pszFilePath指向了函数返回的存放打开文件路径的指针,在使用完后需要手动释放 CoTaskMemFree(pszFilePath); } //释放pItem实例 pItem->Release(); } else { } } //释放pFileOpen实例 pFileOpen->Release(); } //关闭当前线程的com库,并释放加载的DLL文件 CoUninitialize(); } return 0; }
在初步了解com组件后,我们进一步深入学习,下面我们学习如何自己编写一个com组件,并将其注册至注册表中,通过本地的com服务器对其进行调用,在控制台中打印”生成组件的CLSID和helloworld”。
五、用C++编写自己的com组件,注册并使用
在初步了解com组件后,我们进一步深入学习,下面我们学习如何自己编写一个com组件,并将其注册至注册表中,通过本地的com服务器对其进行调用,在控制台中打印”生成组件的CLSID和helloworld”。
1.工程文件Mydemo结构:
总体分为两个部分:1.服务端工程CompTest:生成一个dll文件,手动注册CompTestClass组件 2.客户端工程CtrlTest:生成一个exe文件,使用本地的com服务器对注册的组件的接口进行调用、输出注册组件的CLSID以及“hello wolrd”。
第2个步骤的调用细节在上文有介绍,这里不再细说,主要讲讲第1个步骤。
2.文件执行流程解释:
在实操之前,可以通过此文章了解一下引用计数、类厂、和注册。
3.手动注册CompTest组件步骤:
(1)使用VS的guidgen.exe生成GUID(位于VS/Common7/Tools文件夹下)
(2)重写注册函数
这里我使用的是“regsvr32.exe”对生成的dll进行注册,而实际上这个注册过程仅调用了生成的dll中的DllRegisterSever引出函数,故对其重写。
这个是注册时的核心部分,下面我们只需DllRegisterSever中调用此函数即可,分离实现使代码易懂。
int myReg(LPCWSTR lpPath) //use to register this component to registry, including CLSID, lpPath, ProgID { HKEY thk, tclsidk; //打开键HKEY_CLASSES_ROOT\CLSID,创建新键为CompTestClass的CLSID //此处我生成的CLSID为"{15BAAFA6-C0AA-430F-9C79-3F8860CF77F4}" //在该键下创建键InprocServer32,并将本组件(dll)所在路径lpPath写为该键的默认值 if (ERROR_SUCCESS == RegOpenKey(HKEY_CLASSES_ROOT, L"CLSID", &thk)) { if (ERROR_SUCCESS == RegCreateKey(thk, L"{15BAAFA6-C0AA-430F-9C79-3F8860CF77F4}", &tclsidk)) { HKEY tinps32k; if (ERROR_SUCCESS == RegCreateKey(tclsidk, L"InprocServer32", &tinps32k)) { if (ERROR_SUCCESS == RegSetValue(tinps32k, NULL, REG_SZ, lpPath, wcslen(lpPath) * 2)) { } RegCloseKey(tinps32k); } RegCloseKey(tclsidk); } RegCloseKey(thk); } //在键HKEY_CLASSES_ROOT下创建新键为COMCTL.CompTest, //在该键下创建子键,并将CompTestClass的CLSID写为该键的默认值 //通过此操作,我们通过FrogID找到目标的CLSID,然后再对其进行调用 if (ERROR_SUCCESS == RegCreateKey(HKEY_CLASSES_ROOT, L"COMCTL.CompTest", &thk)) { if (ERROR_SUCCESS == RegCreateKey(thk, L"CLSID", &tclsidk)) { if (ERROR_SUCCESS == RegSetValue(tclsidk, NULL, REG_SZ, L"{15BAAFA6-C0AA-430F-9C79-3F8860CF77F4}", wcslen(L"{15BAAFA6-C0AA-430F-9C79-3F8860CF77F4}") * 2)) { cout << "register success" << endl; } RegCloseKey(tclsidk); } RegCloseKey(thk); } return 0; }
下面是DllRegisterSever函数,负责调用自己重写的注册函数。
extern "C" HRESULT _stdcall DllRegisterServer() { //获取当前模块的文件路径,用于存入注册表中。 WCHAR szModule[1024]; DWORD dwResult = GetModuleFileName(g_hModule, szModule, 1024); if (0 == dwResult) { return -1; } MessageBox(NULL, szModule, L"path", MB_OK); //调用重写的reg函数进行注册 myReg(szModule); return 0; }
同理,对于此Dll文件,有注册,那么肯定也需要有一个函数,可将文件从注册表中移除,这里主要设计到DllUnregisterServer()函数和DllCanUnloadNow()函数,过程与当前步骤类似,不作赘述,代码于附件处查询,需要注意的是,此处的内容涉及到com组件引用计数的知识,可以先了解后再重写函数。
注:生成的GUID有可能无效
本人第一次生成的GUID,发现调用CLSIDFromProgID()函数时一直运行不成功,后面调试发现返回错误码为”CO_E_CLASSSTRING“,即ProgID 的已注册 CLSID 无效。此时,需要重新生成一个GUID并重新注册一次。
(3)重写与Dll调用相关的函数及类
com组件注册之后,是如何被调用并将组件加载到内存呢?
前文有提到,当我们初次调用某一组件的接口时,会使用CoCreateInstance()函数,其实它本质上是封装了如下功能
CoGetClassObject(rclsid, dwClsContext, NULL, IID_IClassFactory, &pCF); hresult = pCF->CreateInstance(pUnkOuter, riid, ppvObj); pCF->Release();
这里又出现了一个新函数CoGetClassObject(),它在这的作用是通过注册表找到对于的DLL文件并将其加载至进程,而后调用该DLL的DllGetClassObject函数返回组件所属类厂对象的IClassFactory指针,最后IClassFactory通过调用QueryInterface(),返回确定组件的接口指针。
简而言之,就是要重写DLL文件的DLLGetClassObject方法使得其能被此组件对象类能被成功调用。
此处关于可能出现的错误码”0x”
上文提到,我第一次生成的GUID码无效,于是我又重新生成GUId码注册了一遍,但是在组件类上忘记及时更新此CLSID了。由于我这里使用条件语句,判定只有类中CLSID码与从注册表中获取的CLSID码比对成功,类厂才能调用此组件,否则会返回错误码”CLASS_E_CLASSNOTAVAILABLE“,即十六进制下的”0x”,意为未在注册表中找到此CLSID对应的注册类。
extern "C" HRESULT _stdcall DllGetClassObject(__in REFCLSID rclsid, __in REFIID riid, LPVOID FAR * ppv) { if (CLSID_CompTestClass == rclsid)//注册用的CLSID被保存,用来检验客户端传入的CLSID是否正确 { CompTestFactory* pFactory = new CompTestFactory(); if (NULL == pFactory) { //创建后指针认为空指针,可能出现了越界错误。 return E_OUTOFMEMORY; } //创建成功,返回客户端传入的目的接口 HRESULT result = pFactory->QueryInterface(riid, ppv); return result; } else { //若不正确,此处会返回错误码:0x,并随后在客户端输出此错误码 return CLASS_E_CLASSNOTAVAILABLE; } }
那么除了重写DllGetClassObject之外,根据上面描述的CoCreateInstance()函数的执行过程,我们还需要自己实现一个Factory类,用它来创建com对象
,本质上就是重写一下它所继承的IClassFactory接口内的函数。
代码直接从附件处查询,此处就不赘述了。
下面分别给出factory.h和factory.cpp的代码。
(4)类厂代码
factory.h
#pragma once #include <Unknwnbase.h> class CompTestFactory : public IClassFactory { public: CompTestFactory(); ~CompTestFactory(); virtual HRESULT _stdcall QueryInterface(const IID& riid, void ppvObject); virtual ULONG _stdcall AddRef(); virtual ULONG _stdcall Release(); virtual HRESULT _stdcall CreateInstance(IUnknown* pUnknown, const IID& riid, void ppvObject); virtual HRESULT _stdcall LockServer(BOOL fLock); protected: ULONG m_Ref; };
factory.cpp
#include "factory.h" #include "CompTestClass.h" CompTestFactory::CompTestFactory() { m_Ref = 0; } CompTestFactory::~CompTestFactory() { } HRESULT _stdcall CompTestFactory::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 CompTestFactory::AddRef() { m_Ref++; return m_Ref; } ULONG _stdcall CompTestFactory::Release() { m_Ref--; if (0 == m_Ref) { delete this; return 0; } return m_Ref; } HRESULT _stdcall CompTestFactory::CreateInstance(IUnknown* pUnkOuter, const IID& riid, void ppvObject) { if (NULL != pUnkOuter) { return CLASS_E_NOAGGREGATION; } HRESULT hr = E_OUTOFMEMORY; CompTestClass::Init(); CompTestClass* pObj = new CompTestClass(); if (NULL == pObj) { return hr; } hr = pObj->QueryInterface(riid, ppvObject); if (S_OK != hr) { delete pObj; } return hr; } HRESULT _stdcall CompTestFactory::LockServer(BOOL fLock) { return NOERROR; }
基本就是实现继承接口的方法。
上文提到的IUnKnown有查询接口的作用,此处解释。
这里我觉得比较值得有意思的点是QueryInterface()函数,这是IUnKnown的方法,前面我们提到所有的COM接口都直接或间接地继承IUnKnown。因此,在实例化一个类时,我们一般都先调用该类的IUnKnown接口(因为其一定存在),那么问题来了,如何拿到我想要使用的哪个接口呢?很简单,提供要使用接口的IID,使用QueryInterface()函数让其返回该接口的指针就好了。那么它到底是如何实现的呢?更简单,使用条件语句将IID和接口地址一一对应即可。如下面的代码:
HRESULT _stdcall CompTestFactory::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; }
此函数还能验证你提供的IID是否存在于此类中,也即没有找到匹配的IID就置传入的接收指针为空,并返回错误码。这篇文章对QueryInterface()函数也作了很通俗的解释,本人也从中获益匪浅。
此DLL文件是用def文件进行导出的,把会使用的到几个函数名写上即可。更多细节可参考此文。
(5)def文件
LIBRARY "CompTest" EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE DllUnregisterServer PRIVATE DllRegisterServer PRIVATE
(5)HelloWorld接口头文件如下:
(注:IID是使用Guidgen生成的,详情看上文CLSID的注册)
#pragma once #include <Unknwn.h> // {54D9F00E-9866-49E8-9B13-5A39D45D180A} static const GUID IID_ICompTest = { 0x54d9f00e, 0x9866, 0x49e8, {0x9b, 0x13, 0x5a, 0x39, 0xd4, 0x5d, 0x18, 0xa} }; class ICompTest : public IUnknown { public: virtual char* _stdcall HelloWorld() = 0; };
HelloWorld函数具体的重写代码请在附件处查看。
补充一些细节:
本人使用的编译器是VS2022,与正常编译exe文件不同,要正确生成Dll还需修改一些设置。
保存使用Ctrl+B运行生成即可。
那么最后就是编写一个接口,包含sayhello这个方法,同时令上面写好的组件类继承该接口并实现即可。
六、学习体会及附件
本人也是刚刚接触com组件,还有很多东西可能理解不算透彻,仅以此文用于学习的记录于整理,若有错误请多多包涵。此外,这个demo仅是单线程,待进一步的学习后,可以尝试多进程下com组件的编写。这个demo比单线程的demo会多一个“锁”。假设这么一个场景,线程1调用了某接口,线程2也调用了此接口,当线程1调用完毕后,此接口引用计数–,为0时我们手动释放,这个过程如果不连贯,则会导致,在手动释放之前,线程2先拿到此时接口的地址,而当它调用接口时实际上接口已经被释放了却不知情,导致出现越界。
附件
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/114302.html


