美文网首页
COM学习(一)——COM基础思想

COM学习(一)——COM基础思想

作者: 一叶障目 | 来源:发表于2017-10-29 20:57 被阅读111次

    概述

    学习微软技术COM是绕不开的一道坎,最近做项目的时候发现有许多功能需要用到COM中的内容,虽然只是简单的使用COM中封装好的内容,但是许多代码仍然只知其然,不知其所以然,所以我决定从头开始好好学习一下COM基础的内容,因此在这记录下自己学习的内容,以便日后参考,也给其他朋友提供一点学习思路。
    COM的全称是Component Object Module,组件对象模型。组件就我自己的理解就是将各个功能部分编写成可重用的模块,程序就好像搭积木一样由这些可重用模块构成,这样将各个模块的耦合降到最低,以后升级修改功能只需要修改某一个模块,这样就大大降低了维护程序的难度和成本,提高程序的可扩展性。COM是微软公司提出的组件标准,同时微软也定义了组件程序之间进行交互的标准,提供了组件程序运行所需的环境。
    COM是基于组件化编程的思想,在COM中每一个组件成为一个模块,它可以是动态链接库或者可执行文件,一个组件程序可以包含一个或者多个组件对象,COM对象不同于OOP(面向对象)中的对象,COM对象是定义在二进制机器代码基础之上,是跨语言的。而OOP中的对象是建立在语言之上的。脱离了语言对象也就不复存在.COM是独立在编程语言之上的,是语言无关的。COM的这一特性使得不同语言开发的组件之间的互相交互成为可能。

    COM对象和接口

    COM中的对象类似于C++中的对象,对象是某个类中的实例。而类则是一组相关的数据和功能组合在一起的一个定义。使用对象的应用(或另一个对象)称为客户,有时也称为对象的用户。
    接口是一组逻辑相关的函数的集合,比如一组处理URL的接口,处理HTTP请求的接口等等。在习惯上接口通常是以"I"开头。对象通过接口成员函数为客户提供各种形式的服务。一个对象可以拥有多个不同的接口,以表现不同的功能集合。 在C++语言中,一个接口就是一个虚基类,而对象就是该接口的实现类,派生自该接口并实现接口的功能。

    class IBook
    {
    public:
        virtual void NextPage() = 0;
        virtual void ForwardPage() = 0; 
    }
    
    class IAppliances
    {
    public:
        virtual void charge() = 0;
        virtual void shutdown() = 0;
    }
    
    class CKindle: public IBook, IAppliances
    {
    public:
        virtual void NextPage();
        virtual void ForwardPage(); 
        virtual void charge();
        virtual void shutdown();
    }
    

    就像上面的例子,上面的例子中提供了一个书本的接口,书本可以翻到上一页,下一页,而电器有充电和关机的接口,最后我们利用kindle这个类来实现这两个接口。所以在使用上我们可以利用下面的伪代码来使用

    pInterface = CreateInterface(ID_IBOOK, ID_KINDLE);
    pInterface->NextPage();
    if(Late())
    {
        pInter2 = pInterface->QueryInterface(ID_APPLIANCES);
        pInter2->shutdown();
    }
    

    在平时我们使用kindle的翻页功能来看书,因为翻页功能在接口IBook,所以首先调用一个创建接口的函数,传入对应接口以及接口实现类的标识,用来生成相应的接口,其实在内部也就是根据类ID来创建一个对应的实现类的实例。然后根据需要转化为对应基类的指针。在看书看累的时候,将接口转化为电子产品的接口,调用对应的关机功能,关闭电子书。在之后比如说kindle进行了升级,也就是重写了实现这些接口的代码,但是接口原型不变,这样使用接口的代码不用改变,也就是说即使kindle对内部进行了升级,优化某些功能,用户在使用上仍然是那样在用,不必改变使用习惯。再比如kindle出了一个新款,提供了背光功能,这个时候可能提供一个新接口:

    class IAppliances2 : public IAppliances
    {
    public:
        virtual void Light() = 0;
    }
    

    然后只需要稍微更新一下CKindle这个实现类,新增一个Light接口的实现,在使用上如果不用背光功能原来的代码就够用了,如果要使用背光功能,只需要将原来的接口类型改为IAppliances2 ,并且添加调用背光功能的函数,而其余的功能也不变,这与实际生活相似,某个产品提供新功能时,一般保持原始功能的使用方法不变,新功能会有新的按钮或者其他方法进行打开。
    再比如说我不想用kindle了改用其他的电子阅读器,只要接口不变,我的使用方法基本不变,唯一改变的可能是我以前拿着kindle,现在拿着其他品牌的阅读器,也就是说可能要改变传入CreateInterface函数中的类标识。

    COM基本接口

    COM中所有接口都派生自该接口:

    struct IUnknown
    {
        virtual HRESULT QueryInterface(REFIID riid,void **ppvObject) = 0;
        virtual ULONG AddRef( void) = 0;
        virtual ULONG Release( void) = 0;
    };
    

    所有类都应该实现上述三个方法,AddRef主要将接口的引用计数+1, 而Release则是将引用计数 -1,当对象的引用计数为0,则会调用析构函数,释放对象的存储空间。每一次接口的创建和转化都会增加引用计数,而每次不再使用调用Release,都会把引用计数 -1,当引用计数为0时会释放对象的空间。
    QueryInterface主要用来进行接口转化,将对象的指针转化为另外一个接口的指针,就好像上面例子中pInter2 = pInterface->QueryInterface(ID_APPLIANCES);这句代码将之前的Ibook接口转化为电子产品的接口。在C++中也就是做了一次强制类型转化。

    对象和接口的唯一标识

    在COM中,对象本身对于客户来说是不可见的,客户请求服务时,只能通过接口进行。每一个接口都由一个128位的全局唯一标识符(GUID,Global Unique Identifier)来标识。客户通过GUID来获得接口的指针,再通过接口指针,客户就可以调用其相应的成员函数。与接口类似,每个组件也用一个 128 位 GUID 来标识,称为 CLSID(class identifer,类标识符或类 ID),用 CLSID 标识对象可以保证(概率意义上)在全球范围内的唯一性。
    实际上,客户成功地创建对象后,它得到的是一个指向对象某个接口的指针,因为 COM 对象至少实现一个接口(没有接口的 COM 对象是没有意义的),所以客户就可以调用该接口提供的所有服务。根据 COM 规范,一个 COM 对象如果实现了多个接口,则可以从某个接口得到该对象的任意其他接口。
    由此可看出,客户与 COM 对象只通过接口打交道,对象对于客户来说只是一组接口。
    在COM中GUID的定义如下:

    typedef struct _GUID {
        unsigned long  Data1;
        unsigned short Data2;
        unsigned short Data3;
        unsigned char  Data4[ 8 ];
    } GUID;
    

    一般我们在程序中只是作为一个标志来使用,并不对它进行特别的操作。生成它一般是使用VS自带的GUID生成工具。
    而CLSID的定义如下:

    typedef GUID CLSID;
    
    函数 功能
    IsEqualGUID 判断GUID是否相等
    IsEqualCLSID 判断CLSID是否相等
    IsEqualIID 判断IID是否相等
    CLSIDFromProgID 把字符串形式的CLSID转化为CLSID结构形式(类似于将字符串的234转化为数字,也是把字面上的CLSID转化为计算机能识别的CLSID)
    StringFromCLSID 把CLSID转化为字符串形式
    IIDFromString 把字符串形式的IID转化为IID接口形式
    StringFromIID 把IID结构转化为字符串
    StringFromGUID2 把GUID形式转化为字符串形式

    其实在COM中一般涉及到ID的都是GUID,只是利用typedef另外定义了一个名称而已
    另外COM也提供了一组函数用来对GUID进行操作:

    函数 功能
    IsEqualGUID 判断GUID是否相等
    IsEqualCLSID 判断CLSID是否相等
    IsEqualIID 判断IID是否相等
    CLSIDFromProgID 把字符串形式的CLSID转化为CLSID结构形式(类似于将字符串的234转化为数字,也是把字面上的CLSID转化为计算机能识别的CLSID)
    StringFromCLSID 把CLSID转化为字符串形式
    IIDFromString 把字符串形式的IID转化为IID接口形式
    StringFromIID 把IID结构转化为字符串
    StringFromGUID2 把GUID形式转化为字符串形式

    COM接口的一般使用步骤

    一般使用COM中的时候首先使用CoInitialize初始化COM环境,不用的时候使用CoUninitialize卸载COM环境,在使用接口中一般需要进行下面的步骤

    1. 调用CoCreateInstance函数传入对应的CLSID和对应的IID,生成对应对象并传入相应的接口指针。
    2. 使用该指针进行相关操作
    3. 调用接口的QueryInterface函数,转化为其他形式的接口
    4. 在最后分别调用各个接口的Release函数,释放接口
      下面提供一个小例子,以供参考,也方便更好的理解COM
    //组件部分
    extern "C" __declspec(dllexport) void __stdcall ComCreateObject(GUID clsID, GUID interfaceID, void** pObj);
    void __stdcall ComCreateObject(GUID clsID, GUID interfaceID, void** pObj)
    {
        if (clsID == CLSID_COMSTRING)
        {
            CComString *pComObject = new CComString;
            *pObj = pComObject->QueryInterface(interfaceID);
        }
    }
    
    class IComBase
    {
    public:
        virtual void* QueryInterface(GUID gInterfaceId) = 0;
        virtual void AddRef() = 0;
        virtual void Release() = 0;
    };
    
    static const GUID IID_ICOMSTRING = { 0xb2fcd22c, 0x63fa, 0x4f61, { 0xbf, 0x12, 0xd3, 0xd2, 0x5a, 0x99, 0x59, 0x24 } };
    class IComString : public IComBase
    {
    public:
        virtual void Init(LPCTSTR pStr) = 0;
        virtual int Find(LPCTSTR lpSubStr) = 0;
        virtual int GetLength() = 0;
    };
    
    static const GUID CLSID_COMSTRING = { 0xf57f3489, 0xff2d, 0x4c97, { 0xb1, 0xf6, 0xc, 0x60, 0x7e, 0xf7, 0xae, 0xfc } };
    
    class CComString : public IComString
    {
    public:
        virtual void* QueryInterface(GUID gInterfaceId);
        virtual void AddRef();
        virtual void Release();
    
        virtual void Init(LPCTSTR pStr);
        virtual int Find(LPCTSTR lpSubStr);
        virtual int GetLength();
    
    protected:
        int m_nCnt = 0;
        CString m_csString;
    };
    
    //cpp
    void* CComString::QueryInterface(GUID gInterfaceId)
    {
        if (gInterfaceId == IID_ICOMSTRING)
        {
            //该接口的引用计数+1
            AddRef();
            return dynamic_cast<IComString*>(this);
        }
        //如果它还实现了其他接口,可以再写判断,生成其他类型的接口 
        return NULL;
    }
    
    void CComString::AddRef()
    {
        m_nCnt++;
    }
    
    void CComString::Release()
    {
        m_nCnt--;
        //引用计数为0,此时没有该类的接口被使用,应该释放该类
        if (m_nCnt == 0)
        {
            delete this;
        }
    }
    
    void CComString::Init(LPCTSTR pStr)
    {
        m_csString = pStr;
    }
    
    int CComString::Find(LPCTSTR lpSubStr)
    {
        return m_csString.Find(lpSubStr);
    }
    
    int CComString::GetLength()
    {
        return m_csString.GetLength();
    }
    

    这些代码被封装在一个dll中,dll中导出一个函数ComCreateObject,外部在使用时调用该函数传入对应的ID,以便生成对应的接口。
    在这个dll里面提供一个接口的基类IComBase,这个是仿照了COM种的IUnknow基类,另外定义了一个IComString字符串的接口,同时定义了它的实现类CComString,为了简单,它的功能方法我直接使用了一个CString类实现。
    在函数ComCreateObject,会根据传入对应的类ID,来生成对应的类实例,然后调用实例的QueryInterface,转化成对应的接口,在实现类中实现了这个方法,实现类中的QueryInterface方法主要完成了类型转化并将引用计数+1。
    而Release函数在每次-1的时候会进行判断,当引用计数为0时销毁该类的实例
    由于类是new出来创建在堆上的,所以每次用完一定要记得调用Release释放,否则会造成内存泄露
    注意:在使用这里使用的是dynamic_cast进行类型转化,在进行类的强制类型转化时,特别是在有多重继承的情况下,最好使用dynamic_cast方式进行转化,当一个类拥有多个基类时,类中有多个虚函数表,为了能正常找到对应的虚函数表,就需要进行对应的偏移量的计算,C中的强制类型转化是直接将对象的首地址进行转化,这样在寻址虚函数表时可能会出错。而dynamic_cast会进行对应的计算。详细情形请参考这里
    在使用上

    void ComInitialize();
    void ComUninitialize();
    typedef void(__stdcall *pfnCreateInstance)(GUID, GUID, void**);
    
    pfnCreateInstance CreateInstance;
    HMODULE hComDll = NULL;
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        ComInitialize();
        IComString *pIString = NULL;
        CreateInstance(CLSID_COMSTRING, IID_ICOMSTRING, (void**)&pIString);
        pIString->Init(_T("Hello World"));
        IComString* pIString2 = (IComString*)(pIString->QueryInterface(IID_ICOMSTRING));
        int nLength = pIString2->GetLength();
        int iPos = pIString2->Find(_T("World"));
    
        printf("%d, %d\n", nLength, iPos);
        pIString->Release();
        pIString2->Release();
        return 0;
    }
    
    void ComInitialize()
    {
        hComDll = LoadLibrary(_T("ComInterface.dll"));
        if (NULL != hComDll)
        {
            CreateInstance = (pfnCreateInstance)GetProcAddress(hComDll, "ComCreateObject");
        }
    }
    
    void ComUninitialize()
    {
        FreeLibrary(hComDll);
    }
    

    给使用者使用时只需要提供对应类和接口的GUID,然后将函数ComCreateObject原型提供给调用者,以便生成对应的接口。
    这里为了模仿COM的使用定义了ComInitialize和ComUninitialize这两个函数,真实的初始化函数怎么写的,我也不知道,在这里只是为了模仿COM的使用。
    至此相信各位小伙伴应该对COM有了一个初步的了解

    相关文章

      网友评论

          本文标题:COM学习(一)——COM基础思想

          本文链接:https://www.haomeiwen.com/subject/fbcupxtx.html