Windows的动态链接库道理及利用1
副标题#e#
1.Windows的动态链接库道理
动态链接库(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的字体文件等。
#p#副标题#e#
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 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 编译指示;
#p#分页标题#e#
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对用户透明,由系统自动挪用。
初始化代码完成的主要事情是:
#p#分页标题#e#
1.初始化变量、分派全局内存块、登录窗口工具等初始化事情。在(3.2)节"操作DLLs实现应用措施间的数据传输"中,用于数据共享的全局内存块就是在初始化代码中分派的。
退出进程LibExit中利用了一个系统界说变量ExitCode,用于符号退出时的状态。 ExitCode的取值与意义如下:
表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 动态挪用
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的错误代码。错误代码的意义如下表:
表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位扩展
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#p#分页标题#e#
如果在应用措施用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;