学习COM的方法:
你需要两本书, 一本是COM本质论, 一本是COM技术内幕.
首先看COM本质论的前两章, 然后看COM技术内幕, 再回头将COM本质论剩下的内容看完.
怎么样才能最快最好的学习一项新技术? 新技术的发明, 一般是出于两种目的, 或者是为了解决现在存在的问题, 或者是为了更方便更简捷,提高效率. 从新技术出现的原因入手,我认为是最佳途径。
学习COM就是采用的这个方法。
com本质论
本文中用到的术语:
1 客户: 指最终使用程序的人。
在最初,我们使用C++编写程序,如果是一个小程序,那么一个人做足够了。但是当许多人在一起合作去完成一个大项目的话,就出现了分工。比如说将不同的功能分为不同的类,每个人负责一个类或者几个类的开发,最后将这些类合在一起,构成了整个程序。比如你做的一个类,叫做FastString. 用这种方法来分工合作有这样的问题:
一是如果客户有三个应用程序,分别是A,B,C,他们都需要某个类,那么该类被加到A,B,C的工程中,分别编译,形成最终的应用程序A,B,C。那么在客户的机器中会有三个FastString.obj,即有三段一样的代码存在,浪费内存。
二是一旦FastString发布了,那么以后如果你再修改它的话,必须要把这个修改后的类给最终用户,用户再重新编译,生成新的应用程序A,B,C.
为了解决这两个问题,出现了DLL技术。在上面的方法中,你的类和其它的类都是用户用他自己的编译器编译的,但是现在DLL是你自己的编译器编译的,和用户的编译器有可能不一样。
所以DLL的出现解决了上述的两个问题,但是却带来了新的问题。
一 因为C++为了实现函数重载,允许编译器对函数名字进行改编(name mangling),但是改编的具体规则又没有一个标准,所以有时会导致应用程序和DLL的引入库之间无法成功链接。
解决方法是: 你要为用户的编译器提供一个适合他的引入库。
二 如果你修改了FastString类,比如说在其中增加了一个数据成员m_cch. 生成一个新的FastString, 我们叫它FastString2.0, 把它发布给客户,客户更新了自己的应用程序A,在其中加入了FastString2.0.h, 生成了应用程序A2.0. 那么当应用程序A2.0调用了机器中旧版的FastString DLL的话,就会崩溃了。
解决方法:将FastString类分成两个类,一个是接口类(FastStringItf),一个是实现类(FastString)。
接口类这样定义:
//FastStringItf.h
class _declspec(dllexport) FastStringItf{
class FastString;//声明实现类
FastString *m_pThis;//大小保持固定,就是四个字节
int Length(void) Const}
其中Length()是这样实现的:
int FastStringItf::Length(void) const
{
return m_pThis->Length();
}
客户只要包含接口类的.h文件就可以了。也就是不管实现类怎么改,客户也不用改变。
现在分成接口类和实现类的这个方法虽然解决了客户程序崩溃的问题,但是却存在另外两个不够好的地方。
一 接口类必须要把每个方法调用显式的传递给实现类。
二 由于每个方法都增加一个函数调用, 所以会影响到执行效率。
解决的方法是将接口类设计成抽象基类, 然后实现类从接口类派生。
com技术内幕
1 接口即接口类。
2 动态链接:
组件CA在DLL中定义。派生自不同的接口并实现各个接口中的方法。
客户如何使用组件呢? 我们知道,接口类是抽象基类,不能用来定义对象。实现类也不能用来直接定义对象,因为这样破坏了封闭性。
我们可以在DLL生成一个全局函数用来生成该对象:
extern "C"
IUnKnown* CreateInstance()
{
IUnKnown*PI = (IUnKnown*)(void*) new CA;
PI->AddRef();
return PI;
}
客户首先调用LoadLibrary()把包含组件的DLL载入,然后再利用GetProcAddress(), 调用CreateInstance(),获取一个IUnKnown 指针。
再调用该指针的QueryInterface(),得到一个接口指针,再调用该接口中提供的方法。
3 组件的注册
每一个组件是一个C++类,它存在于一个DLL中。每一个组件有一个唯一的guid值,称为CLSID。如果我们不想每次都利用 loadLibrary()去动态载入包含该组件的DLL, 我们可以将它注册在注册表中,即某个CLSID所对应的组件是在哪个DLL中。因为DLL知道它自己包含哪个CLSID的组件,所以最好由DLL自己去注册。方法是在DLL中写两个函数STDAPI DLLRegisterServer()和STDAPI DLLUnRegisterServer(). 这两个函数分别调用一些注册表操作函数把自己注册。我们可以自己调用它们来注册该DLL,大部分安装程序也会调用它们来注册相应的DLL。系统中一个工具
REGSVR32.EXE,它可以用来注册,其实方法也就是调用上面的两个函数(方法就是LoadLibrary+getprocAddress).
COM组件在使用前需要注册也就是这个意思,即告诉系统该组件在哪个DLL中。
注册后我们就不用再调用 loadlibrary+getprocaddress去调用createInstance()了。 而是直接调用COM库函数CoCreateInstance()去创建组件对象了,在参数中给出CLSID即可,不用给DLL名字了(因为注册过,所以他自己会根据CLSID去注册表中查找DLL)。
3 COM库函数?
4 类厂:
在COM库函数中有一个CoCreateInstance()函数,当我们调用这个函数时,传入CLSID,就可以生成相应的组件。说来简单,其实这个过程是比较复杂的,程序员要做的事件很多,
一是要在DLL中实现一个若干个类(在该DLL有几个组件就应该生成几个这样的类):
CFactoryCA:public IClassFactory//IClassFactory是一个已经写好的接口
{
createInstance(IUnKnown* pUnknownOuter, const IID&iid, void** ppv)
{
new CA;
}
}
CFactoryCB:public IClassFactory//IClassFactory是一个已经写好的接口
{
createInstance(IUnKnown* pUnknownOuter, const IID&iid, void** ppv)
{
new CB;
}
}
二是要在DLL中实现一个函数:
DllGetClassObject(const CLSID& clsid, const IID& iid, void** ppv)
该函数根据clsid来创建一个相应的CFactoryCA或者CFactoryCB(DLL编写者知道哪个clsid对就哪个工厂类).
最终用户要创建一个组件对象,他调用库函数CoCreateInstance(), 该函数内部会调用CoGetClassObject(), 该函数内部调用DllGetClassObjet(),该函数内部创建一个工厂类,
最后把该工厂类的接口指针传回给CoCreateInstance()中的一个参数,最终用户调用该参数的createInstance()创建了一个组件对象。
那么,COM组件设计者要负责在DLL中完成工厂函数和DllGetClassObject()的编写工作。
1 为什么"聚合"的时候需要两个IUnknown, 如果不这样的话, 就违反了 "如果能从一个接口查询某个特定接口, 那么应该从所有的接口都可以查询到该接口"的规则。对于客户来说,我们不能让他们感觉到我们使用了聚合,而是感觉就像是所有的接口都来自于一个组件那样。所以我们必须采取一些措施来遵守上面的规则。