细谈com编程----vc++篇,我的笔记

   com技术编程,需要安装vc+ 6.0,新建项目。

first,用VC进行COM编程,当然要理解com编程知识,


COM编程概述
   软件工程发展到今天,从一开始的结构化编程,到面向对象编程,再到现在的COM,编程,目
标只有一个,就是希望软件能象积方块一样是累起来的,是组装起来的,而不是一点点编出来的。
结构化编程是函数块的形式,通过把一个软件划分成许多模块,每个模块完成各自不同的功
能,尽量做到高内聚低藕合,这已经是一个很好的开始,我们可以把不同的模块分给不同的人去
做,然后合到一块,这已经有了组装的概念了。软件工程的核心就是要模块化,最理想的情况就
是100%内聚0%藕合。整个软件的发展也都是朝着这个方向走的。结构化编程方式只是一个开始。
下一步就出现了面向对象编程,它相对于面向功能的结构化方式是一个巨大的进步。我们知道整
个自然界都是由各种各样不同的事物组成的,事物之间存在着复杂的千丝万缕的关系,而正是靠
着事物之间的联系、交互作用,我们的世界才是有生命力的才是活动的。我们可以认为在自然界
中事物做为一个概念,它是稳定的不变的,而事物之间的联系是多变的、运动的。事物应该是这
个世界的本质所在。面向对象的着眼点就是事物,就是这种稳定的概念。每个事物都有其固有的
属性,都有其固有的行为,这些都是事物本身所固有的东西,而面向对象的方法就是描述出这种
稳定的东西。而面向功能的模块化方法它的着眼点是事物之间的联系,它眼中看不到事物的概念
它只注重功能,我们平常在划分模块的时侯有没有想过这个函数与哪些对象有关呢?很少有人这
么想,一个函数它实现一种功能,这个功能必定与某些事物想联系,我们没有去掌握事物本身而
只考虑事物之间是怎么相互作用而完成一个功能的。说白了,这叫本末倒置,也叫急功近利,因
为不是我们智慧不够,只是因为我们没有多想一步。面向功能的结构化方法因为它注意的只是事
物之间的联系,而联系是多变的,事物本身可能不会发生大的变化,而联系则是很有可能发生改
变的,联系一变,那就是另一个世界了,那就是另一种功能了。如果我们用面向对象的方法,我
们就可以以不变应万变,只要事先把事物用类描述好,我们要改变的只是把这些类联系起来方法
只是重新使用我们的类库,而面向过程的方法因为它构造的是一个不稳定的世界,所以一点小小
的变化也可能导致整个系统都要改变。然而面向对象方法仍然有问题,问题在于重用的方法。搭
积木式的软件构造方法的基础是有许许多多各种各样的可重用的部件、模块。我们首先想到的是
类库,因为我们用面向对象的方法产生的直接结果就是许多的类。但类库的重用是基于源码的方
式,这是它的重大缺陷。首先它限制了编程语言,你的类库总是用一种语言写的吧,那你就不能
拿到别的语言里用了。其次你每次都必须重新编译,只有编译了才能与你自己的代码结合在一起
生成可执行文件。在开发时这倒没什么,关键在于开发完成后,你的EXE都已经生成好了,如果
这时侯你的类库提供厂商告诉你他们又做好了一个新的类库,功能更强大速度更快,而你为之心
动又想把这新版的类库用到你自己的程序中,那你就必须重新编译、重新调试!这离我们理想的
积木式软件构造方法还有一定差距,在我们的设想里希望把一个模块拿出来再换一个新的模块是
非常方便的事,可是现在不但要重新编译,还要冒着很大的风险,因为你可能要重新改变你自己
的代码。另一种重用方式很自然地就想到了是DLL的方式。Windows里到处是DLL,它是Windows
的基础,但DLL也有它自己的缺点。总结一下它至少有四点不足。(1)函数重名问题。DLL里是一
个一个的函数,我们通过函数名来调用函数,那如果两个DLL里有重名的函数怎么办?(2)各编译
器对C++函数的名称修饰不兼容问题。对于C++函数,编译器要根据函数的参数信息为它生成
修饰名,DLL库里存的就是这个修饰名,但是不同的编译器产生修饰的方法不一样,所以你在VC
里编写的DLL在BC里就可以用不了。不过也可以用extern "C";来强调使用标准的C函数特性,关闭

修饰功能,但这样也丧失了C++的重载多态性功能。(3)路径问题。放在自己的目录下面,别人
的程序就找不到,放在系统目录下,就可能有重名的问题。而真正的组件应该可以放在任何地方
甚至可以不在本机,用户根本不需考虑这个问题。(4)DLL与EXE的依赖问题。我们一般都是用隐
式连接的方式,就是编程的时侯指明用什么DLL,这种方式很简单,它在编译时就把EXE与DLL绑
在一起了。如果DLL发行了一个新版本,我们很有必要重新链接一次,因为DLL里面函数的地址可
能已经发生了改变。DLL的缺点就是COM的优点。首先我们要先把握住一点,COM和DLL一样都是
基于二进制的代码重用,所以它不存在类库重用时的问题。另一个关键点是,COM本身也是DLL,
既使是ActiveX控件.ocx它实际上也是DLL,所以说DLL在还是有重用上有很大的优势,只不过我们
通过制订复杂的COM协议,通COM本身的机制改变了重用的方法,以一种新的方法来利用DLL,来
克服DLL本身所固有的缺陷,从而实现更高一级的重用方法。COM没有重名问题,因为根本不是通
过函数名来调用函数,而是通过虚函数表,自然也不会有函数名修饰的问题。路径问题也不复存
在,因为是通过查注册表来找组件的,放在什么地方都可以,即使在别的机器上也可以。也不用
考虑和EXE的依赖关系了,它们二者之间是松散的结合在一起,可以轻松的换上组件的一个新版
本,而应用程序混然不觉。


二、用VC进行COM编程,必须要掌握哪些COM理论知识
我见过很多人学COM,看完一本书后觉得对COM的原理比较了解了,COM也不过如此,可是就
是不知道该怎么编程序,我自己也有这种情况,我经历了这样的阶段走过来的。要学COM的基本
原理,我推荐的书是《COM技术内幕》。但仅看这样的书是远远不够的,我们最终的目的是要学
会怎么用COM去编程序,而不是拼命的研究COM本身的机制。所以我个人觉得对COM的基本原理不
需要花大量的时间去追根问底,没有必要,是吃力不讨好的事。其实我们只需要掌握几个关键概
念就够了。这里我列出了一些我自己认为是用VC编程所必需掌握的几个关键概念。(这里所说的

均是用C语言条件下的COM编程方式)


(1) COM组件实际上是一个 C++类,而接口都是纯虚类。组件从接口派生而来
    我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:
class IObject
{
public:
virtual Function1(...) = 0;
virtual Function2(...) = 0;
....
};
class MyObject : public IObject
{
public:
virtual Function1(...){...}
virtual Function2(...){...}
....
};
看清楚了吗?IObject就是我们常说的接口,MyObject就是所谓的COM组件。切记切记接口都
纯虚类,它所包含的函数都是纯虚函数,而且它没有成员变量。而COM组件就是从这些纯虚
类继承下来的派生类,它实现了这些虚函数,仅此而已。从上面也可以看出,COM组件是以
C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数都是虚函数,都必须通
过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心.
COM规范规定任何组件、任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是
QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不
可改变的。QueryInterface用于查询组件实现的其它接口,说白了也就是看看这个组件的父
类中还有哪些接口类,AddRef用于增加引用计数,Release用于减少引用计数。引用计数也
是COM中的一个非常重要的概念。大体上简单的说来可以这么理解,COM组件是个DLL,当客
户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给你一个人用的,可能
会有很多个程序同时都要用到它。但实际上DLL只装载了一次,即内存中只有一个COM组件,
那COM组件由谁来释放?由客户程序吗?不可能,因为如果你释放了组件,那别人怎么用,
所以只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前
有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放
它的时侯,COM知道已经没有人用它了,它的使用已经结束了,那它就把它自己给释放了。
引用计数是COM编程里非常容易出错的一个地方,但所幸VC的各种各种的类库里已经基本上
把AddRef的调用给隐含了,在我的印象里,我编程的时侯还从来没有调用过AddRef,我们
只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了
QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN
以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。
IUnknown的这三个函数的实现非常规范但也非常烦琐,容易出错,所幸的事我们可能永远也
不需要自己来实现它们。
IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平
常是怎么实例化一个类对象的?是用‘new’命令!很简单吧,COM组件也一样如此。但是谁
来new它呢?不可能是客户程序,因为客户程序不可能知道组件的类名字,如果客户知道组
件的类名字那组件的可重用性就要打个大大的折扣了,事实上客户程序只不过知道一个代表
着组件的128位的数字串而已,这个等会再介绍。所以客户无法自己创建组件,而且考虑一
下,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一
个单独的对象,这个对象就是类厂。每个组件都必须有一个与之相关的类厂,这个类厂知道
怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂
创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有
用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操
作都交给类厂对象去做了。IClassFactory最重要的一个函数就是CreateInstance,顾名思议
就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了,只有
某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多
的控制机会,而 VB给我们这样的机会则是太少太少了。
IDispatch叫做调度接口。它的作用何在呢?这个世上除了C++还有很多别的语言,比如VB、
VJ、VBScript、 JavaScript等等。可以这么说,如果这世上没有这么多乱七八糟的语言,那
就不会有IDispatch。:-) 我们知道COM组件是C++类,是靠虚函数表来调用函数的,对于VC来
说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以
通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如
VBScript、JavaScript。不行的原因在于它们并不支持指针,连指针都不能用还怎么用多态
性啊,还怎么调这些虚函数啊。唉,没办法,也不能置这些脚本语言于不顾吧,现在网页上
用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本
语言所调用,既然虚函数表的方式行不通,我们只能另寻他法了。时势造英雄,IDispatch应
运而生。:-)  调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的
时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅
此而已。当然实际的过程远比这复杂,当给一个编号就能让别人知道怎么调用一个函数那不
是天方夜潭吗,你总得让别人知道你要调用的函数要带什么参数,参数类型什么以及返回什
东西吧,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数
是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看一看MS的类库里实现
Invoke的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不
需要自己来做这件事,而且可能永远也没这样的机会。:-) 
(3) dispinterface接口、Dual接口以及Custom接口
这一小节放在这里似乎不太合适,因为这是在ATL编程时用到的术语。我在这里主要是想谈
一下自动化接口的好处及缺点,用这三个术语来解释可能会更好一些,而且以后迟早会遇上
它们,我将以一种通俗的方式来解释它们,可能并非那么精确,就好象用伪代码来描述算法
一样。-:)
所谓的自动化接口就是用IDispatch实现的接口。我们已经讲解过IDispatch的作用了,它的好
处就是脚本语言象VBScript、 JavaScript也能用COM组件了,从而基本上做到了与语言无关
它的缺点主要有两个,第一个就是速度慢效率低。这是显而易见的,通过虚函数表一下子就
可以调用函数了,而通过Invoke则等于中间转了道手续,尤其是需要把函数参数转换成一种
规范的格式才去调用函数,耽误了很多时间。所以一般若非是迫不得已我们都想用VTable的
方式调用函数以获得高效率。第二个缺点就是只能使用规定好的所谓的自动化数据类型。如果
不用IDispatch我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度
代码。而用自动化接口就不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预
料到我们要用到的所有类型,它只能根据一些常用的数据类型来写它的处理代码,而且它也
要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成的调度代码只适用于它所
规定好的那些数据类型,当然这些数据类型已经足够丰富了,但不能满足自定义数据结构的
要求。你也可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。
考虑到IDispatch的种种缺点(它还有一个缺点,就是使用麻烦,:-) )现在一般都推荐写双接
口组件,称为dual接口,实际上就是从IDispatch继承的接口。我们知道任何接口都必须从
IUnknown继承,IDispatch接口也不例外。那从IDispatch继承的接口实际上就等于有两个基
类,一个是IUnknown,一个是IDispatch,所以它可以以两种方式来调用组件,可以通过
IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用
这就有了很大的灵活性,这个组件既可以用于C++的环境也可以用于脚本语言中,同时满足了各方面的需要。
相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口
(虽然它实际上不是的),这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。
Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了
(4) COM组件有三种,进程内、本地、远程。对于后两者情况必须调度接口指针及函数参数。
COM是一个DLL,它有三种运行模式。它可以是进程内的,即和调用者在同一个进程内,也可
以和调用者在同一个机器上但在不同的进程内,还可以根本就和调用者在两台机器上。
这里有一个根本点需要牢记,就是COM组件它只是一个DLL,它自己是运行不起来的,必须
有一个进程象父亲般照顾它才行,即COM组件必须在一个进程内.那谁充当看护人的责任呢?
先说说调度的问题。调度是个复杂的问题,以我的知识还讲不清楚这个问题,我只是一般
性的谈谈几个最基本的概念。我们知道对于WIN32程序,每个进程都拥有4GB的虚拟地址空
间,每个进程都有其各自的编址,同一个数据块在不同的进程里的编址很可能就是不一样
的,所以存在着进程间的地址转换问题。这就是调度问题。对于本地和远程进程来说,DLL
和客户程序在不同的编址空间,所以要传递接口指针到客户程序必须要经过调度。Windows
已经提供了现成的调度函数,就不需要我们自己来做这个复杂的事情了。对远程组件来说
函数的参数传递是另外一种调度。DCOM是以RPC为基础的,要在网络间传递数据必须遵守标
准的网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调
度,这个过程很复杂,不过Windows已经把一切都给我们做好了,一般情况下我们不需要自
己来编写调度DLL。
我们刚说过一个COM组件必须在一个进程内。对于本地模式的组件一般是以EXE的形式出现,
所以它本身就已经是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了
调度代码以实现基本的调度。这个进程就是dllhost.exe。这是COM默认的DLL代理。实际上在
分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着很强大的功能,是专门的用于
管理分布式DLL组件的工具。
调度离我们很近又似乎很远,我们编程时很少关注到它,这也是COM的一个优点之一,既平
台无关性,无论你是远程的、本地的还是进程内的,编程是一样的,一切细节都由COM自己
处理好了,所以我们也不用深究这个问题,只要有个概念就可以了,当然如果你对调度有
自己特殊的要求就需要深入了解调度的整个过程了,这里推荐一本《COM+技术内幕》,这
绝对是一本讲调度的好书。
(5) COM组件的核心是IDL。
我们希望软件是一块块拼装出来的,但不可能是没有规定的胡乱拼接,总是要遵守一定的
标准,各个模块之间如何才能亲密无间的合作,必须要事先共同制订好它们之间交互的规
范,这个规范就是接口。我们知道接口实际上都是纯虚类,它里面定义好了很多的纯虚函
数,等着某个组件去实现它,这个接口就是两个完全不相关的模块能够组合在一起的关键
试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,我们没有时间
自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们
需要的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂
商已经在自己的组件中实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组
件,我们不关心组件从哪来,它有什么其它的功能,我们只关心它是否很好的实现了我们
制订好的接口。这种接口可能是业界的标准,也可能只是你和几个厂商之间内部制订的协
议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,是COM组件通信的标准。
COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为
止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话
说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义
的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义
接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个
语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL开始,
先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,
有的人可能善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你最后把DLL
拿给我。这是一种多么好的开发模式,可以用任何语言来开发,也可以用任何语言也欣赏
你的开发成果。
(6) COM组件的运行机制,即COM是怎么跑起来的。
    这部分我们将构造一个创建COM组件的最小框架结构,然后看一看其内部处理流程是怎样的
       IUnknown *pUnk=NULL;
       IObject *pObject=NULL;
       CoInitialize(NULL);
       CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown,
       (void**)&pUnk);
 pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
       pUnk->Release();
       pObject->Func();
       pObject->Release();
       CoUninitialize();
       这就是一个典型的创建COM组件的框架,不过我的兴趣在CoCreateInstance身上,让我们
    来看看它内部做了一些什么事情。以下是它内部实现的一个伪代码:
       CoCreateInstance(....)
       {
            .......
            IClassFactory *pClassFactory=NULL;
            CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL,     IID_IClassFactory,(void **)&pClassFactory);
            pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
            pClassFactory->Release();
            ........
       }
       这段话的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。
       继续深入一步,看看CoGetClassObject的内部伪码:
       CoGetClassObject(.....)
       {
        //通过查注册表CLSID_Object,得知组件DLL的位置、文件名
       //装入DLL库
       //使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。
       //调用DllGetClassObject 
       }
       DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.
       下面是DllGetClassObject的伪码:
        DllGetClassObject(...)
       {
        ......
        CFactory* pFactory= new CFactory;  //类厂对象
        pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
        pFactory->Release();
       ......
       }
       CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance
       的伪码:
       CFactory::CreateInstance(.....)
       {
           ...........
           CObject *pObject = new CObject;  //组件对象
           pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
           pObject->Release();
           ...........
       }
 

(7) 一个典型的自注册的COM DLL所必有的四个函数
    DllGetClassObject:用于获得类厂指针
    DllRegisterServer:注册一些必要的信息到注册表中
    DllUnregisterServer:卸载注册信息
    DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载DLL
    DLL还有一个函数是DllMain,这个函数在COM中并不要求一定要实现它,但是在VC生成的组
    件中自动都包含了它,它的作用主要是得到一个全局的实例对象。
(8) 注册表在COM中的重要作用
    首先要知道GUID的概念,COM中所有的类、接口、类型库都用GUID来唯一标识,GUID是一
    个128位的字串,根据特制算法生成的GUID可以保证是全世界唯一的。
    COM组件的创建,查询接口都是通过注册表进行的。有了注册表,应用程序就不需要知道
    组件的DLL文件名、位置,只需要根据CLSID查就可以了。当版本升级的时侯,只要改一下

    注册表信息就可以神不知鬼不觉的转到新版本的DLL。

 two,com框架分析

COM是一个更好的 C++
   1. COM 是什么
   2. 从 C++ 到 DLL 再到 COM
      2.1 C++
      2.2 DLL
      2.3 COM

二. COM基础
   1. COM基本知识
      1.1 返回值HRESULT
      1.2 初识idl
      1.3 IUnkown接口
   2. 一个比较简单的COM
      2.1 interface.h文件
      2.2 math.h文件
      2.3 math.cpp文件
      2.4 simple.cpp文件
      2.5 Math组件的二进制结构图
      2.6 小结

三. 纯手工创建一个COM组件
   1. 从建工程到实现注册
      1.1 创建一个类型为win32 dll工程
      1.2 定义接口文件
      1.3 增加注册功能
         1.3.1 增加一个MathCOM.def文件
         1.3.2 DllRegisterServer()和DllUnregisterServer()
      1.4 MathCOM.cpp文件
      1.5 小结
   2. 实现ISmipleMath,IAdvancedMath接口和DllGetClassObject()
      2.1 实现ISmipleMath和IAdvancedMath接口
      2.2 COM组件调入大致过程
      2.3 DllGetClassObject()实现
      2.4 客户端
      2.5 小结
   3. 类厂

附录   
A 我对dll的一点认识
一. 没有lib的dll
   1.1 建一个没有lib的dll
   1.2 调试没有lib的dll
二. 带有lib的dll
   2.1 创建一个带有lib的dll
   2.2 调试带有引用但没有头文件的dll
三. 带有头文件的dll
   3.1 创建一个带有引出信息头文件的dll
   3.2 调试带有头文件的dll
四. 小结

一、COM是一个更好的C++

1、COM 是什么

Don Box 说"COM IS LOVE"。COM 的全称是 Component Object Model 组件对象模型。

2、从 C++ 到 DLL 再到 COM

2.1 C++

如某一软件厂商发布一个类库(CMath四则运算),此时类库的可执行代码将成为客户应用中不可分割的一部分。假设此类库的所产生的机器码在目标可执行文件中占有4MB的空间。当三个应用程序都使用CMath库时,那么每个可执行文件都包含4MB的类库代码(见图1.1)。当三个应用程序共同运行时,他们将会占用12MB的虚拟内存。问题还远不于此。一旦类库厂商发现CMath类库有一个缺陷后,发布一个新的类库,此时需要要求所有运用此类库的应用程序。此外别无他法了。

26234753_mjpq.gif

26234753_Hmsq.gif

2.2 DLL

解决上面问题的一个技术是将CMath类做成动态链接库(DLL ,Dynamic Link Library)的形式封装起来 。
在使用这项技术的时候,CMath的所有方法都将被加到 CMath dll 的引出表(export list)中,而且链接器将会产生一个引入库(import library)。这个库暴露了CMath的方法成员的符号 。当客户链接引入库时,有一些存根会被引入到可执行文件中,它在知装载器动态装载 CMath Dll。

当 CMath 位于dll中时,他的运行模型见图1.2

26234753_GtgX.gif

图1.2 CMath引入库

2.3 COM

"简单地把C++类定义从dll中引出来"这种方案并不能提供合理的二进制组件结构。因为C++类那既是接口也是实现。这里需要把接口从实现中分离出来才能提供二进制组件结构。此时需要有二个C++类,一个作为接口类另一个作为实现类。让我们开始COM之旅吧。

二、COM基础

1、 COM基本知识

1.1 返回值HRESULT

COM要求所有的方法都会返回一个HRESULT类型的错误号。HRESULT 其实就一个类型定义:

1. typedef LONG HRESULT;

有关HRESULT的定义见 winerror.h 文件

01. //  Values are 32 bit values layed out as follows:
02. //
03. //  3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
04. //  1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
05. //  +-+----+-------------------------+---------------------------------+
06. //  |S| Res|     Facility            |     Code                        |
07. //  +-+----+-------------------------+---------------------------------+
08. //
09. //  where
10. //
11. //      S - is the severity code
12. //
13. //          0 - Success
14. //          1 - Error
15. //
16. //      Res- is a reserved bit
17. //
18. //      Facility - is the facility code
19. //
20. //      Code - is the facility''s status code

我们一般下面的宏来判断方法是否成功:

1. #define SUCCEEDED(hr)(long(hr)>=0)
2. #define FAILED(hr)(long(hr)<0)

1.2 初识 IDL

每个标准的COM组件都需要一个接口定义文件,文件的扩展名为IDL。让我们看IUnknow接口的定义文件是怎样的。

01. [
02. local,
03. object,
04. uuid(00000000-0000-0000-C000-000000000046),
05. pointer_default(unique)
06. ]
07.  
08. interface IUnknown
09. {
10. typedef [unique] IUnknown *LPUNKNOWN;
11.  
12. cpp_quote("")
13. cpp_quote("// IID_IUnknown and all other system IIDs are provided in UUID.LIB")
14. cpp_quote("// Link that library in with your proxies, clients and servers")
15. cpp_quote("")
16.  
17. HRESULT QueryInterface(
18. [in] REFIID riid,
19. [out, iid_is(riid)] void **ppvObject);
20. ULONG AddRef();
21. ULONG Release();
22. }
23.  
24. [local]属性禁止产生网络代码。
25. [object]属性是表明定义的是一个COM接口,而不是DEC风格的接口。
26. [uuid]属性给接口一个GUID。
27. [unique]属性表明null(空)指针为一个合法的参数值。
28. [pointer_defaul]属性所有的内嵌指针指定一个默认指针属性
29. typedef [unique] IUnknown *LPUNKNOWN;这是一个类型定义
30. cpp_quote这个比较有趣,这是一个在idl文件写注解的方法。这些注解将保存到***.h和***_i.c文件中
31. [in]表示这个参数是入参
32. [out]表示这个参数是出参
33. [iid_is(riid)]表示这个参数需要前一个的riid 参数。

注意:所有具有out属性的参数都需要是指针类型。

1.3 IUnkown接口

在整个例子除了IUnkown这个东西,其他应该不会感到陌生吧!COM要求(最基本的要求)所有的接口都需要从IUnknown接口直接或间接继承,所以IUnknown接口有"万恶之源"之称。

IUnkown接口定义了三个方法。

1. HRESULT QueryInterface([in] REFIID riid,[out] void **ppv);
2. ULONG AddRef();
3. ULONG Release();

其中 AddReft() 和Release()负责对象引用计数用的,而 QueryInterface()方法是用于查询所实现接口用的。每当COM组件被引用一次就应调用一次AddRef()方法。而当客户端在释放COM组件的某个接口时就需要调用Release()方法。

这里所讲的请在下面的例子仔细体会。

2、一个比较简单的COM

此例子共有四个文件组成:

文件名 说明
Interface.h 接口类定义文件
Math.h和Math.cpp 实现类文件
Simple.cpp 主函数文件 这里用来当作COM的客户端

2.1 interface.h 文件

01. #ifndef INTERFACE_H
02. #define INTERFACE_H
03. #include
04.  
05. //{7C8027EA-A4ED-467c-B17E-1B51CE74AF57}
06. static const GUID IID_ISimpleMath =
07. { 0x7c8027ea, 0xa4ed, 0x467c, { 0xb1, 0x7e, 0x1b, 0x51, 0xce, 0x74, 0xaf, 0x57 } };
08.  
09. //{CA3B37EA-E44A-49b8-9729-6E9222CAE84F}
10. static const GUID IID_IAdvancedMath =
11. { 0xca3b37ea, 0xe44a, 0x49b8, { 0x97, 0x29, 0x6e, 0x92, 0x22, 0xca, 0xe8, 0x4f } };
12.  
13. interface ISimpleMath : public IUnknown
14. {
15. public:
16. virtual int Add(int nOp1, int nOp2) = 0;       
17. virtual int Subtract(int nOp1, int nOp2) = 0;
18. virtual int Multiply(int nOp1, int nOp2) = 0;
19. virtual int Divide(int nOp1, int nOp2) = 0;
20. };
21.  
22. interface IAdvancedMath : public IUnknown
23. {
24. public:
25. virtual int Factorial(int nOp1) = 0;
26. virtual int Fabonacci(int nOp1) = 0;
27. };
28. #endif

此文件首先 #include 将 IUnknown 接口定义文件包括进来。

接下来定义了两个接口,GUID(Globally Unique Identifier全局唯一标识符)它能保证时间及空间上的唯一。
ISmipleMath接口里定义了四个方法,而IAdvancedMath接口里定义了二个方法。这些方法都是虚函数,而整个 ISmipleMath 与 IAdvancedMath 抽象类就作为二进制的接口。

2.2 math.h文件

01. #include "interface.h"
02.  
03. class CMath : public ISimpleMath,
04. public IAdvancedMath
05. {
06. private:
07. ULONG m_cRef;
08.  
09. private:
10. int calcFactorial(int nOp);
11. int calcFabonacci(int nOp);
12.  
13. public:
14. //IUnknown Method
15. STDMETHOD(QueryInterface)(REFIID riid, void **ppv);
16. STDMETHOD_(ULONG, AddRef)();
17. STDMETHOD_(ULONG, Release)();
18.  
19. //  ISimpleMath Method
20. int Add(int nOp1, int nOp2);
21. int Subtract(int nOp1, int nOp2);
22. int Multiply(int nOp1, int nOp2);
23. int Divide(int nOp1, int nOp2);
24.  
25. //  IAdvancedMath Method
26. int Factorial(int nOp);
27. int Fabonacci(int nOp);
28. };

此类为实现类,他实现了ISmipleMath和IAdvancedMath两个接口类(当然也可以只实现一个接口类)。

请注意:m_cRef 是用来对象计数用的。当 m_cRef 为0组件对象应该自动删除。

2.3 math.cpp文件

01. #include "interface.h"
02. #include "math.h"
03.  
04. STDMETHODIMP CMath::QueryInterface(REFIID riid, void **ppv)
05. {// 这里这是实现dynamic_cast的功能,但由于dynamic_cast与编译器相关。
06. if(riid == IID_ISimpleMath)
07. *ppv = static_cast(this);
08. else if(riid == IID_IAdvancedMath)
09. *ppv = static_cast(this);
10. else if(riid == IID_IUnknown)
11. *ppv = static_cast(this);
12. else {
13. *ppv = 0;
14. return E_NOINTERFACE;
15. }
16.  
17. reinterpret_cast(*ppv)->AddRef();    //这里要这样是因为引用计数是针对组件的
18. return S_OK;
19. }
20.  
21. STDMETHODIMP_(ULONG) CMath::AddRef()
22. {
23. return ++m_cRef;
24. }
25.  
26. STDMETHODIMP_(ULONG) CMath::Release()
27. {
28. ULONG res = --m_cRef;   // 使用临时变量把修改后的引用计数值缓存起来
29. if(res == 0)        // 因为在对象已经销毁后再引用这个对象的数据将是非法的
30. delete this;
31. return res;
32. }
33.  
34. int CMath::Add(int nOp1, int nOp2)
35. {
36. return nOp1+nOp2;
37. }
38.  
39. int CMath::Subtract(int nOp1, int nOp2)
40. {
41. return nOp1 - nOp2;
42. }
43.  
44. int CMath::Multiply(int nOp1, int nOp2)
45. {
46. return nOp1 * nOp2;
47. }
48.  
49. int CMath::Divide(int nOp1, int nOp2)
50. {
51. return nOp1 / nOp2;
52. }
53.  
54. int CMath::calcFactorial(int nOp)
55. {
56. if(nOp <= 1)
57. return 1;
58.  
59. return nOp * calcFactorial(nOp - 1);
60. }
61.  
62. int CMath::Factorial(int nOp)
63. {
64. return calcFactorial(nOp);
65. }
66.  
67. int CMath::calcFabonacci(int nOp)
68. {
69. if(nOp <= 1)
70. return 1;
71.  
72. return calcFabonacci(nOp - 1) + calcFabonacci(nOp - 2);
73. }
74.  
75. int CMath::Fabonacci(int nOp)
76. {
77. return calcFabonacci(nOp);
78. }
79. CMath::CMath()
80. {
81. m_cRef=0;
82. }

此文件是CMath类定义文件。

2.4 simple.cpp文件

01. #include "math.h"
02. #include
03.  
04. using namespace std;
05.  
06. int main(int argc, char* argv[])
07. {
08. ISimpleMath *pSimpleMath = NULL;//声明接口指针
09. IAdvancedMath *pAdvMath = NULL;
10.  
11. //创建对象实例,我们暂时这样创建对象实例,COM有创建对象实例的机制
12. CMath *pMath = new CMath;  
13.  
14. //查询对象实现的接口ISimpleMath
15. pMath->QueryInterface(IID_ISimpleMath, (void **)&pSimpleMath);  
16. if(pSimpleMath)
17. cout << "10 + 4 = " << pSimpleMath->Add(10, 4) << endl;
18.  
19. //查询对象实现的接口IAdvancedMath
20. pSimpleMath->QueryInterface(IID_IAdvancedMath, (void **)&pAdvMath);
21. if(pAdvMath)
22. cout << "10 Fabonacci is " << pAdvMath->Fabonacci(10) << endl;
23.  
24. pAdvMath->Release();
25. pSimpleMath->Release();
26. return 0;
27. }

此文件相当于客户端的代码,首先创建一个CMath对象,再根据此对象去查询所需要的接口,如果正确得到所需接口指针,再调用接口的方法,最后再将接口的释放掉。

2.5 Math组件的二进制结构


此例子从严格意义上来并不是真正的COM组件(他不是dll),但他已符合COM的最小要求.


adiOS










转载于:https://my.oschina.net/bigfool007139/blog/551612

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一 组件基础 1 软件开发的阶段 1.1 结构化编程 采用自顶向下的编程方式,划分模块 和功能的一种编程方式。 1.2 面向对象编程 采用对象的方式,将程序抽象成类, 模拟现实世界,采用继承、多态的方式 设计软件的一种编程方式。 1.3 面向组件编程 将功能和数据封装成二进制代码,采用 搭积木的方式实现软件的一种编程方式。 2 组件和优点 2.1 组件 - 实际是一些可以执行的二进 制程序,它可以给其他的应用程序、操 作系统或其他组件提供功能 2.2 优点 2.2.1 可以方便的提供软件定制机制 2.2.2 可以很灵活的提供功能 2.2.3 可以很方便的实现程序的分布式 开发。 3 组件的标准 - COMComponent Object Model ) 3.1 COM是一种编程规范,不论任何开发语言 要实现组件都必须按照这种规范来实现。 组件和开发语言无关。 这些编程规范定义了组件的操作、接口的 访问等等。 3.2 COM接口 COM接口是组件的核心,从一定程度上 讲"COM接口是组件的一切". COM接口给用户提供了访问组件的方式. 通过COM接口提供的函数,可以使用组件 的功能. 4 COM组件 4.1 COM组件-就是在Windows平台下, 封装在动态库(DLL)或者可执行文件(EXE) 中的一段代码,这些代码是按照COM的 规范实现. 4.2 COM组件的特点 4.2.1 动态链接 4.2.2 与编程语言无关 4.2.3 以二进制方式发布 二 COM接口 1 接口的理解 DLL的接口 - DLL导出的函数 类的接口 - 类的成员函数 COM接口 - 是一个包含了一组函数指针 的数据结构,这些函数是由组件实现的 2 C++的接口实现 2.1 C++实现接口的方式,使用抽象类 定义接口. 2.2 基于抽象类,派生出子类并实现 功能. 2.3 使用 interface 定义接口 interface ClassA { }; 目前VC中,interface其实就是struct 3 接口的动态导出 3.1 DLL的实现 3.1.1 接口的的定义 3.1.2 接口的实现 3.1.3 创建接口的函数 3.2 DLL的使用 3.2.1 加载DLL和获取创建接口的函数 3.2.2 创建接口 3.2.3 使用接口的函数 4 接口的生命期 4.1 问题 在DLL中使用new创建接口后,在用户 程序使用完该接口后,如果使用delete 直接删除,会出现内存异常. 每个模块有自己的内存堆(crtheap) EXE - crtheap DLL - crtheap new/delete/malloc/free默认情况 下都是从自己所在模块内存堆(crtheap) 中分配和施放内存.而各个模块的 这个内存堆是各自独立.所以在DLL中 使用new分配内存,不能在EXE中delete. 4.2 引用计数和AddRef/Release函数 引用计数 - 就是一个整数,作用是 表示接口的使用次数 AddRef - 增加引用计数 +1 Release - 减少引用计数 -1, 如果 当引用计数为0,接口被删除 4.3 使用 4.3.1 创建接口 4.3.2 调用AddRef,增加引用计数 4.3.3 使用接口 4.3.4 调用Release,减少引用计数 4.4 注意 4.4.1 在调用Release之后,接口指针 不能再使用 4.4.2 多线程情况下,接口引用计数 要使用原子锁的方式进行加减 5 接口的查询 5.1 每个接口都具有唯一标识 GUID 5.2 实现接口查询函数 QueryInterface 6 IUnknown 接口 6.1 IUnknown是微软定义的标准接口 我们实现所有接口就是继承这个接口 6.2 IUnknown定义了三个函数 QueryInterface 接口查询函数 AddRef 增加引用计数 Release 减少引用计数 7 接口定义语言 - IDL(Interface Definition Language ) 7.1 IDL和MIDL IDL - 定义接口的一种语言,与开发 语言无关. MIDL.EXE - 可以将IDL语言定义接口, 编译成C++语言的接口定义 7.2 IDL的基础 import "XXXX.idl" [ attribute ] interface A : interface_base { } 7.2.1 Import 导入,相当于C++的 #include 7.2.2 使用"[]"定义区域,属性描述 关键字 1) object - 后续是对象 2) uuid - 定义对象GUID 3) helpstring - 帮助信息 4) version - 版本 5) point_default - 后续对象 中指针的默认使用方式 比如: uniqune - 表示指针可以 为空,但是不能修改 7.2.3 对象定义 1) 父接口是IUnknown接口 2) 在对象内添加函数,函数定义必须 是返回 HRESULT. HRESULT是32位整数,返回函数是否 执行成功,需要使用 SUCCESSED和 FAILED宏来判断返回值.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值