第五章 DirectX基础与恐怖的COM

第五章 DirectX基础与恐怖的COM
本章,我们将对DirectX以及构成这一难以置信技术的基本组件进行深入的研究。此外,我们还将对COM这一所有DirectX组件之基础进行详细的学习。如果你是一个单纯的C程序员,那你可要万分注意了,不过也别太担心,我会很周到地给你讲解的。

但是,在开讲本章以及你打算跳过这章之前还是要提点警告的。DirectX与COM是紧密联系的。很难在不提及一方的情况下解释清楚另一方。就好像,你如何能在不给出“零”的定义的情况下解释清楚“零”?如果你认为脱离COM来谈DirectX很容易,那就错了。

下面是我们将要讨论的主要内容的列表:
·DirectX介绍
·组件对象模型(COM)
·一个COM实现的可运行例子
·DirectX与COM是如何协调工作的
·COM的未来〕

DirectX基础
这几天我感觉自己就像是Microsoft的卫道士(提醒Microsoft,给我钱),试图把我所有的朋友拖入黑暗一边。但是Microsoft这个坏家伙总有更好的技术!我说的对吗?你认为Microsoft是什么,一个帝国的明星破坏者或者半个叛逆者?看我都在说些什么啊。

DirectX可能会进一步减弱程序员的控制力,但事实上这是值得。它基本上是抽象了视频,音频,输入,网络,安装以及更多功能的系统软件。因此,你可以用相同的代码驱动任何特定PC的硬件而不用管这些硬件是如何设置的。此外DirectX技术比Windows的GDI和(或)MCI(媒体控制接口)要快好几倍,而且功能更大。

图5.1图示了你如何应用以及不应用DirectX创建游戏。注意DirectX解决方案是多简洁与优雅。
 
明白了DirectX是如何工作的了吗?它提供对所有设备近乎硬件级的访问。实现这种访问仰赖于一种叫做组件对象模型的技术以及一系列由Microsoft和硬件生产商写的驱动程序和库函数。Microsoft提供一系列对函数,变量,数据结构等等的标准化协议,硬件生产商在实现与硬件通信之驱动程序时必须遵守这些议。

只要这些协定被切实遵守,你就无需对硬件的细节担忧。你只需调用DirectX,它会替你搞定细节。无论显卡,声卡,输入设备,网卡,或者其他任何设备,只要DirectX支持,你的程序便可在你对设备一无所知的情况下运用该设备。

目前,DirectX包括很多组件。它们在图5.2被列出。
 
·DirectDraw(DirectX8.0及以上版本不再有)
·DirectSound
·DirectSound3D
·DirectMusic
·DirectInput
·DirectPlay
·DirectSeup
·Direct3DRM
·Direct3dIM
·DirectX Graphics(合并了DirectDraw/Direct3D)
·DirectX Audio(合并了DirectSound/DirectMusic)
·DirectShow
到DirectX8.0发布的时候,Microsoft决定把DirectDraw和Direct3D合并到一起,都集成到DirectX Graphics中。结果便是DirectX8.0中DirectDraw被去除了。但你仍然可以使用DirectDraw,只是在DirectX8.0中其未被升级罢了。此外DirectSound和DirectMusic也被集成到DirectX Audio中了。最终,DirectShow(脱胎于DirectMedia)被集成到DirectX中。Microsoft的那帮家伙可真会忙乎啊。

这些变动也许看上去显得生硬且令人困惑,但DirectX酷就酷在只要我们想,那就可以使用DirectX3.0或5.0或6.0,或其他。拜COM(马上我们就会学到它)所赐,我们能够使用任何我们想要的且符合我们需求的版本,这完全取决我们。而且在我们看来,7.0版本和部分8.0功能已经足够了。进一步的,如果你熟悉了DirectX的一个版本那就熟悉了所有的版本,语法可能变了点,界面功能多了点,但是总而言之、言而总之本质是一样的。唯一变化最大的是Direct3D,但我们并不打算在本书中讨论。本质上,我们将只讨论游戏编程。是的,在随书CD上有两本书,其中一本是关于3D的,另一本关于Direct3D,不过在本书中我们将只集中讨论DirectX的主干部分,故而你将学到足够你做游戏的相关知识。当然,从您整个游戏编程职业生涯看来所学绝非止于此,不过当你要去使用其他API时你将仍然能够理解该游戏编程技术的本质,这便是本书写作的终极目标。

硬件模拟层(HEL)和硬件抽象层(HAL)
在图5.2中,你可能注意到在DirectX之下有两个分别叫做HEL(硬件模拟层)和HAL(硬件抽象层)的层次。DirectX设计得非常据远见,它假设先进的特性已被硬件所实现。然而,如果硬件不支持这些特性又会发生什么呢?这便是HAL和HEL这一双模式设计的由来。

HAL,或称之硬件抽象层是面向硬件的(to the metal)层次,其直接与硬件对话。这一层通常是由硬件厂商提供的驱动程序构成,而你可通过一般的DirectX函数调用与之通信。简而言之,当你请求的特性被硬件直接支持时便会使用HAL以达到加速的目的。比如你请求画一幅位图,便会由硬件而非软件来完成任务。

HEL,或称之硬件模拟层。当硬件不支持你请求的某一特性时便会使用它。比如你要求显卡旋转一幅位图,如果硬件不支持旋转则HEL介入并使用软件算法来完成。显然,速度会比较慢,但重点是尽管慢了点但你的程序没有中断啊。此外,HAL与HEL之间的切换对于你而言是透明的。如果你叫DirectX去做某事,只要硬件能做则直接由HAL搞定了,否则将会调用HEL的软件模拟以完成任务。

现在,你可能会想,是不是软件的层次有点太多了。这是个问题。但事实是DirectX很简洁,以致你使用它所付的唯一代价可能就是一两个额外的函数调用。 对于2D/3D绘图、网络以及音频加速这种代价算小的了。你能想像为了画图而去替市面上每种显卡写驱动程序吗?相信我,即使花上上千年你也做不完这活。DirectX是由Microsoft和硬件厂商提供给你的高品质且强大的发布引擎。

深入了解DirectX基础组件
现在让我们来浏览一下DirectX的各个组件和它们的功能:

DirectDraw:这是主要的控制显卡以渲染和2D绘图的引擎。任何绘图都要经由它来完成,因而它可能是DirectX所有组件中最重要的一个。DirectDraw对象代表你系统中的显卡。DirectDraw在DirectX8.0中已经不可用了,因此我们要用它必须使用DirectX7.0的接口。

DirectSound:这是DirectX的音频组件。其只支持数字音频而不支持MIDI。但该组件使你不用再为了发出声音而去使用第三方的音频系统。这可使你的生活轻松了100倍。音频编程是一种黑色艺术,即使在过去也没人愿意为所有的声卡编写驱动。尽管一些厂商提供了如Miles Sound System和DiamondWare Sound Toolkit音频库。两者都是功能强劲的系统可让你简单的为你的DOS或Win32程序加载、播放数字或MIDI音频。但,有了DirectSound,DirectSound3D,以及最近的DirectMusic组件,就没有必要使用第三方库了。

DirectSound3D:这是DirectSound的3D音频组件。它使你能够在一个空间中定位3D音源,就好像音源是漂浮在房间里一样。这是一非常新的技术,但它发展的很快。现今,大部分声卡支持3D效果的硬件加速,包括多普勒转移(Doppler shift)、折射、反射以及更多。然而如果是软件模拟则这些硬件加速功能均将不可用。

DirectMusic:最近才加入DirectX。谢天谢地!DirectMusic是那个被DirectSound遗漏掉的MIDI技术。但不止于此,DirectMusic有一个新的DLS(可下载音频)系统。该系统允许你建立设备的数字表示并用MIDI控制方式播放。这很像声波合成器,但是是以软件方式。DirectMusic拥有一个新的智能化表现引擎。在实时系统中,该引擎通过你提供的模板改变你的音乐。重要的是,DirectMusic可以创建时下流行的音乐格式的音乐。强吧?

DirectInput:该系统处理一切设备输入,包括鼠标,键盘,摇杆,手柄,滚球等等。现在其还支持力反馈设备(由机电马达和力传感器组成,让你能感觉真实之力的设备)。

DirectPlay:这是DirectX的网络部分。它使你能够通过使用Internet、moden、直接电缆连接或其他任何未来的连接媒介建立抽象连接。DirectPlay之所以酷在于它允许在你对网络一无所知的情况下建立连接。你无需写驱动,用sockets或类似的东西。此外,DirectPlay支持游戏进行中的聊天概念和玩家用以聚集和游戏的游戏大厅概念。DirectPlay并不强迫你顺从多人在线游戏体系结构。它所做的一切便是替你发送和接受消息包、察看其中的内容以及检查是否对你有效。

Direct3DRM。这是Direct3D的保留模式。一个高级的,基于对象和帧的3D系统。你可用它建立基本的3D程序。虽然它利用了3D加速但运行速度并不是最快的。其非常适合用来做预排程序,模型显示或相当慢的demo。

Direct3dIM:这是DirectX的即时模式。DirectX中的低级3D支持。起初,它极难使用而且与OpenGl存在很多冲突。老的即时模式使用一种叫执行缓冲的技术,基本上,是你创建的描绘画面的数据与指令的数组,用该技术画的图丑极了。然而,自从DirectX5.0,即时模式通过DrawPrimitive()函数支持许多类OpenGL界面。其允许你把三角形和扇形发给渲染引擎并通过函数调用函数调用来改变其状态而无需使用执行缓存。因此,我现在喜欢DirectX3D即时模式了。尽管本卷以及第二卷是基于软件的3D游戏书籍,但出于完整性的考虑我仍将在卷二的末尾讨论D3DIM。事实上,在第二卷的随书CD上有一本完全关于DirectX3D即时模式的电子书。

DirectSetup/AutoPlay:这是允许一个程序从你的应用程序所在客户机上安装DirectX并在插入CD光盘后直接启动游戏的准DirectX组件。DirectSetup是个小的函数集。这个函数集合在客户机上加载DirectX的运行时文件,并在注册表中注册这些文件。AutoPlay是标准CD子系统,其寻找CD盘根目录下的AUTOPLAY.INF文件。如果找到该文件,AutoPlay便执行文件中的批处理命令。

DirectX Graphics:为了增强性能以及在2D领域应用3D效果,Microsoft决定把DirectDraw和DirectX3D的功能融合到这里。我个人不认为DirectDraw应该被移除,不仅仅是因为有好多软件使用了它,更重要的是使用DirectX3D去做2D绘图是件很痛苦的事情,其会扼杀了许多许多本质上应该是2D的应用程序,比如GUI应用以及类似的游戏。不过无论如何,我们也无需太担心这个,毕竟我们仍然可以使用DirectX7.0版接口的DirectDraw。

DirectAudio:这个融合比DirectX Graphics所带来的破坏性要小的多。这里,DirectSound和DirectMusic被更紧密的集成在一起,没有任何东西被从中移除。在DirectX7.0中DirectMusic还十分弱小,完全基于COM,无法经由DirectSound访问。在DirectAudio中,这些弱点得以改变。只要你愿意你便可以将它们在一起使用。

DirectShow:该组件被应用于Windows平台的流媒体。DirectShow提供多媒体流的高品质视频捕捉和播放。其支持众多的格式,包括ASF,MPEG,AVI,MP3,以及WAV文件。其支持使用Windows驱动模型(WDM)的设备或早期针对Windows的视频设备的视频捕捉。DirectShow是与DirectX的其他技术相集成的。只要硬件允许,它将自动侦测并使用视频和硬盘硬件加速,但其也支持不使用硬件加速。这使事情变得容易多了,因为过去当你要在一个游戏中播放一些视频时你必须要么使用第三方库要么自己写代码。当这些都被集成到DirectX的时候,呵呵,真的棒极了。唯一的问题是它太先进了,以致设置和使用起来有点痛苦。

最后,你可能很想知道关于DirectX所有版本的相关情况。这种复习可能要花上半年的时间。是的,很大程度上我们所处的行业是个大冒险,绘图和游戏技术的发展太快了。然而,由于DirectX是基于COM技术的,你写的譬如基于DirectX3.0的程序是保证能在DirectX8.0上运行的。让我们来看看是如何运行的吧……


COM:是Microsoft的作品还是魔鬼的杰作?
现今的计算机程序很容易的就能达到数百万行。而大系统更是轻易达到数十亿行。随着程序的变大,抽象和继承就显得极端的重要。否则,完全的混乱肯定接踵而至。这种混乱就如同电话公司的客户服务一样。

最近的两个把面向对象编程技术应用于计算机语言的尝试是C++和Java。C++其实是C的一种面向对象化的进化(或者,更可能是个回归){译者:括号里的,不清楚为什么这么说}另一方面,Java是基于C++的,但它完全面向对象并且非常简洁。此外,Java是个平台而C++是种语言。

总之,语言都很棒。但你如何运用它们倒是要花好久来考虑的。即使C++拥有丰富而且强大的OO(面向对象)特性,许多人还是不使用或错误地使用。以致大规模编程仍然是个苦涩的问题。而这便是COM打算处理的困难之一。

COM是多年前应用新的软件规范从零建起的,其工作原理多少有点像建电脑芯片或拼乐高玩具{译者:一种类似积木的玩具},要使他们工作起来,你只要简单地把他们插到一起就行了。每块芯片和玩具自己知道该如何做为一块芯片或玩具(因为它们的接口被很好的定义)。为了实现这种软件技术,你需要一个对于任何你能想像出来的函数集都统一的接口。这些就是COM干的好事。

电脑芯片一个很酷的方面在于,当你在设计之外再加一个芯片时你无需告诉其他的芯片你做了某些改变。如你所知,在软件编程中想做到这点是很难的。你至少要重新编译一下吧。解决这一问题是COM的另一个目标。你应该能够给一个COM对象添加新特性而无需破坏使用老COM对象的软件。此外,COM对象可以在不重新编译源程序的情况下被改变。酷吧。

由于你可以升级COM对象而无须重编译你的程序,这意味着你可以不用发布补丁和新版本便可升级你的软件。譬如,你有一个使用了三个COM对象的程序:一个实现绘图,一个实现硬盘,还有一个实现网络(见图5.3)。现在设想你卖了100,000份该程序的拷贝,但你并不打算发布100,000份的补丁!为了升级负责绘图的COM对象,你所做的一切工作便是给用户一个新的负责绘图的COM对象,然后程序将自动使用它。无须重编译,无须链接,什么也不要做。简单吧。当然,这一作用在低级水平上的技术是相当复杂的,要想写出属于自己的COM对象很具挑战性,但使用它们倒是很简单。
                    图5.3. COM概貌
 
下一个问题是:COM是如何组织的以致具有这种即插即用的属性?答案是,在COM中没有规则,不过大多数情况下COM对象是随程序下载或提供的DDL,或称动态链接库。这样它们便可以轻易的更新和改变。唯一的问题是使用COM的程序必须知道如何从一个DLL中加载COM对象。不过,我们将在后面的“构建准COM对象”章节再讨论该问题。

COM对象到底是什么?
一个COM对象实际上是一个C++类,或者实现了许多接口的一系列C++类。(基本上,一个接口便是一系列函数)这些接口被用来与COM对象通信。看看图5.4。这里我们看到一个有仨接口,分别叫IGRAPHICS, ISOUND,和IINPUT的COM对象。
                    图5.4 一个COM对象的接口
 
每个接口都有好些你可以调用的(当你知道如何调用)函数。这个简单的COM对象有一个或更多的接口,而你可以有一个或更多的COM对象。进一步的,COM规范要求你创建的任何接口必须均从一个叫做IUnknown的特殊基类派生。如果你是C程序员,这些意味着,IUnknown对象是一个构建接口的起点。
让我们来看看IUnknown类的定义:
struct  IUnknown
{

// this function is used to retrieve other interfaces
virtual HRESULT __stdcall QueryInterface(const IID &iid, (void **)ip) = 0;

// this is used to increment interfaces reference count
virtual ULONG __stdcall AddRef() = 0;

// this is used to decrement interfaces reference count
virtual ULONG __stdcall Release() = 0;

};
注意:所有的方法都是虚函数。此外方法使用了__stdcall,这意味着使用标准C/C++的参数入栈顺序。可能你还记得第二章中“Windows编程模型” 讲过__stdcall从右向左把参数压进栈中的吧。

即使你是一个C++程序员,如果你对虚函数比较生疏的话,这个类的定义也可能会让你很困惑的。总之,让我们来剖析一下这个IUnknown,看看里面到底有什么。所有继承自IUnknown的接口都应当被你实现一下,最起码QueryInterface(), AddRef(), 和Release()这三个方法你得亲自实现一下。

进一步的,COM的一条规则是如果你有一个接口你应当能够通过该接口访问到其他任何接口,只要这些接口是来自同一个COM对象。基本上,这个意味着你能从任何地方到任何地方。看看图5.5。
                      图5.5 在COM对象各接口间切换
 
技巧:通常你无须亲自对接口或COM对象调用AddRef() 。其被内置在函数QueryInterface()中。但是有时当你要增加对COM对象的记数以让COM对象误以为有更多的对该对象的引用时,那你也不得不亲自调用这个函数了。

AddRef()是个有趣的函数。COM对象使用一种叫引用记数(reference counting)技术来跟踪自己的使用情况。这种技术的使用来源自COM规范的要求而非编程语言特性。因而当一个COM对象被建立以及一个用于检索到底对该对象有多少次引用的接口被建立时便调用AddRef()。如果COM对象是用malloc()或new〔〕建立的,这是C/C++语言特性,则当引用记数减至0时对象便自行销毁。

COM对象是C++类这一事实也给我们带来了另一个问题,如何在VB,Java,ActiveX等等之中创建和使用他们呢?呵呵,那只是很碰巧的,COM的设计者使用VC++类来实现COM,但你无须非得使用C++其访问COM,甚至无须非得使用C++去创建COM。只要你创建的二进制形式与一个Microsoft C++编译器创建VC++类时创建的二进制形式相同的话,那个COM对象便可用(the COM object will be COM-compliant)。当然,大多数编译器产品有辅助功能或工具用以帮助生成COM对象,所以这不是什么大问题。关于这个问题更棒的是你可以用C++、VB或Delphi写一个COM对象,然后这个COM对象可以被任何语言所使用!内存中的二进制形式总是一样的。

Release()用来减少对一个COM对象或接口的引用记数。大多数情况下,当你用完一个接口时你必须调用这个函数。某些时候下,如果你创建了一个对象然后在这个对象里又创建了另一个对象,此时调用父对象的Release()时将一并调用子对象或派生对象的Release()。不过,以访问顺序相反的顺序调用Release()是个不错的主意哦。

有关接口ID和GUID的更多信息
如我早先提到的那样,每一个COM对象和接口必须有一个用以请求和访问的唯一的128位标识符。这个数字一般叫做GUID(全局唯一标识符)。具体说来,当定义COM接口时,接口有接口ID或称之IID。为了生成这些ID你需要使用一个由Microsoft写的叫GUIDGEN.EXE的程序(或者一个用相同算法写的类似程序),图5.6显示了GUIDGEN.EXE。
              图5.6 GUIDGEN.EXE
 
你所要做的是选一个你要的ID类型(有四种不同的格式),然后该程序会生成一个保证在任何时间任何机器上都不会再次被生成的128位的向量。看上去可能吗?当然不是。这只是数学和概率论意义上的。总之,管用,就别老问“为什么”问个不停了吧。

当你生成了GUID或IID之后,这些ID在你的剪贴板上,你可以把它粘贴到你的程序里。下面是一个我在写这段时生成的IID的例子:
// { C1BCE961-3E98-11d2-A1C2-004095271606}
static const <<name>> =
{ 0xc1bce961, 0x3e98, 0x11d2,
{ 0xa1, 0xc2, 0x0, 0x40, 0x95, 0x27, 0x16, 0x6 } };
当然,<<name>> 处会用你在你程序中为GUID选择的名字替换。你能明白我在说什么吧。

GUID和IID被用来引用COM对象和他们的接口。所以无论何时创建一个新COM对象和一系列接口时,这些数字是你唯一需给那些想使用该COM对象之程序员的东西。一旦他们得到了IID,他们就能创建COM对象和接口。

创建一个类COM对象
创建一个全功能的COM对象将大大超出本书的范围。你只需要知道如何使用它们即可。但是如果你像我一样喜欢刨根问底,那下面我们要做的便是创建一个非常基本的COM对象,作为例子,用以帮助你解答一些问题。

好的,你知道所有COM对象都有一系列的接口,且所有COM对象都必须先从一个IUnknown类派生。然后,一旦你建立了所有的接口,你将把它们放到一个容器类中,接着实现所有的东西。比如,让我们创建一个含有三个接口的COM对象,三个接口是:ISound,IGraphics,以及IInput。你可能像下面这般定义它们:
// graphics 接口
struct IGraphics : IUnknown
{
virtual int InitGraphics(int mode)=0;
virtual int SetPixel(int x, int y, int c)=0;
// 其他方法...
};

// sound 接口
struct ISound : IUnknown
{
virtual int InitSound(int driver)=0;
virtual int PlaySound(int note, int vol)=0;
// 其他方法...
};

// input 接口
struct IInput: IUnknown
{
virtual int InitInput(int device)=0;
virtual int ReadStick(int stick)=0;
// 其他方法...
};

现在你已经定义了所有的接口, 让我们创建容器类吧, 该类是COM对象的核心:

class CT3D_Engine: public IGraphics, ISound, IInput
{
public:

// 在这里实现 IUnknown
virtual HRESULT __stdcall QueryInterface(const IID &iid,
                                        (void **)ip)
{ /* real implementation */ }

// this method increases the interfaces reference count
virtual ULONG __stdcall Addref()
                        { /* real implementation */}

// this method decreases the interfaces reference count
virtual ULONG __stdcall Release()
                        { /* real implementation */}

// note there still isn't a method to create one of these
// objects...

// 现在实现每个接口。

// IGraphics
virtual int InitGraphics(int mode)
                     { /*implementation */}
virtual int SetPixel(int x, int y, int c)
                     {/*implementation */}


// ISound
virtual int InitSound(int driver)
                     { /*implementation */}
virtual int PlaySound(int note, int vol)
                     { /*implementation */}

// IInput
virtual int InitInput(int device)
                     { /*implementation */}

virtual int ReadStick(int stick)
                     { /*implementation */}

private:

// .. locals

};

注意:你仍然向往一个通用的创建COM对象的方法是吗?这无疑确实是个问题。COM规范规定中有很多方法可以做到这一点,但其中没有一个能把实现和具体的平台或语言联系在一起。一个做到这一点的简单方法是创建一个叫 CoCreateInstance()或ComCreate()的函数,用以创建初始化实例对象的IUnknown。该函数经常加载一个含有COM代码的DLL然后从该代码出运行。再一次的,这项技术超出了你需要知道范围,我只是把这个技术扔到你面前任你处置罢了。我将跳过这些,以这个例子继续我们的讨论。

从这个例子你可以看出,COM接口和其编码除了使用了惯用的C++虚类之外什么也没做。但是真正的COM对象要在此处被恰当的编码和注册,以及加入许多必须的规则。但是最起码的,它们是带方法、函数指针或其他东西的类(或者如果你是C程序员,结构)。总之让我们进行一个简明的回溯以复习你已学到的有关COM的知识。

COM的简要说明
COM是一种编写软件组件的新方法。它使你能够创建动态链接到运行时上并可复用的软件模块。每一个COM对象都有一个或多个做实际工作的接口。这些接口不过是方法或经由虚函数表指针引导的函数的集合。(更多介绍在下一章)

由于使用了GUID,每一个COM对象和接口都是被唯一标识的。当然,你必须为你的COM对象生成GUID。你使用GUID或IID访问COM对象、接口以及与其他程序员共享COM对象和接口。

如果你创建一个新的COM对象来升级旧的,你必须与新接口一起实现旧接口。这是一条非常重要的原则:所有基于COM对象的程序无须重新编译,在新版本的COM对象下仍将正常运行。

COM是一个能够被任何语言在任何机器上实施的通用技术规范。唯一的要求是COM对象的二进制形式必须与由MicrosoftVC编译器生成的虚类之二进制形式相同。此外,COM可以使用在别的机种上,像Mac,SGI等等,只要它们遵守建立和使用COM对象的规范。

最后,COM通过其组件级通用架构,增进加了创建大型计算机程序(数十亿行规模)的可能。而且当然,DirectX、OLE和AcriveX都是基于COM的,所以你需要理解COM。

一个可运行的COM程序
作为一个创建COM对象和若干接口的完整例子,我已经为你创建了DEMO5_1.CPP。这个程序实现一个叫CCOM_OBJECT的COM对象。这个对象由两个接口组成,分别叫IX和IY。这个程序是一个正规COM对象的实现。不过当然,其中省略了许多诸如创建DDL,动态加载等等高级细节,但是一旦所有的方法和IUnknown类被建立,COM对象就完全实现了。

我要你做的工作是仔细的阅读代码,修改代码来看看它是如何工作的。清单5.1包括了COM对象和一个简单C/C++ main()驱动函数的全部代码。
清单 5.1 一个完整的COM对象程序
// DEMO5_1.CPP - A ultra minimal working COM example
// NOTE: not fully COM compliant

// INCLUDES ///

#include <stdio.h>
#include <malloc.h>
#include <iostream.h>
#include <objbase.h> // 注意你必须包含这个头文件。
                     // 它包含了你必须在COM程序中
                     // 使用的重要常量

// GUIDS //

// these were all generated with GUIDGEN.EXE

// {B9B8ACE1-CE14-11d0-AE58-444553540000}
const IID IID_IX =
{ 0xb9b8ace1, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };


// {B9B8ACE2-CE14-11d0-AE58-444553540000}
const IID IID_IY =
{ 0xb9b8ace2, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };

// {B9B8ACE3-CE14-11d0-AE58-444553540000}
const IID IID_IZ =
{ 0xb9b8ace3, 0xce14, 0x11d0,
{ 0xae, 0x58, 0x44, 0x45, 0x53, 0x54, 0x0, 0x0 } };


// INTERFACES /

// define the IX interface
interface IX: IUnknown
{

virtual void __stdcall fx(void)=0;

};

// define the IY interface
interface IY: IUnknown
{

virtual void __stdcall fy(void)=0;

};

// CLASSES AND COMPONENTS /

// define the COM object
class CCOM_OBJECT :    public IX,
                    public IY
{
public:

    CCOM_OBJECT() : ref_count(0) {}
    ~CCOM_OBJECT() {}

private:

virtual HRESULT __stdcall QueryInterface(const IID &iid, void **iface);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();

virtual    void __stdcall fx(void)
              {cout << "Function fx has been called." << endl; }
virtual void __stdcall fy(void)
              {cout << "Function fy has been called." << endl; }

int ref_count;

};

// CLASS METHODS //

HRESULT __stdcall CCOM_OBJECT::QueryInterface(const IID &iid,
                                              void **iface)
{
// this function basically casts the this pointer or the IUnknown
// pointer into the interface requested, notice the comparison with
// the GUIDs generated and defined in the beginning of the program

// requesting the IUnknown base interface
if (iid==IID_IUnknown)
    {
    cout << "Requesting IUnknown interface" << endl;
    *iface = (IX*)this;

    } // end if

// maybe IX?
if (iid==IID_IX)
    {
    cout << "Requesting IX interface" << endl;
    *iface = (IX*)this;

    } // end if
else  // maybe IY
if (iid==IID_IY)
    {
    cout << "Requesting IY interface" << endl;
    *iface = (IY*)this;

    } // end if
else
    { // cant find it!
    cout << "Requesting unknown interface!" << endl;
    *iface = NULL;
    return(E_NOINTERFACE);
    } // end else

// if everything went well cast pointer to
// IUnknown and call addref()
((IUnknown *)(*iface))->AddRef();

return(S_OK);

} // end QueryInterface

///

ULONG __stdcall CCOM_OBJECT::AddRef()
{
// increments reference count
cout << "Adding a reference" << endl;
return(++ref_count);

} // end AddRef

///

ULONG __stdcall CCOM_OBJECT::Release()
{
// decrements reference count
cout << "Deleting a reference" << endl;
if (ref_count==0)
    {
    delete this;
    return(0);
    } // end if
else
    return(ref_count);

} // end Release

///

IUnknown *CoCreateInstance(void)
{
// this is a very basic implementation of CoCreateInstance()
// it creates an instance of the COM object, in this case
// I decided to start with a pointer to IX ?IY would have
// done just as well

IUnknown *comm_obj = (IX *)new(CCOM_OBJECT);

cout << "Creating Comm object" << endl;

// update reference count
comm_obj->AddRef();

return(comm_obj);

} // end CoCreateInstance

//

void main(void)
{

// create the main COM object
IUnknown *punknown = CoCreateInstance();

// create two NULL pointers the IX and IY interfaces
IX *pix=NULL;
IY *piy=NULL;

// from the original COM object query for interface IX
punknown->QueryInterface(IID_IX, (void **)&pix);

// try some of the methods of IX
pix->fx();

// release the interface
pix->Release();
// now query for the IY interface
punknown->QueryInterface(IID_IY, (void **)&piy);

// try some of the methods
piy->fy();

// release the interface
piy->Release();

// release the COM object itself
punknown->Release();

} // end main
我已经预先为你编译了一个可执行程序DEMO5_1.EXE,但是如果你想要做些试验并想编译DEMO5_1.CPP,务必记住创建一个Win32控制台应用程序。因为demo使用的是main()而非WinMain()。


与DirectX COM对象协同工作
现在你对DirectX是什么以及COM是如何工作的有概念了吧。让我们再对它们是如何协同工作的进行进一步的研究。如我所说,DirectX是由许多COM对象组成的。当你加载运行时版本的DirectX时,COM对象是以DLL形式存在于你的系统中的。当你运行第三方的DirectX游戏时,游戏将通过DirectX程序加载一个或多个DLL,然后游戏申请接口,接着接口所对应的方法被用来完成工作。这便是运行时的事了。

在“编译时三角”方面,情况有些不同(The compile-time angle is a little different)。DirectX的设计者知道他们面对的是游戏开发者,而且他们假设大多数游戏开发者痛恨Windows编程方式。唉,于是他们明白最好把COM做得小些,小些,再小些,否则游戏程序员也同样会恨DirectX。于是,90%的DirectX COM对象--相对于COM对象--被包装成非常小的函数调用。所以,你无须为了初始化COM而去调用CoCreateInstance()以及诸如此类的函数。DirectX试图隐藏起使用COM对象时那烦琐讨厌的细节,以使你轻易使用DirectX提供的核心功能。

综上所述,要编译一个DirectX程序你必须包含一系列包装了COM的输入库,这样你就可以调用DirectX、使用这些被包装的函数、创建COM对象。大多数情况下,下面是你需要的库:
DDRAW.LIB
DSOUND.LIB
DINPUT.LIB
DINPUT8.LIB
DSETUP.LIB
DPLAYX.LIB
D3DIM.LIB
D3DRM.LIB
但是记住,这些输入库并不包含COM对象本身。这些只是包装库,其只是做出加载DirectX DLL的调用,而DirectX DLL才真正包含COM对象。最终,当你调用DirectX COM对象时,调用的结果往往是一个接口的指针。指针指向的地方才是真正做事情的东西。就如例子DEMO5_1.CPP一样,一旦你获得了接口的指针,你就可以随意调用函数了,或用C++术语说:方法调用。但是,如果你是一个C程序员,若你对函数指针不甚了解,可以浏览一下下一节的内容。如果你是一个C++程序员,如果你愿意,你大可跳过下节。

COM和函数指针
一旦你创建了一个COM对象并获得一个接口指针,则你真正获得的是一个VTABLE(虚函数表)指针。看看图5.7。虚函数的应用使你能够编写那些只有到“运行时”的时候才能确定调用哪个函数的函数调用。这是COM和虚函数的关键。本质上说,C++便是植根于此的。但你也可以直接用函数指针在C中实现同样的功能。
       图 5.7. 虚函数表结构
 
函数指针是一种用来调用函数的指针。与被硬绑定到某些代码上的函数不同,只要函数指针的原型说明与你指向的那个函数相同,你便可以任意移动函数指针。举个例子,譬如你要写一个把一个像素绘制到屏幕上的绘图驱动程序。但是再假定你要支持许多不同的显卡,而且这些显卡工作方式迥异。如图5.8所示。
           图 5.8. 需要支持不同显卡的软件设计
 
你想要对这些不同显卡以相同方式调用绘像素函数,但当插入不同显卡时,绘像素函数内部的代码是不同的。下面是典型的C程序员的解决方法。
int SetPixel(int x, int y, int color, int card)
{
// what video card do we have?
switch(card)
      {
      case ATI:    { /* hardware specific code */ } break;
      case VOODOO: { /* hardware specific code */ } break;
      case SIII:   { /* hardware specific code */ } break;
      .
      .
      .
      default:     { /* standard VGA code */  } break;

      } // end switch

// return success
return(1);

} // end SetPixel

你看到问题之所在了吗?首先,那个switch语句太恐怖,它运行慢,代码长,易出错,而且你还要在加入另一种新支持的显卡时修改函数。一个比严格C更好的解决方法是像下面那样使用函数指针:

// function pointer declaration, weird huh?
int (* SetPixel)(int x, int y, int color);
// now here's all our set pixel functions

int SetPixel_ATI(int x, int y, int color)
{
// code for ATI

} // end SetPixel_ATI

///

int SetPixel_VOODOO(int x, int y, int color)
{
// code for VOODOO

} // end SetPixel_VOODOO

///

int SetPixel_SIII(int x, int y, int color)
{
// code for SIII

} // end SetPixel_SIII
现在,你就准备好吃惊吧。当系统启动时,它检查安装了哪种显卡,然后一次且仅一次设置通用函数指针,使之指向正确的显卡函数。比如,你想要SetPixel()指向ATI版本的函数,则你可以这样编码:
// assigning a function pointer
SetPixel = SetPixel_ATI;
是不是很简单?图5.9图形化的显示了这一切。
                  图 5.9. 使用函数指针去调用不同的代码块
 
注意:从某种程度上说SetPixel()是SetPixel_ATI()的别名。这是函数指针的关键。现在你用通常的调用方式调用SetPixel(),但是并不是调用空的SetPixel(),真正调用的是SetPixel_ATI():
// this really calls SetPixel_ATI(10,20,4);
SetPixel(10,20,4);

重点是你的编码将一直是一样的。只是针对你赋给函数指针的不同值,函数指针将代表不同的函数,干不同的事。所有虚函数都用到函数指针,并且与编程语言完美结合,如你刚才所做的那样。

基于这种思想,我们将看到你是如何完成你的通用显卡驱动的设计的。你所要做的一切便是测试一下到底安装了哪个显卡,然后设定一次以使SetPixel()指向合适的SetPixel*()函数。大功告成了。看看下面的代码:
int SetCard(int card)
{
// assign the function pointer based on the card
switch(card)
      {
      case ATI:
           {
           SetPixel = SetPixel_ATI;

           } break;

      case VOODOO:
           {
           SetPixel = SetPixel_VOODOO;
           } break;

      case SIII:
           {
           SetPixel = SetPixel_SIII;
           } break;

      default: break;

      } // end switch

} // end SetCard
在你代码的开头,你调用一个设置函数,比如:
SetCard(card);
之后的事你很熟悉了吧。这便是如何在C++中使用函数指针和虚函数的。下面让我们来看看这些技术是如何用于DirectX的。

创建和使用DirectX接口
此刻,我想你已经深知:COM是接口的集合,而接口是简单的函数指针(确切地说是VTABLE)。从而,应用一个DirectX COM对象时,你所要做的一切便是创建它,获取一个接口指针,然后用适当的语法对接口调用函数。譬如,我用主DirectDraw接口来演示一下:
首先,你需要三样东西来用DirectDraw做试验。
·DirectDraw运行时COM对象和DDL必须被加载和注册。这是DirectX安装程序干的事情。
·你必须在你的win32程序中包含 DDRAW.LIB输入库,以便可以链接到你调用的函数(译者注:这点初学者常忘记,以VC6.0为例,方法是在 工程->设置->Link页中的“对象/库模板”域中的最后键入所需输入库的全名,包括扩展名)。
·你必须在你的程序中include DDRAW.H,这样编译器才能够识别到DirectDraw的头文件信息、原型声明和数据类型。

知道这些之后,下面是DirectDraw1.0接口指针的数据类型:
LPDIRECTDRAW lpdd = NULL;
以及DirectDraw4.0接口指针的数据类型:
LPDIRECTDRAW7 lpdd = NULL;
以及DirectDraw7.0的:
LPDIRECTDRAW7 lpdd = NULL;
不过没有8.0的了。

现在,创建创建DirectDraw COM对象并获得DirectDraw对象的接口指针(DirectDraw对象代表显卡),你所要做的一切便是像下面这般使用函数DirectDrawCreate():
DirectDrawCreate(NULL, &lpdd, NULL);
这将返回基本的DirectDraw1.0接口。在第六章“初次接触:DirectDraw”我将讨论有关参量的细节。但现在,只要知道,这个调用创建一个DirectDraw对象,并把接口指针赋给lpdd。

现在,你已经准备好针对DirectDraw做些调用了。暂且留步!你还不知道可以用哪些函数或方法呢,呵呵,这正是你读本书的目的。作为例子,下面的代码告诉你如何设置一个640×480,256色深的视频模式:

lpdd->SetVideoMode(640, 480, 256);

除了简单你还能说什么呢?唯一额外的工作便是对DirectDraw接口指针lpdd进行的“->”操作。当然,真正发生的事情是检索接口的虚函数表,但在这里并看不出这个。

本质上说,任何对DirectX的调用均是下面的形式:

interface_pointer->method_name(parameter list);

当然,通过使用QueryInterface(),你可以从最初的DirectDraw接口跳转到任何你想使用的接口上(比如DirectX3D)。甚至,由于有多个DirectX版本,而Microsoft又于不久前停止编写获得任何最新界面的函数,所以有时,你不得不自己使用QueryInterface()手动获得最新版本的DirectX接口。让我们对此研究研究。

接口的查询
DirectX的一个诡异的地方是所有的版本号均不同步。这还真是个问题,而且绝对是个导致困惑的源头。下面是事情的缘由:当第一个DirectX版本发布时,DirectDraw的接口被命名为:
IDIRECTDRAW
然后,当DirectX2.0发布时,DirectDraw被更新到2.0。于是我们便有了:
IDIRECTDRAW
IDIRECTDRAW2
现在,当DirectX6.0发布时,我们看到的是:
IDIRECTDRAW
IDIRECTDRAW2
IDIRECTDRAW4
IDIRECTDRAW7
而到了DirectX8.0,便不再支持DirectDraw了。所以你所拥有的最新版的DirectDraw接口便只是IDIRECTDRAW7了。明白了吗?

等一下!咋没接口3和接口5?我也不知道,但是这确实是个问题。但关键是,就算你使用DirectX8.0,这并不意味着接口也被更新到8.0。进一步说,DirectX版本号和各接口的版本号以及各个接口版本号之间可以是完全的不同步。DirectX6.0的DirectDraw接口版本到IDIRECTDRAW4,但是DirectSound接口只到1.0,那时叫IDIRECTSOUND。瞧这个乱啊。上述叙述是想告诉你,当你使用一个DirectX接口时,你应当确保你使用的接口是最新版的。如果你不确定,那使用1.0版接口的指针,然后通过通用创建函数以得到最新的接口版本。

为了解释一下刚才我到底在说些什么,下面是个例子:DirectDrawCreate()返回一个1.0版的接口指针,但是DirectDraw已经到了IDIRECTDRAW7了,那么你如何才能利用到新功能呢?
有两种方法:使用低级COM函数,或使用QueryInterfaced()。让我们使用后者吧。整个过程大致如下:首先,你DirectDrawCreate()以创建DirectDraw COM接口,其返回讨厌的IDIRECTDRAW接口指针。然后,你使用这个指针来调用QueryInterface(),并通过接口ID(或GUID)获取IDIRECTDRAW7指针。下面是个例子:
LPDIRECTDRAW  lpdd;   // version 1.0
LPDIRECTDRAW7 lpdd7;  // version 7.0

// create version 1.0 DirectDraw object interface
DirectDrawCreate(NULL, &lpdd, NULL);

// now look in DDRAW.H header, find IDIRECTDRAW7 interface
// ID and use it to query for the interface
lpdd->QueryInterface(IID_IDirectDraw7, &lpdd7);

此刻,你有两个接口指针。但你不需要指向IDIRECTDRAW的指针。所以你应该释放该指针。
// release, decrement reference count
lpdd->Release();

// set to NULL to be safe
lpdd = NULL;

记住了吗?当你用完了之后你应当释放接口。因而,当你的程序结束时你同样应当释放IDIRECTDRAW7接口,如下:
// release, decrement reference count
lpdd7->Release();

// set to NULL to be safe
lpdd7 = NULL;

Ok,现在你知道如何从一个接口得到另一个接口指针了吧。够烦琐的吧,在DirectX7.0中我们见到了一缕希望。Microsoft添加了一个新函数DirectDrawCreateEx(),其直接返回IDIRECTDRAW7接口。神奇吧?但他们又在DirectX8.0中枪毙了该函数。但谁在乎呢?反正我们仍然可以使用这个函数:
HRESULT WINAPI DirectDrawCreateEx(
  GUID FAR *lpGUID,  // the GUID of the driver, NULL for active display
  LPVOID *lplpDD,    // receiver of the interface
  REFIID iid,       // the interface ID of the interface you are requesting
  IUnknown FAR *pUnkOuter  // advanced COM, NULL
);
这个新函数允许你发送一个iid以表明想要请求的DirectDraw版本,然后函数将为你创建一个COM对象。我们只需这样调用函数:

LPDIRECTDRAW7 lpdd;  // version 7.0

// create version 7.0 DirectDraw object interface
DirectDrawCreateEx(NULL, (void **)&lpdd, IID_IDirectDraw7, NULL);

基本上,对DirectDrawCreateEx的调用直接创建一个所要求的接口。所以你无须用DirectDraw1.0做为中介了。好了,这便是所有有关使用DirectX和COM的知识。当然,你还没有看到DirectX组件拥有的数百个函数和接口。不过后面你将看到的。

COM的未来
目前,已经发布了许多类似COM的技术,比如CORBA(Common Object Request Broker Architecture)。但是,由于你只关心Windows游戏,其他技术便显得不是很重要了。
最新版本的COM是COM++,其更强大,规则更合理,以及深思熟虑的实现细节。COM++使分布式软件组件开发更加容易。诚然,COM++比COM要复杂一点,不过嘿嘿,这就是生活。
除了COM和COM++还有一个完全Internet/intranet版本的COM叫做DCOM(分布式COM)。拜DCOM所赐,COM对象甚至无须在你的机器上,他们可以由网络上的其他机器提供。酷不酷?试想一下,你的程序作为一个强大DCOM服务器下的客户端。照我看,真是个难以置信的技术。

摘要
本章覆盖了一些有趣的技术资料和概念。COM不容易理解,所以要想理解它还真的要刻苦学习一下。但是,使用COM就太简单了,这一点你将在下一章看到。总之,你浏览了一下DirectX和它所有的组件。所以,在下面的章节中,一旦你知道了每个组件的细节,并且知道如何使用它们,你便会对它们如何协调在一起有个更好的认识了。

 

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值