用户登录  |  用户注册
首 页商业源码原创产品编程论坛
当前位置:PB创新网文章中心编程技巧Delphi

Windows的动态链接库原理

减小字体 增大字体 作者:佚名  来源:本站整理  发布时间:2009-03-16 20:14:40
动态链接库(DLLs)是从C语言函数库和Pascal库单元的概念发展而来的。所有的C语言标准库函数都存放在某一函数库中,同时用户也可以用LIB程序创建自己的函数库。在链接应用程序的过程中,链接器从库文件中拷贝程序调用的函数代码,并把这些函数代码添加到可执行文件中。这种方法同只把函数储存在已编译的.OBJ文件中相比更有利于代码的重用。

  但随着Windows这样的多任务环境的出现,函数库的方法显得过于累赘。如果为了完成屏幕输出、消息处理、内存管理、对话框等操作,每个程序都不得不拥有自己的函数,那么Windows程序将变得非常庞大。Windows的发展要求允许同时运行的几个程序共享一组函数的单一拷贝。动态链接库就是在这种情况下出现的。动态链接库不用重复编译或链接,一旦装入内存,Dlls函数可以被系统中的任何正在运行的应用程序软件所使用,而不必再将DLLs函数的另一拷贝装入内存。 

1.1. 动态链接库的工作原理 
  “动态链接”这几字指明了DLLs是如何工作的。对于常规的函数库,链接器从中拷贝它需要的所有库函数,并把确切的函数地址传送给调用这些函数的程序。而对于DLLs,函数储存在一个独立的动态链接库文件中。在创建Windows程序时,链接过程并不把DLLs文件链接到程序上。直到程序运行并调用一个DLLs中的函数时,该程序才要求这个函数的地址。此时Windows才在DLLs中寻找被调用函数,并把它的地址传送给调用程序。采用这种方法,DLLs达到了复用代码的极限。
  动态链接库的另一个方便之处是对动态链接库中函数的修改可以自动传播到所有调用它的程序中,而不必对程序作任何改动或处理。
  DLLs不仅提供了函数重用的机制,而且提供了数据共享的机制。任何应用程序都可以共享由装入内存的DLLs管理的内存资源块。只包含共享数据的DLLs称为资源文件。如Windows的字体文件等。 

1.2. Windows系统的动态链接库 
  Windows本身就是由大量的动态链接库支持的。这包括Windows API函数 ( KRNLx86.EXE,USER.EXE,GDI.EXE,…),各种驱动程序文件,各种带有.Fon和.Fot 扩展名的字体资源文件等。Windows还提供了针对某一功能的专用DLLs,如进行DDE编程的ddeml.dll,进行程序安装的ver.dll等。
  虽然在编写Windows程序时必然要涉及到DLLs,但利用Delphi ,用户在大部分时候并不会注意到这一点。这一方面是因为Delphi提供了丰富的函数使用户不必直接去使用Windows API;另一方面即使使用Windows API,由于Delphi把API函数和其它Windows DLLs函数重新组织到了几个库单元中,因而也不必使用特殊的调用格式。所以本章的重点放在编写和调用用户自定义的DLLs上。
  使用传统的Windows编程方法来创建和使用一个DLLs是一件很令人头痛的事,正如传统的Windows编程方法本身就令人生畏一样。用户需要对定义文件、工程文件进行一系列的修改以适应创建和使用DLLs的需要。Delphi的出现,在这一方面,正如在其它许多方面所做的那样,减轻了开发者的负担。更令人兴奋的是Delphi利用DLLs 实现了窗体的重用机制。用户可以将自己设计好的窗体储存在一个DLLs中,在需要的时候可随时调用它。 

2.1. DLLs的编写 
  在Delphi环境中,编写一个DLLs同编写一个一般的应用程序并没有太大的区别。事实上作为DLLs 主体的DLL函数的编写,除了在内存、资源的管理上有所不同外,并不需要其它特别的手段。真正的区别在工程文件上。
  在绝大多数情况下,用户几乎意识不到工程文件的存在,因为它一般不显示在屏幕上。如果想查看工程文件,则可以打开View菜单选择Project Source项,此时工程文件的代码就会出现在屏幕的Code Editor(代码编辑器)中。
  一般工程文件的格式为: 
  program   工程标题;
  uses     子句;
  程序体 
  而DLLs工程文件的格式为: 
  library 工程标题;
  uses 子句;
  exprots 子句;
  程序体 
  它们主要的区别有两点:
  1.一般工程文件的头标用program关键字,而DLLs工程文件头标用library 关键字。不同的关键字通知编译器生成不同的可执行文件。用program关键字生成的是.exe文件,而用library关键字生成的是.dll文件;
  2.假如DLLs要输出供其它应用程序使用的函数或过程,则必须将这些函数或过程列在exports子句中。而这些函数或过程本身必须用export编译指令进行编译。
  根据DLLs完成的功能,我们把DLLs分为如下的三类:
1.完成一般功能的DLLs;
2.用于数据交换的DLLs;
3.用于窗体重用的DLLs。
  这一节我们只讨论完成一般功能的DLLs,其它内容将在后边的两节中讨论。 

2.1.1. 编写一般DLLs的步骤 
  编写一般DLLs的步骤如下:
  1.利用Delphi的应用程序模板,建立一个DLLs程序框架。
  对于Delphi 1.0的用户,由于没有DLLs模板,因此:
  (1).建立一个一般的应用程序,并打开工程文件;
  (2).移去窗体和相应的代码单元;
  (3).在工程文件中,把program改成library,移去Uses子句中的Forms,并添加适当的库单元(一般SysUtils、Classes是需要的),删去begin...end之间的所有代码。
  2.以适当的文件名保持文件,此时library后跟的库名自动修改;
  3.输入过程、函数代码。如果过程、函数准备供其它应用程序调用,则在过程、函数头后加上export 编译指示;
  4.建立exports子句,包含供其它应用程序调用的函数和过程名。可以利用标准指示 name 、Index、resident以方便和加速过程/函数的调用;
  5.输入库初始化代码。这一步是可选的;
  6.编译程序,生成动态链接库文件。 

2.1.2. 动态链接库中的标准指示 
  在动态链接库的输出部分,用到了三个标准指示:name、Index、resident。
  1.name
  name后面接一个字符串常量,作为该过程或函数的输出名。如: 
exports
InStr name MyInstr;
  其它应用程序将用新名字(MyInstr)调用该过程或函数。如果仍利用原来的名字(InStr),则在程序执行到引用点时会引发一个系统错误。
  2.Index
  Index指示为过程或函数分配一个顺序号。如果不使用Index指示,则由编译器按顺序进行分配。
  Index后所接数字的范围为1...32767。使用Index可以加速调用过程。
  3.resident
  使用resident,则当DLLs装入时特定的输出信息始终保持在内存中。这样当其它应用程序调用该过程时,可以比利用名字扫描DLL入口降低时间开销。
  对于那些其它应用程序常常要调用的过程或函数,使用resident指示是合适的。例如: 
exports
InStr name MyInStr resident; 

2.1.3. DLLs中的变量和段 
一个DLLs拥有自己的数据段(DS),因而它声明的任何变量都为自己所私有。调用它的模块不能直接使用它定义的变量。要使用必须通过过程或函数界面才能完成。而对DLLs来说,它永远都没有机会使用调用它的模块中声明的变量。
  一个DLLs没有自己的堆栈段(SS),它使用调用它的应用程序的堆栈。因此在DLL中的过程、函数绝对不要假定DS = SS。一些语言在小模式编译下有这种假设,但使用Delphi可以避免这种情况。Delphi绝不会产生假定DS = SS的代码,Delphi的任何运行时间库过程/函数也都不作这种假定。需注意的是如果读者想嵌入汇编语言代码,绝不要使SS和DS登录同一个值。 

2.1.4. DLLs中的运行时间错和处理 
  由于DLLs无法控制应用程序的运行,导致很难进行异常处理,因此编写DLLs时要十分小心,以确保被调用时能正常执行 。当DLLs中发生一个运行时间错时,相应DLLs并不一定从内存中移去(因为此时其它应用程序可能正在用它),而调用DLLs的程序异常中止。这样造成的问题是当DLLs已被修改,重新进行调用时,内存中保留的仍然可能是以前的版本,修改后的程序并没有得到验证。对于这个问题,有以下两种解决方法:
  1.在程序的异常处理部分显式将DLL卸出内存;
  2.完全退出Windows,而后重新启动,运行相应的程序。
  同一般的应用程序相比,DLL中运行时间错的处理是很困难的,而造成的后果也更为严重。因此要求程序设计者在编写代码时要有充分、周到的考虑。 

2.1.5. 库初始化代码的编写 
  传统Windows中动态链接库的编写,需要两个标准函数:LibMain和WEP,用于启动和关闭DLL。在LibMain中,可以执行开锁DLL数据段、分配内存、初始化变量等初始化工作;而WEP在从内存中移去DLLs前被调用,一般用于进行必要的清理工作,如释放内存等。Delphi用自己特有的方式实现了这两个标准函数的功能。这就是在工程文件中的begin...end部分添加初始化代码。和传统Windows编程方法相比,它的主要特色是:
  1.初始化代码是可选的。一些必要的工作(如开锁数据段)可以由系统自动完成。所以大部分情况下用户不会涉及到;
  2.可以设置多个退出过程,退出时按顺序依次被调用;
  3.LibMain和WEP对用户透明,由系统自动调用。
  初始化代码完成的主要工作是:
  1.初始化变量、分配全局内存块、登录窗口对象等初始化工作。在(10.3.2)节“利用DLLs实现应用程序间的数据传输”中,用于数据共享的全局内存块就是在初始化代码中分配的。
  2.设置DLLs退出时的执行过程。Delphi有一个预定义变量ExitProc用于指向退出过程的地址。用户可以把自己的过程名赋给ExitProc。系统自动调用WEP函数,把ExitProc指向的地址依次赋给WEP执行,直到ExitProc为nil。
  下边的一段程序包含一个退出过程和一段初始化代码,用来说明如何正确设置退出过程。 
library Test;
{$S-}
uses WinTypes, WinProcs;
var
SaveExit: Pointer; 
procedure LibExit; far;
begin
if ExitCode = wep_System_Exit then
begin
{ 系统关闭时的相应处理 }
end
else
begin
{ DLL卸出时的相应处理 }
end;
ExitProc := SaveExit; { 恢复原来的退出过程指针 }
end; 
begin
{DLL的初始化工作 }
SaveExit := ExitProc; { 保存原来的退出过程指针 }
ExitProc := @LibExit; { 安装新的退出过程 }
end.
  在初始化代码中,首先把原来的退出过程指针保存到一个变量中,而后再把新的退出过程地址赋给ExitProc。而在自定义退出过程LibExit结束时再把ExitProc的值恢复。由于ExitProc是一个系统全局变量,所以在结束时恢复原来的退出过程是必要的。
  退出过程LibExit中使用了一个系统定义变量ExitCode,用于标志退出时的状态。 ExitCode的取值与意义如下: 
表10.1 ExitCode的取值与意义
━━━━━━━━━━━━━━━━━━━━━
取 值 意 义
—————————————————————
  WEP_System_Exit Windows关闭 
WEP_Free_DLLx DLLs被卸出
━━━━━━━━━━━━━━━━━━━━━ 
  退出过程编译时必须关闭stack_checking,因而需设置编译指示 {$S-} 。 

2.1.6. 编写一般DLLs的应用举例 
  在下面的程序中我们把一个字符串操作的函数储存到一个DLLs中,以便需要的时候调用它。应该注意的一点是:为了保证这个函数可以被其它语言编写的程序所调用,作为参数传递的字符串应该是无结束符的字符数组类型(即PChar类型),而不是Object Pascal的带结束符的Srting类型。程序清单如下:
library Example;
uses
SysUtils,
Classes;
{返回字符在字符串中的位置}
function InStr(SourceStr: PChar;Ch: Char): Integer; export;
var
Len,i: Integer;
begin
Len := strlen(SourceStr);
for i := 0 to Len-1 do
if SourceStr[i] = ch then
begin
Result := i;
Exit;
end;
Result := -1;
end;
exports
Instr Index 1 name ''MyInStr'' resident;
begin
end. 

2.2. 调用DLLs
  有两种方法可用于调用一个储存在DLLs中的过程。
  1.静态调用或显示装载
  使用一个外部声明子句,使DLLs在应用程序开始执行前即被装入。例如: 
  function Instr(SourceStr : PChar;Check : Char); Integer; far; external ''UseStr'';
  使用这种方法,程序无法在运行时间里决定DLLs的调用。假如一个特定的DLLs在运行时无法使用,则应用程序将无法执行。
  2.动态调用或隐式装载
  使用Windows API函数LoadLibray和GetProcAddress可以实现在运行时间里动态装载DLLs并调用其中的过程。
  若程序只在其中的一部分调用DLLs的过程,或者程序使用哪个DLLs, 调用其中的哪个过程需要根据程序运行的实际状态来判断,那么使用动态调用就是一个很好的选择。
  使用动态调用,即使装载一个DLLs失败了,程序仍能继续运行。 
2.3. 静态调用
  在静态调用一个DLLs中的过程或函数时,external指示增加到过程或函数的声明语句中。被调用的过程或函数必须采用远调用模式。这可以使用far过程指示或一个{$F +}编译指示。
  Delphi全部支持传统Windows动态链接库编程中的三种调用方式,它们是:
  ● 通过过程/函数名
  ● 通过过程/函数的别名
  ● 通过过程/函数的顺序号 
  通过过程或函数的别名调用,给用户编程提供了灵活性,而通过顺序号(Index)调用可以提高相应DLL的装载速度。 

2.4.1 动态调用中的API函数 
  动态调用中使用的Windows API函数主要有三个,即:Loadlibrary,GetProcAddress和Freelibrary。
   1.Loadlibrary: 把指定库模块装入内存
  语法为: 
  function Loadlibrary(LibFileName: PChar): THandle; 
LibFileName指定了要装载DLLs的文件名,如果LibFileName没有包含一个路径,则Windows按下述顺序进行查找:
  (1)当前目录;
  (2)Windows目录(包含win.com的目录)。函数GetWindowDirectory返回这一目录的路径;
  (3)Windows系统目录(包含系统文件如gdi.exe的目录)。函数GetSystemDirectory返回这一目录的路径;
  (4)包含当前任务可执行文件的目录。利用函数GetModuleFileName可以返回这一目录的路径;
  (5)列在PATH环境变量中的目录;
  (6)网络的映象目录列表。
  如果函数执行成功,则返回装载库模块的实例句柄。否则,返回一个小于HINSTANCE_ERROR的错误代码。错误代码的意义如下表: 
   表10.2 Loadlibrary返回错误代码的意义
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误代码         意        义
——————————————————————————————————————
    0 系统内存不够,可执行文件被破坏或调用非法
    2 文件没有被发现
    3 路径没有被发现
    5 企图动态链接一个任务或者有一个共享或网络保护错
    6 库需要为每个任务建立分离的数据段
    8 没有足够的内存启动应用程序
   10 Windows版本不正确
   11 可执行文件非法。或者不是Windows应用程序,或者在.EXE映
      像中有错误
   12 应用程序为一个不同的操作系统设计(如OS/2程序)
13 应用程序为MS DOS4.0设计
   14 可执行文件的类型不知道
   15 试图装载一个实模式应用程序(为早期Windows版本设计)
16 试图装载包含可写的多个数据段的可执行文件的第二个实例
   19 试图装载一个压缩的可执行文件。文件必须被解压后才能被装裁
   20 动态链接库文件非法
   21 应用程序需要32位扩展
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  假如在应用程序用Loadlibrary调用某一模块前,其它应用程序已把该模块装入内存,则Loadlibrary并不会装载该模块的另一实例,而是使该模块的“引用计数”加1。 
  2.GetProcAddress:捡取给定模块中函数的地址
  语法为: 
  function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc; 
Module包含被调用的函数库模块的句柄,这个值由Loadlibrary返回。如果把Module设置为nil,则表示要引用当前模块。
  ProcName是指向含有函数名的以nil结尾的字符串的指针,或者也可以是函数的次序值。如果ProcName参数是次序值,则如果该次序值的函数在模块中并不存在时,GetProcAddress仍返回一个非nil的值。这将引起混乱。因此大部分情况下用函数名是一种更好的选择。如果用函数名,则函数名的拼写必须与动态链接库文件EXPORTS节中的对应拼写相一致。
  如果GetProcAddress执行成功,则返回模块中函数入口处的地址,否则返回nil。
3.Freelibrary:从内存中移出库模块
  语法为: 
  procedure Freelibrary(Module : THandle); 
Module为库模块的句柄。这个值由Loadlibrary返回。
  由于库模块在内存中只装载一次,因而调用Freelibrary首先使库模块的引用计数减一。如果引用计数减为0,则卸出该模块。
  每调用一次Loadlibrary就应调用一次FreeLibray,以保证不会有多余的库模块在应用程序结束后仍留在内存中。 
2.4.2. 动态调用举例 
  对于动态调用,我们举了如下的一个简单例子。系统一共包含两个编辑框。在第一个编辑框中输入一个字符串,而后在第二个编辑框中输入字符。如果该字符包含在第一个编辑框的字符串中,则标签框显示信息:“位于第n位。”,否则显示信息:“不包含这个字符。”。如图是程序的运行界面。
输入检查功能的实现在Edit2的onKeyPress事件处理过程中,程序清单如下。 
procedure TForm1.Edit2KeyPress(Sender: TObject; var Key: Char);
var
order: Integer;
txt: PChar;
PFunc: TFarProc;
Moudle: THandle;
begin
Moudle := Loadlibrary(''c:\dlls\example.dll'');
if Moudle > 32 then
begin
Edit2.text := '''';
Pfunc := GetProcAddress(Moudle,''Instr'');
txt := StrAlloc(80);
txt := StrPCopy(txt,Edit1.text);
Order := TInstr(PFunc)(txt,Key);
if Order = -1 then
Label1.Caption := ''不包含这个字符 ''
else
Label1.Caption := ''位于第''+IntToStr(Order+1)+''位'';
end;
Freelibrary(Moudle);
end;
  在利用GetProcAddess返回的函数指针时,必须进行强制类型转换: 
Order := TInstr(PFunc)(text,Key);
  TInStr是一个定义好了的函数类型: 
type
TInStr = function(Source: PChar;Check: Char): Integer; 

3.1. DLLs中的全局内存 
  Windows规定:DLLs并不拥有它打开的任何文件或它分配的任何全局内存块。这些对象由直接或间接调用DLLs的应用程序拥有。这样,当应用程序中止时,它拥有的打开的文件自动关闭,它拥有的全局内存块自动释放。这就意味着保存在DLLs全局变量中的文件和全局内存块变量在DLLs没有被通知的情况下就变为非法。这将给其它使用该DLLs的应用程序造成困难。
  为了避免出现这种情况,文件和全局内存块句柄不应作为DLLs的全局变量,而是作为DLLs中过程或函数的参数传递给DLLs使用。调用DLLs的应用程序应该负责对它们的维护。
  但在特定情况下,DLLs也可以拥有自己的全局内存块。这些内存块必须用gmem_DDEShare属性进行分配。这样的内存块直到被DLLs显示释放或DLLs退出时都保持有效。
  由DLLs管理的全局内存块是应用程序间进行数据传输的又一途径,下面我们将专门讨论这一问题。 

3.2. 利用DLLs实现应用程序间的数据传输 
  利用DLLs实现应用程序间的数据传输的步骤为:
  1. 编写一个DLLs程序,其中拥有一个用gmem_DDEShare属性分配的全局内存块;
  2. 服务器程序调用DLLs,向全局内存块写入数据;
  3. 客户程序调用DLLs,从全局内存块读取数据。 
3.2.1. 用于实现数据传输的DLLs的编写 
  用于实现数据传输的DLLs与一般DLLs的编写基本相同,其中特别的地方是:
  1. 定义一个全局变量句柄: 
var
hMem: THandle;
  2. 定义一个过程,返回该全局变量的句柄。该过程要包含在exports子句中。如: 
function GetGlobalMem: THandle; export;
begin
Result := hMem;
end;
  3. 在初始化代码中分配全局内存块:
程序清单如下: 
begin
hMem := GlobalAlloc(gmem_MOVEABLE and gmem_DDEShare,num);
if hMem = 0 then
MessageDlg(''Could not allocate memory'',mtWarning,[mbOK],0);
end.
  num是一个预定义的常数。
Windows API函数GlobalAlloc用于从全局内存堆中分配一块内存,并返回该内存块的句柄。该函数包括两个参数,第一个参数用于设置内存块的分配标志。可以使用的分配标志如下表所示。
表10.3 全局内存块的分配标志
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
标 志 意 义
—————————————————————————————————
gmem_DDEShare 分配可由应用程序共享的内存
gmem_Discardable 分配可抛弃的内存(只与gmem_Moveable连用)
gmem_Fixed 分配固定内存
gmem_Moveable 分配可移动的内存
gmem_Nocompact 该全局堆中的内存不能被压缩或抛弃
gmem_Nodiscard 该全局堆中的内存不能被抛弃
gmem_NOT_Banked 分配不能被分段的内存
gmem_Notify 通知功能。当该内存被抛弃时调用GlobalNotify函数
gmem_Zeroinit 将所分配内存块的内容初始化为零
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 
  有两个预定义的常用组合是:
GHND = gmem_Moveable and gmem_Zeroinit
GPTK = gmem_Fixed and gmem_Zeroinit
  第二个参数用于设置欲分配的字节数。分配的字节数必须是32的倍数,因而实际分配的字节数可能比所设置的要大。
  由于用gmem_DDEShare分配的内存在分配内存的模块终止时自动抛弃,因而不必调用GlobalFree显式释放内存。



2003-10-20 18:00:00
发表评语»»»

2003-10-28 13:09:17 最近摘录了另外一篇,有兴趣可以看看
http://www.delphibbs.com/keylife/iblog_show.asp?xid=3847


2003-10-31 14:26:59 如何在 DLL 处理类

在进行 Delphi 编程时,许多人喜欢将类的处理放到应用程序本身。但如果能够利用 DLL 来处理类,将会使你的应用增色不少。
DLL 中的类与普通类没有什么差别,对于DLL 的处理,只要记住两个要点:
1、不能在一端分配内存,而在另一端释放;
2、必须有类的数据导出处理,接口中处理的应是标准数据类型,想在导出函数中直接处理类只会增加你的麻烦。
注:可以利用接口处理导出类,但那对 OLE 或许还有用,对于一般的应用,简而避之。
下面是一个例子:
DLL:
type
TMyDLLClass = class
private
FData: string;
public
constructor Create;
destructor Destroy; override;
property Data: string read FData write FData;
end;
定义几个通用的接口:
type
TMyDLLOBJ = type Pointer; //定义 DLL 中的通用对象指针
function CreateMyDLLObj: TMyDLLOBJ; stdcall; //初始化类
function DoneMyDLLObj(OBJ: TMyDLLOBJ); stdcall; //释放类
然后增加你的导出函数:
procedure FetchMyDLLData(OBJ: TMyDLLOBJ; Buffer: PChar; Size:
Longint): stdcall;
var
MyDLLClass: TMyDLLClass;
begin
MyDLLClass := TMyDLLClass(OBJ);
if MyDLLClass <> nil then
StrPLCopy(Buffer, MyDLLClass.Data, Size);
end;
看出来了吗,就这么简单,而且相当可靠。我的应用大多采用这种处理方式,在 DLL 中处理数据库,用一个数据记录来传递和处理信息。应用程序完全与数据库分开,要更换数据库类型,只要更新 DLL,应用程序根本不必改动!而且不管用什么语言作主应用,都可以利用 Delphi 的强大部分(单元结构 - 让你的代码容易管理;TList - 我用得是最多的一个类,加载数据表;以及 string ,可以使你在处理串时偷到不少懒...)来使你的应用更加容易开发!


2003-10-31 14:57:37 使用动态链接库简介:本章内容:
. 究竟什么是D L L
. 静态链接与动态链接
. 为什么使用D L L
. 创建和使用D L L
. 显示D L L中的模式窗体
. 在D e l p h i应用程序中使用D L L
. DLL的入口和出口函数
. DLL中的异常
. 回调函数
. 从D L L中调用回调函数
. 在不同的过程中共享D L L数据
. 引出D L L中的对象
本章讨论了Wi n 3 2动态链接库,也就是D L L。D L L是用来编写Wi n d o w s应用程序的关键组成部分。
本章讨论了使用和创建D L L的几个方面,它给出了D L L怎样工作的概述并讨论了怎样创建和使用D L L, 你将学会怎样调入D L L和链接由它们引出的过程和函数的不同方法。本章还包括回调函数的使用并举例说明在不同调用进程中如何实现共享数据。

1 究竟什么是DLL
动态链接库是程序模块,它包括代码、数据或资源,能够被其他的Wi n d o w s应用程序共享。DLL的主要特点之一是应用程序可以在运行时调入代码执行,而不是在编译时链接代码,因此,多个应用程序可以共享同一个D L L的代码。事实上,文件Kernel32.dll、User32.dll、GDI32.dll就是核心Win32系统的动态链接库。Kernel.dll负责内存、进程和线程的管理。USER32.DLL包含了一些程序,是创建窗口和处理Win32消息的用户接口。GDI32.DLL负责处理图形。你还会听说其他的系统DLL,譬如AdvAPI32.dll和ComDlg32.dll,它们分别处理对象安全性/注册操作和通用对话框。
使用动态链接库的另一个特点是有利于应用程序的模块化。这样就简化了应用程序的修改,因为一般只需要修改DLL,而不是整个应用程序。Windows环境自身就是模块化类型的典型实例。每当安装一个新设备,就安装一个设备驱动程序(即DLL ),使设备能够与Windows相互通信。在磁盘上,一个DLL基本上类似于一个Windows可执行文件(*.EXE)。一个主要的区别是,DLL不是一个独立的可执行文件,尽管它可能包含了可执行代码。大部分DLL文件的扩展名是.dll,也有的可能是.drv(设备驱动程序)、.sys(系统文件)、.fon(字体文件),这些不包含可执行代码。
注意Delphi引入了一种叫做程序包的特殊用途的DLL,它应用于Delphi和C++编程环境。
DLL通过动态链接技术(dynamic linking)与其他应用程序共享代码,这将在本章后面部分讨论。总之,当一个应用程序使用了一个DLL,Win32系统会确保内存中只有一个该DLL的拷贝,这是通过内存映射文件来实现的。DLL首先被调入Win32的全局堆,然后映射到调用这个DLL进程的地址空间。
在Win32系统中,每个进程都被分配有自己的3 2位线性地址空间。当一个DLL被多个进程调用时,每个进程都会获得该DLL的一份映像。因此,在16位Windows中,程序代码、数据、资源不被进程共享,而在Win32中,DLL是可以被看作是属于调用该DLL进程自己的代码。
但这并不意味着,如果多进程调用一个DLL,物理内存就分配有该DLL的每个实例。通过从系统的全局堆到调用该DLL的每一进程的地址空间的映射,DLL映像置于每个进程的地址空间。至少在理想情况下应这样。
设置DLL的首选基地址
如果DLL被调入进程的地址空间时设置了基地址,这样DLL数据就可以被共享。如果DLL的基地址与已经分配的DLL地址重叠的话,Win32重新分配基地址。这样,每一个重新分配的DLL实例都有自己的物理上的内存空间和交换文件空间。
这是很关键的,通过使用$IMAGEBASE指示符,给每个DLL都设置一个基地址,这样不会引起冲突或不会出现地址重叠。如果有多个应用程序都调用同一个D L L,设置一个唯一的基地址,这样无论是在进程的低端地址或者是在一般的D L L (如V C L包)的高端地址,都不会引起冲突。一般可执行文件(EXE和DLL)缺省的基地址为$400000,这就意味着,除非修改DLL的基地址,否则就会与主程序的基地址引起冲突,因此进程间也就不能共享DLL的数据。
在调用时,DLL不需要重新分配或安装,因为它保存在本地磁盘上,DLL的内存页面被直接映射到磁盘上的DLL文件。DLL代码不需占用系统页面文件(也叫交换文件)的空间。这就是为什么系统提交页的总数和大小可能比系统交换文件加内存要大。
你可以参阅Delphi的在线帮助中的“Image Base Address”,那里有使用$IMAGEBASE指示符的详细介绍。
有关D L L的一些术语如下:
. 应用程序,一个扩展名为.exe的Windows程序。
. 可执行文件,一个包含可执行代码的文件,它包括.dll文件和.exe文件。
. 实例,当提到应用程序和DLL时,在内存中出现的可执行文件就是实例。Win32系统通过实例句柄的方式来引用实例。例如,如果一个应用程序运行两次,就会有应用程序的两个实例,同时就有两个实例句柄。当一个DLL被调入时,实例及其相应的实例句柄同时产生。应该注意的是,这里所提的实例与类的实例不能混淆。
. 模块,在32位Windows系统中,模块和实例可以说是同义的。而在16位的Windows系统中,是建立一个模块数据库来管理模块的,一个模块对应一个模块句柄。在Win32中,应用程序的每一个实例都拥有自己的地址空间;所以,没有必要为模块单独指定标识符。不过,微软仍然保留了它自己的术语。注意一点,模块和实例是同一个概念。
. 任务,Windows是一个多任务(或任务切换)环境,所以它必须能够为运行的多个实例合理分配系统资源和时间。于是,Windows建立一个任务数据库,这个数据库包括任务的实例句柄和其他必要信息,以此实现任务切换功能。任务是Windows用来管理和分配资源与时间段的重要元素。

9.2 静态链接与动态链接
静态链接是指Delphi编译器把要调用的函数和过程编译成可执行代码。函数的代码可存留在应用程序的.dpr文件或一单元中。当链接用户的应用程序时,这些函数与过程便成为最终的可执行文件的一部分.

.......

假设有两个应用程序,都要调用一个单元的同一个函数,当然,这两个应用程序都要在其uses子句中包含该单元。如果这两个程序要同时运行,那么内存中就存在两份该函数,如果还有第三个这样的应用程序,内存中就会有第三份该函数的实例,这样,就会三次占据内存。这个小例子就表明了动态链接的优越性之一。函数通过动态链接,被放到一个DLL中。那么如果一个应用程序把该函数调入内存,其他应用程序就可以通过映射D L L的映像到自己进程内存空间来共享代码。理论上讲,最终结果是内存中只存在该DLL的一份实例。
对于动态链接,在程序运行时,通过引用一个外部函数(该函数包含在DLL中)而将该函数链接到可执行文件中。其中的引用可以在应用程序中声明,但是通常情况下是放在一个专门的引入(import)单后面的字符串就是该DLL的名称。要使用这个单元,应用程序只需把MaxUnit加到它的uses子句中即可。
当这个程序运行时,该DLL就会自动地被调入内存,并且任何需要调用Max()的程序都被链接到这个DLL中的Max()函数。
调用DLL有两种方式,这是其中一种,叫隐式调用,就是让Windows在应用程序调入时自动地调

3 为什么要使用DLL
使用D L L有若干理由,其中有一些前面已经提到过了。大体说来,使用动态链接库可以共享代码、系统资源,可以隐藏实现的代码或底层的系统例程、设计自定义控件。下面将分别讨论这几个方面的内容。
3.1 共享代码、资源和数据
在本章节前面已经提到,共享代码是创建动态链接库的主要目的所在。但与单元的代码共享不同,DLL的代码可以被任何Windows应用程序共享,而单元代码的共享局限于Delphi应用程序。
另外,DLL提供了共享资源的途径,诸如位图、字体、图标等等这些都可以放到一个资源文件中,并直接链接到应用程序。如果把这些资源放到DLL中,那么就可以让许多应用程序使用,而不必在内存里重复装入这些资源。
这行代码的意思表明,ClientToSreen()在动态链接库User32.dll中,它的名称叫ClientToSreen.
在1 6位的Windows中,DLL有自己的数据段,于是,所有要调用同一个DLL的应用程序能够访问同一个全局变量和静态变量。但在Win32系统中,这就不同了。因为DLL的映像被映射到每个进程的地址空间,该DLL的所有数据属于映射到的进程。值得一说的是,尽管进程间不能共享DLL的数据,但是同一个进程的所有线程可以共享,因为线程是相互独立的,所以在访问某一DLL的全局变量时,务必小心,防止引起冲突。
但这并不意味着没有办法实现在进程间共享一个DLL的数据。一个技术可以通过内存映射文件的方法在内存中创建一个共享的区域。一切需调用D L L的应用程序都可以读这些存储在内存中的共享区域的数据。
3.2 隐藏实现的细节
有些时候,你可能想隐藏例程实现的细节,DLL就可以实现这一点。不管为何要隐藏你的代码,DLL可以使函数被应用程序访问,而其中的代码细节不被显现,你所要做的只是提供别人能访问DLL的接口。你也许认为Delphi的编译单元(DCU)也可以隐藏细节,但是DCU只适用于Delphi应用程序,而且还受版本的局限。而DLL与语言无关,所以,创建的DLL可以被C++、VB或其他任何支持DLL的语言调用。
Windows单元是Win32 DLL的接口单元。Delphi提供了Win32 API的源文件,其一是Windows单元的源文件windows.pas.
3.3 自定义控件
自定义控件通常放在DLL中。这些控件不同于Delphi的自定义组件。自定义控件是在Windows下注册,并且可以在任何Windows开发环境中使用。将这些类型的自定义控件加进DLL中,是考虑到即使有多个应用程序要使用这些自定义控件,内存中也只有该控件的一份实例。
注意其实将自定义控件加进DLL这种机制已经过时,现在,微软使用OLE和ActiveX控件,自定义控件已很少见了。


2003-10-31 15:13:22 Delphi环境中编写调用DLL的方法和技巧 第一章 为什么要使用动态链接库(DLL)
提起DLL您一定不会陌生,在Windows中有着大量的以DLL为后缀的文件,它们是保证Windows正常运行和维护升级的重要保证。其实,DLL是一种特殊的可执行文件。说它特殊主要是因为一般它都不能直接运行,需要宿主程序比如*.EXE程序或其他DLL的动态调用才能够使用。简单的说,在通常情况下DLL是经过编译的函数和过程的集合。
使用DLL技术主要有以下几个原因:
一、减小可执行文件大小。
DLL技术的产生有很大一部分原因是为了减小可执行文件的大小。当操作系统进入Windows时代后,其大小已经达到几十兆乃至几百兆。试想如果还是使用DOS时代的单执行文件体系的话一个可执行文件的大小可能将达到数十兆,这是大家都不能接受的。解决的方法就是采用动态链接技术将一个大的可执行文件分割成许多小的可执行程序。
二、实现资源共享。
这里指的资源共享包括很多方面,最多的是内存共享、代码共享等等。早期的程序员经常碰到这样的事情,在不同的编程任务中编写同样的代码。这种方法显然浪费了很多时间,为了解决这个问题人们编写了各种各样的库。但由于编程语言和环境的不同这些库一般都不能通用,而且用户在运行程序时还需要这些库才行,极不方便。DLL的出现就像制定了一个标准一样,使这些库有了统一的规范。这样一来,用不同编程语言的程序员可以方便的使用用别的编程语言编写的DLL。另外,DLL还有一个突出的特点就是在内存中只装载一次,这一点可以节省有限的内存,而且可以同时为多个进程服务。
三、便于维护和升级。
细心的朋友可能发现有一些DLL文件是有版本说明的。(查看DLL文件的属性可以看到,但不是每一个DLL文件都有)这是为了便于维护和升级。举个例子吧,早期的Win95中有一个BUG那就是在闰年不能正确显示2月29日这一天。后来,Microsoft发布了一个补丁程序纠正了这个BUG。值得一提的是,我们并没有重装Win95,而是用新版本的DLL代替了旧版本的DLL。(具体是哪一个DLL文件笔者一时想不起来了。)另一个常见的例子是驱动程序的升级。例如,著名的DirectX就多次升级,现在已经发展到了6.0版了。更妙的是,当我们试图安装较低版本的DLL时,系统会给我们提示,避免人为的操作错误。例如我们升级某硬件的驱动程序时,经常碰到Windows提示我们当前安装的驱动程序比原来的驱动程序旧。
四、比较安全。
这里说的安全也包括很多方面。比如,DLL文件遭受病毒的侵害机率要比普通的EXE文件低很多。另外,由于是动态链接的,这给一些从事破坏工作的“高手”们多少带来了一些反汇编的困难。

第二章 在Delphi中编写DLL top
说了那么多,总该言归正传了。编写DLL其实也不是一件十分困难的事,只是要注意一些事项就够了。为便于说明,我们先举一个例子。
library Delphi;
uses SysUtils, Classes;

function TestDll(i:integer):integer;stdcall;
begin
Result:=i;
end;
exports
TestDll;

begin
end.
上面的例子是不是很简单?熟悉Delphi的朋友可以看出以上代码和一般的Delphi程序的编写基本是相同的,只是在TestDll函数后多了一个stdcall参数并且用exports语句声明了TestDll函数。只要编译上面的代码,就可以得到一个名为Delphi.dll的动态链接库。现在,让我们来看看有哪些需要注意的地方。
一、在DLL中编写的函数或过程都必须加上stdcall调用参数。在Delphi 1或Delphi 2环境下该调用参数是far。从Delphi 3以后将这个参数变为了stdcall,目的是为了使用标准的Win32参数传递技术来代替优化的register参数。忘记使用stdcall参数是常见的错误,这个错误不会影响DLL的编译和生成,但当调用这个DLL时会发生很严重的错误,导致操作系统的死锁。原因是register参数是Delphi的默认参数。
二、所写的函数和过程应该用exports语句声明为外部函数。
正如大家看到的,TestDll函数被声明为一个外部函数。这样做可以使该函数在外部就能看到,具体方法是单激鼠标右键用“快速查看(QuickView)”功能查看该DLL文件。(如果没有“快速查看”选项可以从Windows CD上安装。)TestDll函数会出现在ExportTable栏中。另一个很充分的理由是,如果不这样声明,我们编写的函数将不能被调用,这是大家都不愿看到的。
三、当使用了长字符串类型的参数、变量时要引用ShareMem。
Delphi中的string类型很强大,我们知道普通的字符串长度最大为256个字符,但Delphi中string类型在默认情况下长度可以达到2G。(对,您没有看错,确实是两兆。)这时,如果您坚持要使用string类型的参数、变量甚至是记录信息时,就要引用ShareMem单元,而且必须是第一个引用的。既在uses语句后是第一个引用的单元。如下例:
uses ShareMem, SysUtils, Classes;
还有一点,在您的工程文件(*.dpr)中而不是单元文件(*.pas)中也要做同样的工作,这一点Delphi自带的帮助文件没有说清楚,造成了很多误会。不这样做的话,您很有可能付出死机的代价。避免使用string类型的方法是将string类型的参数、变量等声明为Pchar或ShortString(如:s:string[10])类型。同样的问题会出现在当您使用了动态数组时,解决的方法同上所述。

第三章 在Delphi中静态调用DL
调用一个DLL比写一个DLL要容易一些。首先给大家介绍的是静态调用方法,稍后将介绍动态调用方法,并就两种方法做一个比较。同样的,我们先举一个静态调用的例子。
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.DFM}

//本行以下代码为我们真正动手写的代码
function TestDll(i:integer):integer;stdcall;
external ’Delphi.dll’;
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text:=IntToStr(TestDll(1));
end;

end.
上面的例子中我们在窗体上放置了一个编辑框(Edit)和一个按钮(Button),并且书写了很少的代码来测试我们刚刚编写的Delphi.dll。大家可以看到我们唯一做的工作是将TestDll函数的说明部分放在了implementation中,并且用external语句指定了Delphi.dll的位置。(本例中调用程序和Delphi.dll在同一个目录中。)让人兴奋的是,我们自己编写的TestDll函数很快被Delphi认出来了。您可做这样一个实验:输入“TestDll(”,很快Delphi就会用fly-by提示条提示您应该输入的参数是什么,就像我们使用Delphi中定义的其他函数一样简单。注意事项有以下一些:
一、调用参数用stdcall。
和前面提到的一样,当引用DLL中的函数和过程时也要使用stdcall参数,原因和前面提到的一样。
二、用external语句指定被调用的DLL文件的路径和名称。
正如大家看到的,我们在external语句中指定了所要调用的DLL文件的名称。没有写路径是因为该DLL文件和调用它的主程序在同一目录下。如果该DLL文件在C:,则我们可将上面的引用语句写为external ’C:Delphi.dll’。注意文件的后缀.dll必须写上。
三、不能从DLL中调用全局变量。
如果我们在DLL中声明了某种全局变量,如:var s:byte 。这样在DLL中s这个全局变量是可以正常使用的,但s不能被调用程序使用,既s不能作为全局变量传递给调用程序。不过在调用程序中声明的变量可以作为参数传递给DLL。
四、被调用的DLL必须存在。
这一点很重要,使用静态调用方法时要求所调用的DLL文件以及要调用的函数或过程等等必须存在。如果不存在或指定的路径和文件名不正确的话,运行主程序时系统会提示“启动程序时出错”或“找不到*.dll文件”等运行错误。

第四章 在Delphi中动态调用DLL top
动态调用DLL相对复杂很多,但非常灵活。为了全面的说明该问题,这次我们举一个调用由C++编写的DLL的例子。首先在C++中编译下面的DLL源程序。

#include
extern ”C” _declspec(dllexport)
int WINAPI TestC(int i)
{
return i;
}
编译后生成一个DLL文件,在这里我们称该文件为Cpp.dll,该DLL中只有一个返回整数类型的函数TestC。为了方便说明,我们仍然引用上面的调用程序,只是将原来的Button1Click过程中的语句用下面的代码替换掉了。

procedure TForm1.Button1Click(Sender: TObject);
type
TIntFunc=function(i:integer):integer;stdcall;
var
Th:Thandle;
Tf:TIntFunc;
Tp:TFarProc;
begin
Th:=LoadLibrary(’Cpp.dll’); {装载DLL}
if Th>0 then
try
Tp:=GetProcAddress(Th,PChar(’TestC’));
if Tp<>nil
then begin
Tf:=TIntFunc(Tp);
Edit1.Text:=IntToStr(Tf(1)); {调用TestC函数}
end
else
ShowMessage(’TestC函数没有找到’);
finally
FreeLibrary(Th); {释放DLL}
end
else
ShowMessage(’Cpp.dll没有找到’);
end;
大家已经看到了,这种动态调用技术很复杂,但只要修改参数,如修改LoadLibrary(’Cpp.dll’)中的DLL名称为’Delphi.dll’就可动态更改所调用的DLL。
一、定义所要调用的函数或过程的类型。
在上面的代码中我们定义了一个TIntFunc类型,这是对应我们将要调用的函数TestC的。在其他调用情况下也要做同样的定义工作。并且也要加上stdcall调用参数。
二、释放所调用的DLL。
我们用LoadLibrary动态的调用了一个DLL,但要记住必须在使用完后手动地用FreeLibrary将该DLL释放掉,否则该DLL将一直占用内存直到您退出Windows或关机为止。
现在我们来评价一下两种调用DLL的方法的优缺点。静态方法实现简单,易于掌握并且一般来说稍微快一点,也更加安全可靠一些;但是静态方法不能灵活地在运行时装卸所需的DLL,而是在主程序开始运行时就装载指定的DLL直到程序结束时才释放该DLL,另外只有基于编译器和链接器的系统(如Delphi)才可以使用该方法。动态方法较好地解决了静态方法中存在的不足,可以方便地访问DLL中的函数和过程,甚至一些老版本DLL中新添加的函数或过程;但动态方法难以完全掌握,使用时因为不同的函数或过程要定义很多很复杂的类型和调用方法。对于初学者,笔者建议您使用静态方法,待熟练后再使用动态调用方法。

第五章 使用DLL的实用技巧 top
一、编写技巧。
1 、为了保证DLL的正确性,可先编写成普通的应用程序的一部分,调试无误后再从主程序中分离出来,编译成DLL。
2、为了保证DLL的通用性,应该在自己编写的DLL中杜绝出现可视化控件的名称,如:Edit1.Text中的Edit1名称;或者自定义非Windows定义的类型,如某种记录。
3 、为便于调试,每个函数和过程应该尽可能短小精悍,并配合具体详细的注释。
4 、应多利用try-finally来处理可能出现的错误和异常,注意这时要引用SysUtils单元。
5 、尽可能少引用单元以减小DLL的大小,特别是不要引用可视化单元,如Dialogs单元。例如一般情况下,我们可以不引用Classes单元,这样可使编译后的DLL减小大约16Kb。
二、调用技巧。
1 、在用静态方法时,可以给被调用的函数或过程更名。在前面提到的C++编写的DLL例子中,如果去掉extern ”C”语句,C++会编译出一些奇怪的函数名,原来的TestC函数会被命名为@TestC$s等等可笑的怪名字,这是由于C++采用了C++ name mangling技术。这个函数名在Delphi中是非法的,我们可以这样解决这个问题:
改写引用函数为
function TestC(i:integer):integer;stdcall;
external ’Cpp.dll’;name ’@TestC$s’;
其中name的作用就是重命名。
2、可把我们编写的DLL放到Windows目录下或者Windowssystem目录下。这样做可以在external语句中或LoadLibrary语句中不写路径而只写DLL的名称。但这样做有些不妥,这两个目录下有大量重要的系统DLL,如果您编的DLL与它们重名的话其后果简直不堪设想,况且您的编程技术还不至于达到将自己编写的DLL放到系统目录中的地步吧!
三、调试技巧。
1、我们知道DLL在编写时是不能运行和单步调试的。有一个办法可以,那就是在Runparameters菜单中设置一个宿主程序。在Local页的Host Application栏中添上宿主程序的名字就可进行单步调试、断点观察和运行了。
2、添加DLL的版本信息。开场白中提到了版本信息对于DLL是很重要的,如果包含了版本信息,DLL的大小会增加2Kb。增加这么一点空间是值得的。很不幸我们如果直接使用Projectoptions菜单中Version选项是不行的,这一点Delphi的帮助文件中没有提到,经笔者研究发现,只要加一行代码就可以了。如下例:

library Delphi;
uses
SysUtils, Classes;

{$R *.RES}
//注意,上面这行代码必须加在这个位置

function TestDll(i:integer):integer;stdcall;
begin
Result:=i;
end;

exports
TestDll;

begin
end.
3 、为了避免与别的DLL重名,在给自己编写的DLL起名字的时候最好采用字符数字和下划线混合的方式。如:jl_try16.dll。
4 、如果您原来在Delphi 1或Delphi 2中已经编译了某些DLL的话,您原来编译的DLL是16位的。只要将源代码在新的Delphi 3或Delphi 4环境下重新编译,就可以得到32位的DLL了。

[后记]:除了上面介绍的DLL最常用的使用方法外,DLL还可以用于做资源的载体。例如,在Windows中更改图标就是使用的DLL中的资源。另外,熟练掌握了DLL的设计技术,对使用更为高级的OLE、COM以及ActiveX编程都有很多益处。

Tags:

作者:佚名

文章评论评论内容只代表网友观点,与本站立场无关!

   评论摘要(共 0 条,得分 0 分,平均 0 分) 查看完整评论
PB创新网ourmis.com】Copyright © 2000-2009 . All Rights Reserved .
页面执行时间:30,859.38000 毫秒
Email:ourmis@126.com QQ:2322888 蜀ICP备05006790号