COM组件学习

  • Post author:
  • Post category:其他


一、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的值设置为0x05020000

是MinGW会将NTDDT_VERSION的值设置为0x05020000

MinGW会将NTDDT_VERSION的值设置为0x05020000

,需将其修改,参考

文章

;若出现提示“undefined reference to _imp__CoInitialize

undefined 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方法使得其能被此组件对象类能被成功调用。

此处关于可能出现的错误码”0x80040154″

上文提到,我第一次生成的GUID码无效,于是我又重新生成GUId码注册了一遍,但是在组件类上忘记及时更新此CLSID了。由于我这里使用条件语句,判定只有类中CLSID码与从注册表中获取的CLSID码比对成功,类厂才能调用此组件,否则会返回错误码”CLASS_E_CLASSNOTAVAILABLE“,即十六进制下的”0x80040154″,意为未在注册表中找到此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
    {
//若不正确,此处会返回错误码:0x80040111,并随后在客户端输出此错误码
        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先拿到此时接口的地址,而当它调用接口时实际上接口已经被释放了却不知情,导致出现越界。


附件



版权声明:本文为weixin_51304431原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。