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

概述

学习微软技术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有了一个初步的了解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容