Windows可执行文件呈现两种形式:程序和动态链接库(DLL)。当应用程序启动时(DLL的静态链接),或者当它们在运行期间装载DLL和访问它时(DLL的动态链接),将会发生动态链接。通过动态地装载DLL,动态的应用程序才成为了可能。调用全局函数但又需要额外支持的应用程序可以欣然地享受这个特性,假如程序员需要一个暴露类和方法的DLL。正如读者将要看到的,这个额外支持可以用于 DelPhi包或.NET组件。然而,在开始这个讨论之前,本章将先介绍传统的DLL,它们是这个论题的基础。
.NET体系结构的重要特性之一是它将一个应用程序分割成多个组件、一个可执行组件和一些库组件。这些组件能被直接链接到应用程序上,并在程序启动时或者在被需要时得到装载。利用Win32上的Delphi包,一个类似的体系结构过去是可能的,而现在仍是可能的。正如我们在本章中将要讨论的,这是用来在DeIPhi中开发动态应用程序的首选技巧。
Wind32中的DLL
本书的第2章重点强调了Win32操作系统体系结构的几个元素,其中就包括DLL的重要作用。本章的目标是提供一些比较专业的细节,然后深入探讨怎样在DeIPhi开发DLL。
什么是动态链接
首先,重要的是需要完全理解函数或过程的静态链接与动态链接之间的差别。如果一个子例程在一个源文件中不是可直接获得的,编译器将把这个子例程添加到一个内部表上。当然,DelPhi编译器必须已查到子例程的声明,并知道它的参数与类型,否则它将发布一条错误消息。
当一个正常(即静态链接)的子例程通过了编译之后,连接器从一个Delphi编译单元(或者说静态库)中取出该子例程的编译代码,并将它添加到应用程序的代码上。最终的可执行文件将包含应用程序和各个相关单元的所有代码。Delphi连接器是非常聪明的,它只从应用程序所涉及的单元中提取出最少数量的代码,而且只链接得到实际使用的函数与方法。这也是它叫做“智能连接器”的原因。
注意: 这条规则的一个显著例外是虚方法的包含。编译器无法事先确定程序将要调用哪些虚方法,因此它必须全部包含这些虚方法。由于这一缘故,带有许多虚函数和库的程序往往会生成较大的可执行文件。在开发VCL时,Borland开发人员必须平衡利用虚函数所获得的灵活性与通过限制虚函数所实现的可执行文件大小的减小。
就动态链接(发生在程序的代码调用DLL函数的时候)而言,连接器使用子例程的exteral声明中所包含的信息来创建可执行文件内的一个导人表。Windows在将可执行文件装载到内存中时,首先装载所有必需的DLL,然后应用程序才启动。在这个装载过程中,Windows用DLL函数在内存中的地址填写应用程序的导人表。如果由于某些原因而没有找到DLL,或者一个得到引用的子例程没有出现在找到的DLL中,则应用程序将不启动。
每当应用程序调用一个外部函数时,它就使用这个导人表将该调用转发给DLL代码(它现在被装载在应用程序的地址空间中)。需要注意的是,这一模式不涉及两个不同的应用程序。DLL已经变成了运行程序的一部分,并被装载在同一个地址空间中。所有的参数传递均发生在应用程序的栈上(因为DLL没有一个单独的堆栈),或者发生在CPU寄存器中。由于DLL被装载到应用程序的地址空间内,所以该DLL和它所产生的全局数据的任何内存分配都将驻留在主进程的地址空间内。因而,数据和内存指针能够直接从LL被传递给应用程序,反之亦然。
还存在使用DLL的另外一个技巧,这一技巧甚至比我们刚才讨论的技巧具有更大的动态性:在运行期间,程序员可以将一个DLL装载到内存中,搜索一个函数(假定程序员知道它的名称),然后按名称调用这个函数。这一技巧需要更复杂的代码,并需要花费额外的时间来查找函数的位置。然而,函数的执行在速度上将与调用隐式装载的DLL相同。从积极的方面看,程序员不需要拥有用来启动应用程序的DLL。读者将在本章较后面的Dyna-Call示例中使用这一技巧。
DLL的用途
由于读者已经对DLL如何工作有了一个大致的了解,所以我们现在可以开始讨论在Win32开发中使用DIJ一的原因。类似的概念也适用于.NET库组件。首先,第一个好处是,如果不同的程序使用同一个DLL,那么只需将这个DLL装载到内存中一次即可,从而节省系统内存。DLL被映射到每个进程(每个正在运行的应用程序)的私有地址空间内,但它们的代码到内存中的装载只需进行一次。
注意: 更精确地说,操作系统将会设法把DLL装载到每个应用程序的地址空间内的同一个地址处(通过使用DLL中所指定的首选基址)。如果该地址在某个具体应用程序的虚拟地址空间内是不可获得的,那么用于那个进程的DLL代码映像必须被重新定位—在性能和内存使用方面均开销很大的一个操作,因为重新定位是逐个进程地进行的,而不是系统级的。
另一个有趣的特性是,程序员可以提供一个DLL的不同版本,代替当前的DLL,而且无需重新编译正在使用当前DLL的应用程序。当然,这一技巧只有当DLL中的函数与前一个版本有相同的参数时才管用。即使DLL有了新的函数也无关紧要。只有当DLL的较旧版本中的一个函数在新版本中不存在,或者当一个函数接受一个对象引用并且那些类、基础类乃至编译器版本不匹配时,才可能会出现问题。
第二个好处特别适用于复杂的应用程序。如果程序员有一个非常大而且要求频繁更新或缺陷纠错的应用程序,可以将它分解成一些可执行文件和动态库,这样程序员就能够仅分布那些修改过的部分,而不是分布单个大的可执行文件。这么做对Windows系统库来说格外有意义。如果Microsoft给用户提供Windows系统库的一个更新版本——比如在操作系统的一个版本或一个服务包中,那么程序员一般不必重新编译他们的代码。使用库来创建灵活的应用程序在.NET中已经得到更进一步的推进,因为.NET提供了特殊和改进的动态装载支持、版本管理以及安全支持。
另一个常见的技巧是使用动态库来专门存储资源。程序员可以建立一个DLL的不同版本来包含用于不同语言的字符串,然后在运行时改变语言;程序员也可以准备一个包含图标与位图的库,然后将这些图形元素用在不同的应用程序中。开发一个程序的专用语一言版本是 特别重要的,而且VCL库和.NET均包含了对这种开发的支持。Delphi IDE利用它的Intrerated Translation Environment(集成转换环境,简称ITE)帮助程序员做这种开发。
另外一个关键优点是DLL独立于编程语言。大多数Windows编程环境,其中包括终端用户应用程序中的大部分宏语言,都允许程序员调用被存储在DLL中的函数。然而,这一灵活性只适用于函数的使用。当程序员需要共享类和对象时,编程语言之间的差别会引起许多麻烦。例如,调用一个用C++编写的DLL会导致参数的名称撕裂问题。为了共享一个跨编程语言的库中的对象,Microsoft首先推出了COM基础结构和现在的.NET体系结构, 它们都是从头开始被设计的,目标是以一种语言独立的方式处理和共享对象。
Delphi DLL编写者应遵守的规则
Win32上的Delphi DLL程序员应该遵守以下几条规则: 1、DLL的项目声明由library关键字指出,这个关键字取代了program关键字。另外一个可选关键字是Package。在.NET中,库和包项目都产生库组件,其中包是常规代码共享的首选方法。 2、由外部程序调用的DLL函数或过程必须被列举在DLL的exports子句中。这使得外部世界可以看见这些子例程。 3、导出的函数还应该被声明为stdcall,以便使用标准的Win32参数传递技巧来替代优化的register参数传递技巧(它是Delphi中的默认设置)。这条规则的例外情况是如果程序员只打算从其他Delphi应用程序中使用这些库。当然,程序员也可以使用另外一个凋用 约定,假定另一个编译器理解这个约定(比如C编译器上的默认设置cdecl)。 4、DLL的参数类型应该是默认的Windows类型(大部分是C兼容的数据类型),如果程序员希望能够将该DLL用在其他开发环境中。导出字符串还需要遵守另外的规则,就像读者将在FirstDLL示例中所看到的。 5、DLL可以使用被其他调用应用程序所共享的全局数据。每当一个应用程序装载一个DLL时,它将把这个DLL的全局数据装载在它自己的地址空间中,就像读者将在DLLMem示例中所看到的。 6、Delphi库应该捕捉所有的内部异常,除非程序员只打算从其他Delphi程序中使用这个库。
使用现有的DLL
在本书的许多例子中,读者在调用 Windows API函数时就已经用到了一些现有的DLL。读者可能还记得,所有的API函数都被声明在Windows系统单元中(在Delphi for Win32中)。函数被声明在这个单元的interface部分内,如下所示:
function PlayMetaFile(DC:HDC;MF:HMETATAFILE):BOOL;stdcall; function PaintRgrt(DC:HDC;RGN:HRGN):BOOL;stdcall; function PolyPolygon(DC:HDC;var:POints;Var:nPOints;p4:Integer):BOOL;stdcall; functfon PtlnRegion(RGN:HRGN:pZ3:Integer):stoL;stdcall; |
然后.这个单元在imp卜mentation部分内引用了一个DI上中的外部定义,而不是提供每个函数的代码: const gdi32。gdf3Z.d77; function PlayMetaFile;external gdi32 "a.eP7awetaFf7e; funchon PaintggnZ exte.nalg山32 n训田 内fntR卯; function yolyPolygon;externalg山32 n回腿Po7yPo7y卯n; functfon PtlnRegfo;exte.nalg山32 na阴PtlnRe外on; 这些函数的外部定义引用了它们所使用的那个*I*一的名称。该DI*.的名称必须包含.DI。!。扩展名,否则程序在*h渤W*l2000以P下将不工作(但可以在Wn渤*s打下工作)。另一个7h素是 I)IJ函数的名称。如果 DelPhi函数(或过程)的名称与 DIJ函数的名称相匹配(区分大小写),那么name指令不是必需的。 要想调用一个驻留在某个IjIJ。中的函数,读者可以像上面介绍的那样,在一个单元的接口部分或者在实现部分内的外部定义中提供这个函数的声明;如果这个函数只是被局部地用在一个单元中,读者也可以仅在这个单元的实现部分内的外部定义中提供这个函数的声明。一l。J函数得到了适当的定义,读者就可以像调用其他任何函数一样在自己的DelPhi应川程序代码中调用它。 提示:1)elph包含了大量*1ndows川’1的Delph语言转换。Delph的Source\Rtl\Win文件夹内的许多文件都含有这样的转换。引用其他 Window。API的其他1)叫)hi单元可以在 I)elPhi Jed项目(www.deIPhijed.org)中找到。 上如读者在第 9章中已见过的,Delphi for.NET以一种相似的方式但有时使用不同的参数声明了 Windows API函数,并使用了*-l.import表征,而不是external定义。在下一章中,读者将会看到关于 DI-I。import和 Plnvoke的较详细描述。 在DelPhi中开发DLL 除了使用在其他环境中编写的DI*.之外,程序员还可以使用DelPhi来开发可供DelPhi应用程序使用,或者能够用在其他任何一个支持DI*一的开发工具中的DI.I-。在Delphi中开发DIJ是如此容易,以至于程序员可能会滥用这个特性。一般说来,读者应该尽量开发包而不是普通的DIJ.。正如我们在本章前面讨论过的,包常常含有构件,但它们也可以含有普通的非构件类,以便程序员能够编写面向对象的代码并有效地复用它。当然,包也可以含有简单的例程、常量、变量等。 正如前文中曾经提到过的,当程序的某一部分代码需要频繁地改变时,建立一个DI.I一是非常有帮助的。在这种情况下,读者可以经常替换DI.I-,同时让程序的其他部分保持不变。同样,当需要编写一个给不同用户组提供不同特性的应用程序时,可以给那些用户分发一个**.的不同版本。 作为探讨怎样使用Delphi来开发DIJ的一个起点,笔者将介绍一个用Delphi开发的库。首先,在 New items对话框的 Delphi Projects页面上选择*-I-Wizard选项。这个操作将创建一个以 library关键字开头的、非常简单的源文件,其中这个关键字指出读者需要开发一个*上.而不是可执行文件。 现在,可以给这个库添加例程,并将它们列举在一11.l中,读者可以看到 FirstDll示例的一个简化版本实际的示例代码比这段代码复杂得多)。 程序清单 111 Fi。tDll示例库的初始版本的代码 libr邑ry Firstdll; u瞩@羹 SysUti s,Wi ndows; 条exports声明语句中。在程序清单 (正如接下来的几小节中所描述的, function Triple(N:Intege 回@回In tr歹 Result:。N”3; except RUSUlt:=1; end; end; funct卡on Double(N:Integel beqln ti’歹 RUSUlt:·N # 2; 6xC6Pt RUSUlt:=-1: end; end; @Xpe*过羹 Triple,Double; nteger;st众a gnteger;st血a
一般情况下(而且在配书代码中),主项目文件只包括uses和exports语句,而函数声明放在一个单独的单元中。在FirstDll示例中,笔者打算稍微修改上面的代码,以便每当一个函数得到调用时显示一条消息。读者可以用许多种方式来显示消息;本例或者使用Dia-lOgs单元并调用 ShowMessage函数,或者使用 Windows单元并调用 MessageBox函数。 如果读者喜欢Dialogs单元,DelPhi必须将大量VCI.代码链接到该应用程序中。如果读者将VCI一静态地链接到这个DI-I.中,最后的大小将几乎是400KB。原因是ShowMes-sage函数显示一个含有vcl一控件的窗体,并使用vcl。图形类;那些类间接地引用诸如VCI.流系统、VCI一应用程序和屏幕对象之类的东西。在这种情况下,一个更好的替代方案是使用直接的API调用来显示消息,即使用Windows单元并调用MessageBox函数,以避 免对VCI.代码的需要。这一代码修改可以将应用程序的大小降到40KB左右。 如果使用该DI*.的API版本来运行一个像CallFrst示例(稍后描述它)那样的测试程序,它的行为将是不正确的。事实上,无需首先关闭DI。I。所显示的消息框,读者仍可以多次单击那些调用 DIJ函数的按钮。出现这种情况的原因是 MessageBox API调用的第一个参数是零。这个参数的值应该是程序的主窗体,或者说应用程序窗体的句柄——一读者在这个DIJ中还没有的一个信息。第章 动态体系结构(库、包与组件J 一般情况下.DIJ巾的函数可U(t用什何一种举地的参数.#扳回仟何一种举刑的值。 D 什签柳则有两种例外情旧, l·如果计划从其他编程语言中调用该 Dll,就应该尝试使用 Windows本机数据类型而 l 不是 IJelphi的专有类型。例如,要想表示颜色值,应该使用整数或 Windows Color- IRef类型而不是 DelPhi本机 TColor类型,否则需要做相应的转换。由于兼容性缘故, l 应该避免使用其他一些 DelPhi类型,其中包括(其他语言无法使用的)对象和(可 l 以用 PChar字符串代替的)Delphi字符串。换句话说,每个 Windows开发环境都必 0 须支持API的基本类型,而且如果程序员坚持这一点,那么它们所开发的DI1在其 0 他开发环境中将是可使用的(这部分地解释了.NET所解决的语言互用件麻烦)n J·即使只打算从一个DelPhi应用程序中使用该DIJ-,但如果不采取一些防范措施,程 J 序员仍无法在整个**。范围内传递 DelPhi字符串(和动态数组)。这起因于 DelPhi D 蚁m巾k山中班由M十十_D不从而〕田从而〕知仅价中人1:十人o脉的恤止打他且 JH埋H什十于付甲w力八——同创了比、舟了比W样议匕们。坯”I”I叨刚加讲比dJ‘估足 l 将 ShareMem系统单元同时包含在该 DI工 和使用该 DI上的程序中。这个单元必须被 D 包含为每个项目的第一个单元。此外,还必须随同程序和该库一起部署 1切rhdMM.DLL文件(这个名称代表[rland Memory Manager)。 l 在 FirstDll。示例中,笔者同时采用这两种方法:一个函数接收并返回一个 De巾hi字符 l 串.而另一个函数接收一个 PChar指针,然后将给这个指针赋值。第一个函数是用 Delphi 编写的,并且笔者不打算在此列出它——它是微不足道的,因为它用一个分隔符并置两个字 符串。 第二个函数稍微复杂一点,因为 PChar字符串没有一个简单的“十”操作符,而且具 有较弱的自动内存管理,因此读者必须使用一些特定的支持函数。下面是完整的代码;它使 用了输人与输出PChar缓冲区,这两个缓冲区兼容于任何一个Windows开发环境: 刊nc辽fon Do讪ieKbar(Du子fer二n,Bufferout:P〔bar; sufferoutLen:Cardfnal;Separator:Char):LongBool;staall; V邑r Sepstr:a*my[0..l]of〔bar; begin ti,歹 // i尸 the bu尸fer fs Iarpe enough ff BufferoutLen>StrLen(Bufferln)”Z+2 t回@n be巴in // copy the input buff6ff6r fn the output buf沪@r StrCopy(Bufferout,sufferln); 人 buf7d the soprator string tva7ue p7us "u71 termfnatorJ Sepstr[01:。Separator; 钻pstr 阻1:。佯o; 人 append the soprator StrCat(Bufferout,Sepstr); 人append the】"put bu ffeffer once用ore StrCat(sufferout,Bu乎乎erln); Result:。True; end d羹e V not enouqh spsce 372 klphi 2005 Axrl到精通 — — ReSUlt:-FUISS; @X*@pt Result:=False; en回; end; 这段代码的第二个版本当然更加复杂,但第一个版本只能在klphi中使用。此外,正 如前面所提过的,第一个版本要求读者包含ShareMem单元并部署Borl。ndMM.DI.I。 调用 Dephi DLL 那么,怎样使用刚才建立的库呢?读者可以从另一个Delphi项目内或者从其他环境中 调用它。例如,笔者创建了CallFrst项目(被存储在FirstDI-I.目录中)。要想访问DI人函 数,必须将它们声明为外部的。读者可以从Delphi DI*一的源代码中复制并粘贴函数的定义, 然后按如下所示添加 external子句: functl—Double(N:Integer):Integer; sideal];externa1FIRSTDLL.DLL’; 一旦它们被声明为external,该DI人的函数就可以作为局部函数来使用。下面是两个 例子,其中调用了字符串相关的函数(图*.l显示了一个输出示例人 P POCPOCedure TFo.n1·BtnDoublestrfngclick(Sender:了object): ho回in // caN the DLL尸onotion dfrectly Ed】tDOUble·TeXt:-Doublestrfng(Editsource.Text,。;。); end; P POCPOC巴必u回eTForml.BtnDoubl@PChar〔15oh(Sende w邑r BUff6p:StFihg; 巴@回In 人 ske the bufhr IarqB enough SetLength(Buffer,1000); 人 ca7I the DLL 刊notion T0bjec ff DoublePChar(PChar(Edftsource.Text),PChar(su乎乎er EditDOUb16.T6Xt:=BUff6F; end; .000./’)thee 图 11.ICallFrst示例的输出结果,其中调用了刚才创建的 Delphi DLL 修改库名称 同标准应用程序的情形一样,对于一个库,程序员最终得到的是一个匹配于DelPhi项第N章 动态体系结构(库、包与组件 【73 目文件名的库名称。DelPhi有一些特殊的编译器指令,程序员可以在库中使用这些指令来指 定它们的可执行文件名称。其中的两条指令($I-IBPREFIX,在库名称前面添加一个单词; 以及$IJBVERSI(川,在扩展名后面添加一个版本号)在 I.inux上比在 Windows中更有意 义。第三条指令$IJBSUFFIX在库名称后面与扩展名前面添加文本,并且在 Windows上还 经常用来修改可执行文件名(在对库和包进行版本管理的时候)。 在* 中,这些指令可以从 Project()Ptions对话框的 APPlication页面中进行设置,如 图 11.2中所示。例如,请考虑下面这些指令,它们将生成一个叫做 MarcoNameTest60.dll 的库文件: library N WbheTest; (SLIBPREFIXharco’} (多LIBSUFFIX 60) —— —— 厂 图11.2 Project()Ptions对话框的APPlication页面现在含有一个I.ibraryName部分 Delphi包专有地使用$I.IBSUFFIX指令。Delphi 2005中的 VCI.包生成VCI..DCP文 件和VCI-90.BPI一文件。这一技巧的优点是程序员将不需要为DelPhi的每个新版本都修改 他们包的requres部分。同时,程序员可以把编译包的多个版本部署在同一台计算机上的各 个可执行文件中。每个程序均装载为它而编译的那个版本。 注意:.BPI.扩展名代表 Borland Package.ibrary。这实质上是一个具有一些额外定制 特性的DIJ.。 运行时调用DLL函数 前面曾经提过,程序员可以将一个DIJ.的装载推迟到它被需要的那一刻。因此,即使该DIJ.不可获得时,程序员仍可以使用程序的其余部分。在Window。中动态装载一个DI上可通过调用 I-oadl-ibrary API函数来实现;该函数将在程序文件夹、路径上的文件夹以及一些系统文件夹中查找该DI.I-。如果该DI-I一没有被找到,Windows将显示一个错误消息—一程序员可以通过调用 Delphi的 Safel-。adl。ibrary函数来跳过的它。该函数和它所封装的 API函数具有同样的效果,但它可以取消标准 Windows错误消息的显示,并且应该是DelPhi中动态地装载DI*一的首选方式。 如果库被找到并得到装载(可以通过检查l。adl-ibrary或Safel。adl。hrary函数的返回:374Delphi 2005 AnAfl到精通值来判断),那么程序可以调用GetProcAddress API函数,然后该函数查找DIJ的导出表,进而查找作为参数传人的函数名称。如果GetProAddress发现了一个匹配,它将返回一个指向被请求过程的指针。现在,程序员可以将这个函数指针转换到适当的数据类型并调用它。 不管使用哪个装载函数,都不应该忘了最后调用Freel。山rary函数,以便DI.1.能够从内存中得到适当的释放。事实上,系统为库使用一个引用计数技巧,以便在每个装载请求与一个释放请求匹配起来时,释放那些库。 笔者开发用来演示怎样动态装载DIJ的示例叫做DynaCall。它使用笔者在本章前面创建的FirstDI*一库(要想让本程序能够工作,必须将这个DI*.从其源文件夹中复制到I)yna-(”all示例所驻留的同一个文件夹中)。本例不是声明Do uHe和Tr凶e函数并直接使用它们,而是使用一段稍微复杂一些的代码来获得同样的结果。然而,优点在于即使没有该DIJ,示例程序仍将正常运行。另外,如果给该DIJ添加新的“兼容”函数,必须修订程序的源代码,并重新编译它来访问这些新的函数。下面是示例程序的核心代码: tit 丁二n宜FunCtion一炉unc!f poe(:二ntege1:In艺@g巨r;另过回c邑]]; Cbost 01 In5m5=’n厂stdN.山7‘; p——@回u—TForml.Button1Click(Sender:了object); V邑r HInst:THandle二 FPo节nter:TFarP罗>co; 汕yFunct:T二ntFunctfon; b.回In Hinst:。s邑feL0adLfbr邑ry(DllHame); 】哆H二nst>o t回@n tr以 *POi OtOO:-6O在POOC**dCCSS m二SSt, Pchar(Editl.Text))Z If FPOiflt6F<>R毛lthfh boln MyFUUCt:=TICtFUUCtiOO(FPOIOtOO); SpinEditl.Value:-MyFunct(SpinE*iii.Value); end 6156 Showessage(Editl.Text+’DLL function not刊und’); flual]罗 FreeLibrary(HInst)二 @n回 d吕e Sho以essagE(DllNaM+’7fbmVnothund); .问必; 答告:由于这个库使用了hdand Memory Manager(内存管理器),所以动态装载它的 程序也必须使用这个内存管理器。因此,需要将 ShareMem单元包含在 Dyna- Call示例的项目中。 解像 个 就 一旦获得了一个指向某个过程的指针,应该怎样在Delph中调用这个过程呢?决方案是将该指针转换成一个过程类型,然后使用这个过程类型变量来调用这个过程第11章 动态体系结构(库、包与组件 375 上面的代码清单中那样。需要注意的是,读者所定义的过程类型必须兼容于该过程在DI.I. 中的定义。这也是该技巧惟一致命的弱点——一没有参数类型的任何实际检查。 那么,这一技巧的优点是什么呢?从理论上讲,读者可以在任何时候使用它访问任何 [)I*。的任何函数。实际上,就像本例中那样,当读者获得一些含有兼容函数的不同Ijll。, 或者获得包含一些兼容函数的单独*I*时,该技巧是有用的。读者可以通过在编辑框中输 人Double与Trghe方法的名称来调用它们。现在,如果有人给读者一个含有一个新函数的 DI.I,而且该函数接受一个整数作为参数并返回一个整数,那么读者可以通过在编辑框中输 入该函数的名称调用它。甚至不需要重新编译应用程序。 就这段代码而言,编译器与连接器将忽视该DI1的存在性。当示例程序被装载时,该 DIJ.不会马上得到装载。读者可以使本程序变得更灵活,并使用一个m 或另一个配置文 件来保存要使用的各个*上的名称。在某些情况下,这是一大优点。一个程序可以在运行 时切换DI.I。———直接装载技巧所不允许的事情。 只有基于编译器与连接器的系统,比如 Delphi才可以使用直接技巧。一般说来,这一 技巧更可靠,运行速度也稍微快一些。在笔者看来,DynaCall示例的间接装载技巧只在特 殊情况下才是有用的,但它是极其强大的。另一方面,将动态装载用于包含窗体的包是非常 有价值的,读者将会在本章的结尾部分看到这一点。 注意 不用说, NET中的情况有很大的不同。在这个平台上,程序员可以利用 I‘In- yoke或被前缀了一个I.oadl.ibrary调用的Plnvoke来使用无管理化库(就像下一 章中介绍的那样),但也可以创建管理化DLL组件。NET平台支持组件的动态 装载,就像本章的最后一部分所讨论的那样。 内存中的 Win32 DLL:代码与数据 在开始讨论包之前,需要先弄清楚动态库的一个技术要素:动态库怎样使用内存。让我 们首先从库的代码部分开始,然后关注库的全局数据。当Windows装载一个库的代码时, 同对待其他任何一个代码模块一样,它必须做一个“修正”操作。这个修正操作包括修补跳 转的地址和内部函数调用(通过使用它们被装载到的实际地址)。这个操作的结果是代码加 载的内存取决于代码已经被装载到什么地方。 这对于可执行文件来说不成问题,但是对于库来说可能会引起很大的问题。如果两个可 执行文件将同一个库装载在同一个基地址处,那么计算机的**M(物理内存)中将只有 DI*。代码的一个物理副本,因而节省了内存空间。当一个库被装载并且它的内存地址已在 使用中时,它需要被重新定位,也就是说,应用一个不同的修正来移动这个库。因此,最终 的结果将是DIJ代码在RAM中的第二个物理副本。 程序员可以使用基于 GetProcAddress API函数的动态装载技巧来检查一个函数已经被 映射到当前过程的哪个内存地址。这段代码如下所示: Labd.〔apd。n:-F。**日t(‘A叨*@s51和’,【 GetProcAddress(HDLLInst,’setData)]); 这段代码在一个标签框中显示该函数在调用应用程序的地址空间内的内存地址。如果读 者使用这段代码运行两个程序,它们一般将显示同一个地址。这一技巧演示了代码在同一个376 elphi 2005 n入门到精通 内存地址上只被装载一次。 用来获得更多此类信息的另一个技巧是使用 Dewhi的 Modules窗口。该窗口可以显示 模块所引用的每个库的基地址,以及库中每个函数的地址,如图11.3所示。 —— .N一一、epeW po 一 防口回间自u隧 .CM’#.,. .——O 回 囫口则巴M回 .U5tR32W .detuw @岛D脚冲n区回 .qopY4M .mory. 回 日旧Yr 0回 ’——O .boldroW .WA5fbeM 回〔①杠nN回 .M4 .Ulth-eeW IllggllslsllfI99 地加加恻w脚洲n风洲肌仰网间则则 OwtM\lflFtdtfCW,d,。0 MO 〔Wm出。以利N凶问u石 C\WDtxiwslty--BgnMWP4 CAWIWMThWMIAfW CAWIMrtwe\f、qpeTh-3.-xJI4 C\WWWSyvwe:WPCht4W 乙W地呕山明囚口旧们盼托V肉咽厘U回 厂poe ii民阶g寡b口恻阳M聊试t门白 CI\WNf>3Wlngv.u-rrxde)ZW CW①OP-’t-’Nd1Mfl3WboqyytrtA (似助O。以贴yv-u叨W所住刀裙 〔们mbh旧冲d恰o印瞩响d口D瞩 ol--mllpepete5TtalW (:powe. \edKYyWHfv--tjtl!,xtf’ 4 二回回 O:M:WWKWWe 一——o D:VodNFWde\lllFy’or1F’rttwedeDM — ——— ffv’ffW4M Sy<ot4at ·SWthe .引刀匕问 4y4.k tpe Ny’,o*ql —— wM--_——_ Etfftx.yl MtWWeft u).Aop 4 EufryKattoMedf U:vi。,W F问回以龙江虐d同门.间。p贮℃二.、<筹< O自触各队本*大问迁阿tli 3二f阐K EPotH-’.w gyU:ht( 二吓@删邀出困口时医呵汇朗对 以这jwk P-,’M13-IErt.orxw el326FFM 4s.-Tt"mHWW- tri?.MMIF AtKnedfr’or*to~,~ fjln:4“id ~--AL6tAgrl eMnd4- GWtwwt oil:W:c C。teddepe W jtW4 GdLKc’xuW~pe tXT327lpe E。<Nx’,’u- t&l2>2# WtrtNxxm, fpo?7”e hew。<WyXysgnP7M rdfw-.otroto IG)32,W wedypetfot l(atZZ7W eQ”ye IND27’iii gr.w.7’po oD774be RrCh-1-mm ttwiC <h-1,w’ IIi3274f7t ———————— k’Doxwe IOIVB36 bel..-w’, W7:7STh theSgWDewe fIXIH7HK satFer,-b~wm pHle,W 吕f tt回闲w】臧 以翼 m)UNm iWL.W’x--,,uJZ“ f’se~.,. tJ-eX7Ape Fwi42MMh FaiQW)T syst.ra-- top32tw ”,t el;Map t)-tOO32pptC bewe twwl tfor,-tofg po32M48 〔一Hw 但团【网IM 口 F4---W kyDZtlf4 0——Mw——14’ Fyu’,4Xt NPI$2lCM Fed4 wtw d 图 11.3 正在 IDE的主窗格中打开着的 Delphi Modules窗口 重要的是需要知道,在DelPhi中,DI.I一的基地址是程序员可以通过设置来请求的东西: 技巧是在Ptoject wtions对话框的连接器页面中设置 Image Base值。例如,在 DIMem库 中,笔者将 Image Base设为$00800000。ie者需要为自己的每个库形定一个不同的值,以 便验证它不会与可执行文件所使用的任何系统库或其他库(比如包)发生冲突。同样,这是 程序员可以通过使用调试器的Module窗口来发现的东西。 虽然这并不保证一个椎一性的布局,但DI-L被重新定位的可能性比不设置一个基地址 低得多;在这种情况下,基地址欧认为$400000,这是可执行文件被驻续到的地方,因而保 证一个重定位操作。 提示:读者也可以使用 Process*Xplorer从、syshternals.corn上检查任何一台计 算机上的任何一个进程。这个工具甚至有一个选项用于突出显示被重新定位的 DI-L。检查带有库的同一个程序在不同的操作系统(Windo。2000、Windows XP和 Windows Me)上运行的结果,并确定一个未使用区域。 这是DI。l。代码的情况,但是全局数据的情况又怎样呢?基本上,DLL的每个副本在调 用应用程序的地址空间中都有各自的数据副本。然而,在使用了同一个饥工的应用程序之 间共享全局数据是可能的。最常用的数据共享技巧是使用内存映像文件。笔者将把这一技巧 用于一个Do,但该技巧也可以用来在应用程序之间直接共享数语。第11章 动态体系结构(库、包与组件 377 本例用于库时og做DllMem,用于演示程序时叫做UseMem。DIJ。代码有一个导出4个 子例程的项*义件: library dllmem; *互@羹 SySUti S, DllMemU mD77Hebo.paa’; exports SetData,GetDatal GetshareData,SetshareDa宜a; end. 实际的代码在一个二级单元(DllMemU.PAS)中。这个单元包含4个例程的代码,它 们用来读取或写人两个全局存储单元。这两个存储单元存有一个整数和一个指向一个整数的 指针。下面是变量声明和两个SeI例程: var PlafnData:Integer。0;人not5bar@d ShareData:“Integer;//5hared p*t edure SetData(I:Integer);st回call; begin PlainData:二 I; end; procedure SetshareData(I: Integer);stdcall; be91n ShareData“:=I: en大 使用内存映像文件共享数据 对于没有被共享的数据,没有什么别的事要做。然而,要想访问共享数据,DI上必须 创建一个内存映像文件,然后获得一个指向这个内存区的指针。这两步操作需要两个Win- 。1*ws AN调用: ’t”reateFi!eMaPPing函数要求如下参数:文件名(或者说$FFFFFFFF,以便使用内 存中的一个虚拟文件)、一些安全与保护表征、数据的大小以及一个内部名称(必须 同名才能从多个调用应用程序中共享这个映像文件)。 ·MapView()fFIle函数要求如下参数:内存映像文件的句柄、一些表征与偏移量以及 数据的大小。 下面是initialization部分的源代码,这一部分在Dll每次被装载到一个新的进程空间 时被执行一次(也就是说,对使用该DIJ的每个应用程序都执行一次): V邑r hMapFile:THandle; const Vi rtua1H@Ha刃e。sharoNDS玄a’Z Datasf7e。siZeof(Integ@r); Initializatlon 378 elphi 2005 n入门到精通 V create nlenmry mpped fn吕 h间邑pF们e:=CreateFi]~appfng(】FFFFFFFF,n卡】, PagmReadwrste,0,DatasiZe,Virtua1FfleName); If hMapFile·0 thou ra】sc Except’on.Create(’Error creatfnq Ine"mry-mppedf肯7e‘); //get the po哼nter to the actua了data ShareData:·MapviewofFile( hMapH ie,Fi IMapMri ie,0,0,DataS5ze); 当应用程序结束运行并且 DI-I一得到释放时,它必须释放指向映像文件的指针和文件映 像: h问&1卡Z回dOil UnmapVle闲ofFfle(ShareData); CloseHandle(hMapFfie)于 UseMem演示程序的窗体有4个编辑框(其中的两个被关联到一个UpDown控件)、5 个按钮和一个标签。第一个按钮将第一个编辑框的值保存在DIJ一数据中,以便从关联的 UpDown控件中取得该值: SetData(UpDownl.Posftfon); 如果单击第M个按钮,演示程序将把Dll一数据复制到第M个编辑框中: EdftZ.丁aXt:一工:t丁ost:(detD:t:; 第三个按钮用来显示一个函数的内存地址,同时这行代码显示在这一部分的开始处。最 后两个按钮的代码基本上与前两个按钮的相同,但它们调用SetshareData过程与Getshare- Dal函数。 如果运行演示程序的两个副本,将会看到对于DLI一的普通全局数据,每个副本都拥有 各自的值,但共享数据的值是共有的。在两个副本程序中设置不同的值,然后读取它们,就 会明白笔者的意思是什么。图*.4举例说明了这种情况。 一 一 图 11.4 如果运行 UseMem程序的两个副本,通过比较来自这两个实例的数据(左 边与右边)将会看出 DLL中的全局数据(Get按钮附近的编辑框)未被共 享,而来自内存映像文件的数据是相同的(Getshare按钮附近的编辑框) 否告:内存映像文件保留了一个64KB的最小虚拟地址范围,并以 4KB页面为单位消 耗物理内存。本例使用的4字节整型数据在共享内存中是相当浪费的,特别是在 使用这个相同的技巧来共享多个值的时候。如果需要共享多个变量,则应该将它第N章 动态体系结构(库、包与组件 37 们全部放在一个单独的共享内存区中(然后通过使用指针或为所有变量建立一个 记录结构来访问不同的变量)。 DelPhi包 在DelPhl for Win32中,构件包是DI*。的一个重要类型。包允许开发人员包装一组构 件,然后静态地链接构件(将它们的编译代码添加到应用程序的可执行文件中)或者动态地 链接构件(将构件代码保存在一个DI.I.——程序员将随同应用程序以及所需的其他所有包 一起分发的运行时包)。 使用运行时包 针对包的链接有两种形式,它们有一些优点和缺点。有许多要素是需要牢记的: ·使用运行时包使可执行文件小得多。 ·将包单元链接到程序中允许程序员只分发包代码的一部分。一个应用程序的可执行文 件的大小加上所需包*工的大小始终比静态链接程序的大小大很多。链接器只包含 程序所使用的代码,而一个包必须将它所包含的所有单元的interface部分中所声明 的所有函数和类都链人。 ·如果分发多个基于相同包的Delphi应用程序,最终的结果可能是分发较少的代码, 因为那些运行时包将被共享。换句话说,一旦应用程序的用户有了标准的De巾hi运 行时包,开发人员就可以给他们分发非常小的程序。 ·如果运行多个基于相同包的Delphi应用程序,则可以在运行时节省一些内存空间; 那些运行时包的代码在多个DelPhi应用程序当中只被装载到内存中一次。 ·不用太担心分发一个大型的可执行文件。请记住,在对一个程序做小改动时,可以使 用各种工具当中的任意一个创建一个补丁文件,这样只需分发一个包含那些差别的文 件即可,无需分发那些文件的一个完整副本。 ·如果在一个运行时包中放置一个程序的一些窗体,就可以在程序之间共享它们。然 而,当修改这些窗体时,一般还需要重新编译主程序,并将主程序和窗体重新分发给 用户。有一些可以用来回避这个问题的技巧,我们将在下一节中详细讨论这些技巧。 ·一个包就是一些编译单元(其中含有类、类型、变量、例程)的一个集合,这些单元 与程序内的单元没有任何不同。惟一的不同之处在于创建过程。这些包单元的代码和 使用它们的主程序的各个单元的代码保持一致。可论证的是,这是包胜过DI人的关 键优势之一。 创建设计时与运行时包 包呈现两种形式:由 DelPhi IDE使用的设计时包,以及由应用程序任选地使用的运行 时包。选择纯设计时包还是纯运行时包取决于包的类型。当试图安装一个包时,IDE检查它 含有纯设计时标志还是纯运行时标志,并决定是否允许用户安装该包,以及它是否应该被添加 到运行时包列表上。由于存在两种非排斥的选择,所以它们也可以被组合起来。因此,存在3 种不同的构件包(由 Proect(中nons对话框的 escriPtion页面中的 3个单选按钮指出): ·纯设计时构件包(在源代码中含有($DESIGN(JNIJ}指令)能被安装在 DelPhi环380 elPhi 2005 n入门到精通 境中。这些包通常含有一个构件的设计时部分,比如它的属性编辑器和注册码。它们 还经常含有构件本身,尽管这不是最专业的技巧。设计时包构件的代码通常被静态地 链人可执行文件内,进而使用对应.IxU文件的代码。然而,需要记住的是,从技 术11讲,将纯设计时包用做运行时包也是可能的。 ·纯运行时构件包(在源代码中含有($RUN()NIl}指令)由 DeIPhi应用程序在运 行时使用。它们不能被安装在DelPhi环境中,但是当一个已安装的设计时包需要它 们时,它们被自动添加到运行时包列表上。运行时包通常含有构件类的代码,但是不 具有设计时支持(这么做的目的是为了最小化开发人员随同其可执行文件分发的构件 库的大小)。运行时包之所以重要,是因为它们能够随同应用程序一起被自由地分发, 但是其他Delphi程序员将无法把它们安装在环境中来创建新的包。 ·设计时与运行时包(在源代码中均不含有{$DESIGN()NIY} 与{$RUN(JNIl} 包指令)能被安装和自动添加到运行时包列表上。通常,这些包含有极少需要或者根 本不需要设计时支持的构件(除了有限的构件注册码之外)。 编写与编译包 包的结构在项目管理器中是可见的(因为来自早期 DelPhi版本的 Package Edtor不再 可用),并且具有两个部分: ·Contains列表指出这个包中所包含的各个单元。 ·Re(…res列表指出这个包所需要的各个包。一个包一般将需要 rtl和 vcl包(主运行 时库包和核心 VCI.包),但它可能还需要 VCldh包(含有大部分数据库相关的类), 如果它里面的代码做任何数据库相关的操作。 当编译一个包时,程序员将同时产生一个带有编译代码的DIJ.(.BPI一文件)和一个文 件厂一个.DCP文件)(仅含有不包括已编译机器码在内的符号信息)。Delphi编译器使用后 一个文件来收集与当前包的各个单元相关的符号信息,因而无需访问那些单元(.DCU)文 件;.I*7U文件既含有符号信息,又含有已编译的机器码。这个过程减少编译时间,并且允 许程序员仅分发不含有预编译单元文件的包。为了将包静态链接到一个应用程序中,预编译 单元仍是需要的。 包的版本管理 一个非常重要而又常常遭到误解的要素是更新包的分发。当更新一个DI-I.时,程序员 可以分发这个新的版本,而且需要这个DI-1.的可执行程序仍将正常工作(除非删除了现有 的导出函数或修改了它们的一些参数)。 然而,当分发一个Delphi包时,如果更新这个包并修改这个包的任何一个单元的inter- fa;。部分,那么可能需要重新编译所有使用这个包的应用程序。如果需要给一个类添加方法 或属性,这一步是必需的,但如果只是添加新的全局符号(或修改客户应用程序没有使用的 任何东西),这一步不是必需的。如果修改只影响了包单元的implementation部分,那么不 存在任何问题。 DeIPhi中的 DCU文件含有一个基于其时间戳和一个校验和(从单元的接口部分中计算 得出)的版本标记。当修改一个单元的 interface部分时,基于它的其他每个单元都应该被 重新编译。IJelphi编译器将该单元来自以前编译的时间棚和检验和跟新的时间棚和校验和进第 章 动态体系结构(库、包与组件 :38 0 行比较,并决定相关单元是否必须被重新编译。由于这一缘故,当程序员获得DeIPhi的一 D 个新版本并且它带有已修改过的系统单元时,必须重新编译每个单元。 D 在包被首次引人时,编译器给相应的包库添加了一个额外的人口函数(用包的一个校验 J 和来命名,其中这个校验和是从包所包含的各个单元的校验和与它所需要的各个包的校验和 D 中计算得出的)。然后,这个校验和函数由使用这个包的程序调用,以便任何较旧的可执行 D 程J子在启动时运行失效。 JDe1Phi的近期版本已经放宽了包的运行时限制(尽管如此,对eU文件的设计时限制 D 依然未变。)包的校验和不再被检测,所以程序员可以直接修改一个包的各个单元,并随同 D 已有可执行文件一起部署包的一个新版本。由于方法是按名引用的,所以程序员不能删除由 J 主程序调用的任何一个已有方法。程序员甚至不能修改方法参数,由于名称撕裂技巧的缘故 D(这些技巧用来保护包的方法以防止参数方面的修改)。 D 删除一个从调用程序中引用的方法将会在装载过程中停止程序。然而,如果程序员做其 J 他修改,程序可能会它的执行期间意外地出现故障。例如,如果使用一个相似的构件来代替 D 从一个包中编译过的窗体上所放置的一个构件,调用程序也许仍然能够在给定的内存偏移位 0 置上访问到这个构件,尽管它现在已不是原来的构件! D 如果决定使用这种不可靠的技巧来修改一个包内的单元的接口,而又不重新编译使用这 D 个包的所有程序,程序员至少应该有节制地做修改。当给窗体添加新的属性或非虚方法时, 0 人立该能够维持与已经使用这个包的已有程序的完全兼容性。此外,添加字段和虚方法可能会 l 影响类的内部结构,进而导致正期待一个不同的类数据和虚方法表(VMT)布局的已有程 序出现问题。 警告:在这里,笔者指的是分发已经被分解成**E文件和包的已编译程序,而不是指 分发构件给其他 Delphi程序员。在后一种情况中,版本管理规则更加严格,而 且读者必须格外当心包的版本管理。 至于这一方面,笔者建议绝对不要修改已被包导出的任何一个单元的接口。为此,可以 给包添加一个包含窗体创建函数的单元,并使用它来访问定义该窗体的另一个单元。虽然没 有办法隐藏一个已被链人到包中的单元,但如果从未直接使用一个单元中所定义的类,而只 是通过其他例程来使用它,那么程序员在修改它方面将会拥有更大的灵活性。程序员还可以 使用窗体继承来修改包内的窗体,而又不影响原始版本。 构件编写者应该遵循的最严格的规则是:为了包内代码的长期部署与维护,应该计划持 有一个主版本连同~个次要的维护版本。这个包的一个主版本将要求所有客户程序从源代码 中被重新编译;包文件应该被重命名成带有一个新的版本号,而且单元的接口部分能够被修 改。这个包的维护版本应该仅限于实现上的修改,以保护与已有的可执行文件和单元的完全 兼容性,Borland对它的更新包一般也采取同样的做法。 使用包的动态体系结构 初看起来,读者可能以为DelPhi包只不过是一种构件分发方法,用来分发将要被安装 在环境中的构件。然而,程序员也可以将 Delphi包用做一种代码结构设计方法,进而以一 种动态方式组织代码;与程序员使用普通DLL时不同,这种动态方式仍然保留的全部能力。请考虑这样一种情形:一个包是一些已编译单元的集合,并且一个程序使用几个单元。这个程序所引用的单元将被编译到可执行文件内,除非程序员要求DelPhi动态地链人到这些单元的包内。正如前面所讨论的,程序员可以在静态与动态链接之间切换的轻松性是使用包的主要原因之一。 使用运行时包的应用程序 要想开发一个应用程序以便它的代码被分解成一个或多个包和一个主可执行文件,程序员只需要将一些单元编译在一个包中,然后设置主程序的选项来动态地链接这个包。例如,笔者创建了一个颜色选择窗体(图 11.5显示了设计时的这个窗体),并将它放置在 Pack Form文件夹的 PackwithForm单元中。读者可以在图 11.5中看到带有这个包的结构的 Prqect Manager窗口。 M。。1____._.____.__ —— WSWef’·iwpeNRM
|