电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

用VC进行COM编程所必须掌握的理论知识


发布日期:2024/1/26
 

这篇文章是给初学者看的尽量写得比较通俗易懂并且尽量避免编程细节完全是根据我自己的学习体会写的其中若有技术上的错误之处请大家多多指正

为什么要用COM

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

() COM组件实际上是一个C++类而接口都是纯虚类组件从接口派生而来我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西

class IObject

{

public:

virtual Function() = ;

virtual Function() = ;

};

class MyObject : public IObject

{

public:

virtual Function(){}

virtual Function(){}

};

看清楚了吗?IObject就是我们常说的接口MyObject就是所谓的COM组件切记切记接口都是纯虚类它所包含的函数都是纯虚函数而且它没有成员变量而COM组件就是从这些纯虚类继承下来的派生类它实现了这些虚函数仅此而已从上面也可以看出COM组件是以 C++为基础的特别重要的是虚函数和多态性的概念COM中所有函数都是虚函数都必须通过虚函数表VTable来调用这一点是无比重要的必需时刻牢记在心

() COM组件有三个最基本的接口类分别是IUnknownIClassFactoryIDispatch

COM规范规定任何组件任何接口都必须从IUnknown继承IUnknown包含三个函数分别是 QueryInterfaceAddRefRelease这三个函数是无比重要的而且它们的排列顺序也是不可改变的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它呢?不可能是客户程序因为客户程序不可能知道组件的类名字如果客户知道组件的类名字那组件的可重用性就要打个大大的折扣了事实上客户程序只不过知道一个代表着组件的位的数字串而已这个等会再介绍所以客户无法自己创建组件而且考虑一下如果组件是在远程的机器上你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象这个对象就是类厂每个组件都必须有一个与之相关的类厂这个类厂知道怎么样创建组件当客户请求一个组件对象的实例时实际上这个请求交给了类厂由类厂创建组件实例然后把实例指针交给客户程序这个过程在跨进程及远程创建组件时特别有用因为这时就不是一个简单的new操作就可以的了它必须要经过调度而这些复杂的操作都交给类厂对象去做了IClassFactory最重要的一个函数就是CreateInstance顾名思议就是创建组件实例一般情况下我们不会直接调用它API函数都为我们封装好它了只有某些特殊情况下才会由我们自己来调用它这也是VC编写COM组件的好处使我们有了更多的控制机会而VB给我们这样的机会则是太少太少了

IDispatch叫做调度接口它的作用何在呢?这个世上除了C++还有很多别的语言比如VB VJVBScriptJavaScript等等可以这么说如果这世上没有这么多乱七八糟的语言那就不会有IDispatch:) 我们知道COM组件是C++类是靠虚函数表来调用函数的对于VC来说毫无问题这本来就是针对C++而设计的以前VB不行现在VB也可以用指针了也可以通过VTable来调用函数了VJ也可以但还是有些语言不行那就是脚本语言典型的如 VBScriptJavaScript不行的原因在于它们并不支持指针连指针都不能用还怎么用多态性啊还怎么调这些虚函数啊没办法也不能置这些脚本语言于不顾吧现在网页上用的都是这些脚本语言而分布式应用也是COM组件的一个主要市场它不得不被这些脚本语言所调用既然虚函数表的方式行不通我们只能另寻他法了时势造英雄IDispatch应运而生:) 调度接口把每一个函数每一个属性都编上号客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了IDispatch再根据这些编号调用相应的函数仅此而已当然实际的过程远比这复杂仅给一个编号就能让别人知道怎么调用一个函数那不是天方夜潭吗你总得让别人知道你要调用的函数要带什么参数参数类型什么以及返回什么东西吧而要以一种统一的方式来处理这些问题是件很头疼的事IDispatch接口的主要函数是Invoke客户程序都调用它然后Invoke再调用相应的函数如果看一看MS的类库里实现 Invoke的代码就会惊歎它实现的复杂了因为你必须考虑各种参数类型的情况所幸我们不需要自己来做这件事而且可能永远也没这样的机会:)

() 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接口派生的类显然它就只能用虚函数表的方式来调用接口了

() COM组件有三种进程内本地远程对于后两者情况必须调度接口指针及函数参数

COM是一个DLL它有三种运行模式它可以是进程内的即和调用者在同一个进程内也可以和调用者在同一个机器上但在不同的进程内还可以根本就和调用者在两台机器上这里有一个根本点需要牢记就是COM组件它只是一个DLL它自己是运行不起来的必须有一个进程象父亲般照顾它才行即COM组件必须在一个进程内那谁充当看护人的责任呢?先说说调度的问题调度是个复杂的问题以我的知识还讲不清楚这个问题我只是一般性的谈谈几个最基本的概念我们知道对于WIN程序每个进程都拥有GB的虚拟地址空间每个进程都有其各自的编址同一个数据块在不同的进程里的编址很可能就是不一样的所以存在着进程间的地址转换问题这就是调度问题对于本地和远程进程来说DLL 和客户程序在不同的编址空间所以要传递接口指针到客户程序必须要经过调度Windows 已经提供了现成的调度函数就不需要我们自己来做这个复杂的事情了对远程组件来说函数的参数传递是另外一种调度DCOM是以RPC为基础的要在网络间传递数据必须遵守标准的网上数据传输协议数据传递前要先打包传递到目的地后要解包这个过程就是调度这个过程很复杂不过Windows已经把一切都给我们做好了一般情况下我们不需要自己来编写调度DLL

我们刚说过一个COM组件必须在一个进程内对于本地模式的组件一般是以EXE的形式出现所以它本身就已经是一个进程对于远程DLL我们必须找一个进程这个进程必须包含了调度代码以实现基本的调度这个进程就是dllhostexe这是COM默认的DLL代理实际上在分布式应用中我们应该用MTS来作为DLL代理因为MTS有着很强大的功能是专门的用于管理分布式DLL组件的工具

调度离我们很近又似乎很远我们编程时很少关注到它这也是COM的一个优点之一既平台无关性无论你是远程的本地的还是进程内的编程是一样的一切细节都由COM自己处理好了所以我们也不用深究这个问题只要有个概念就可以了当然如果你对调度有自己特殊的要求就需要深入了解调度的整个过程了这里推荐一本《COM+技术内幕》这绝对是一本讲调度的好书

() COM组件的核心是IDL

我们希望软件是一块块拼装出来的但不可能是没有规定的胡乱拼接总是要遵守一定的标准各个模块之间如何才能亲密无间的合作必须要事先共同制订好它们之间交互的规范这个规范就是接口我们知道接口实际上都是纯虚类它里面定义好了很多的纯虚函数等着某个组件去实现它这个接口就是两个完全不相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商我们的软件中需要用到某个模块我们没有时间自己开发所以我们想到市场上找一找看有没有这样的模块我们怎么去找呢?也许我们需要的这个模块在业界已经有了标准已经有人制订好了标准的接口有很多组件工具厂商已经在自己的组件中实现了这个接口那我们寻找的目标就是这些已经实现了接口的组件我们不关心组件从哪来它有什么其它的功能我们只关心它是否很好的实现了我们制订好的接口这种接口可能是业界的标准也可能只是你和几个厂商之间内部制订的协议但总之它是一个标准是你的软件和别人的模块能够组合在一起的基础是COM组件通信的标准

COM具有语言无关性它可以用任何语言编写也可以在任何语言平台上被调用但至今为止我们一直是以C++的环境中谈COM那它的语言无关性是怎么体现出来的呢?或者换句话说我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的但显然是不行的除了C++谁还认它呢?正是出于这种考虑微软决定采用IDL来定义接口说白了IDL实际上就是一种大家都认识的语言用它来定义接口不论放到哪个语言平台上都认识它我们可以想象一下理想的标准的组件模式我们总是从IDL开始先用IDL制订好各个接口然后把实现接口的任务分配不同的人有的人可能善长用VC有的人可能善长用VB这没关系作为项目负责人我不关心这些我只关心你把最终的DLL 拿给我这是一种多么好的开发模式可以用任何语言来开发也可以用任何语言来欣赏你的开发成果

() 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);

//查询IClassFactory指针

pFactory>Release();

}

CoGetClassObject的流程已经到此为止现在返回CoCreateInstance看看CreateInstance的伪码

CFactory::CreateInstance()

{

CObject *pObject = new CObject; //组件对象

pObject>QueryInterface(IID_IUnknown (void**)&pUnk);

pObject>Release();

}

() 一个典型的自注册的COM DLL所必有的四个函数

DllGetClassObject:用于获得类厂指针

DllRegisterServer:注册一些必要的信息到注册表中

DllUnregisterServer:卸载注册信息

DllCanUnloadNow:系统空闲时会调用这个函数以确定是否可以卸载DLL

DLL还有一个函数是DllMain这个函数在COM中并不要求一定要实现它但是在VC生成的组件中自动都包含了它它的作用主要是得到一个全局的实例对象

() 注册表在COM中的重要作用

首先要知道GUID的概念COM中所有的类接口类型库都用GUID来唯一标识GUID是一个位的字串根据特制算法生成的GUID可以保证是全世界唯一的 COM组件的创建查询接口都是通过注册表进行的有了注册表应用程序就不需要知道组件的DLL文件名位置只需要根据CLSID查就可以了当版本升级的时侯只要改一下注册表信息就可以神不知鬼不觉的转到新版本的DLL

本文是本人一时兴起的涂鸭之作讲得并不是很全面还有很多有用的体会没写出来以后如果有时间有兴趣再写出来希望这篇文章能给大家带来一点用处那我一晚上的辛苦就没有白费了:)

上一篇:2008微软技术创新日 9月27日登陆北京

下一篇:读取并修改App.config文件实例