深入Delphi下的DLL编程 分享


======================================================
注:本文源代码点此下载
======================================================

相信有些计算机知识的朋友都应该听说过“dll”。尤其是那些使用过windows操作系统的人,都应该有过多次重装系统的“悲惨”经历——无论再 怎样小心,没有驱动损坏,没有病毒侵扰,仍然在使用(安装)了一段时间软件后,发现windows系统越来越庞大,操作越来越慢,还不时的出现曾经能使用 的软件无法使用的情况,导致最终不得不重装系统。这种情况常常是由于dll文件的大量安装和冲突造成的。这一方面说明dll的不足,另一方面也说明dll 的重要地位,以至我们无法杜绝它的使用。

dll(动态链接库,dynamic link library)简单来说是一种可通过调用执行的已编译的代码模块。dll是windows系统的早期产物。当时的主要目的是为了减少应用程序对内存的使 用。只有当某个函数或过程需要被使用时,才从硬盘调用它进入内存,一旦没有程序再调用该dll了,才将其从内存中清除。光说整个windows系统,就包 括了成百上千个dll文件,有些dll文件的功能是比较专业(比如网络、数据库驱动)甚至可以不安装的。假如这些功能全部要包括在一个应用程序 (application program)里,windows将是一个数百m大小的exe文件。这个简单的例子很容易解释dll的作用,而调用dll带来的性能损失则变得可被忽略 不计。

多个应用程序调用同一个dll,在内存里只有一个代码副本。而不会象静态编译的程序那样每一个都必须全部的被装入。装载dll时,它将被映射到进程的地址空间,同时使用dll的动态链接并非将库代码拷贝,而仅仅记录函数的入口点和接口。

同 时dll还能带来的共享的好处。一家公司开发的不同软件可能需要一些公用的函数/过程,这些函数/过程可能是直接的使用一些内部开发的dll;一些常用的 功能则可以直接使用windows的标准dll,我们常说的windows api就是包含在windows几个公用dll文件里的函数/过程;理论上(如果不牵涉作者的版权),知道一个dll的声明及作用(函数定义的输入参数及 返回值),我们完全可以在不清楚其实现(算法或编译方式)的情况下直接使用它。

假如一个dll中函数/过程的算法得到了更新,bug得到了修正, 整个dll文件会得到升级。一般来说为了保证向下兼容,调用声明与返回结果应该保持不变。但实际上,即使是同一家开发的dll,随着功能的改善,也很难保 证某个调用执行完全不变。在使用其他人开发的dll时这种糟糕情况更加的严重。比如我在一个绘图程序里使用了某著名图形软件商旧版本的dll包,我所有的 调用都是根据他发布的旧版的声明来执行的。假设用户安装了该软件商的一个新软件,导致其中部分dll被更新升级,假如这些dll已经有过改动,直接后果将 是我的软件不再稳定甚至无法运行!不要轻视这种情况,事实上它是很普遍的,比如windows在修正bug和升级过程中,就不断改动它包含的那些dll。 往往新版dll不是简单的增加新的函数/过程,而是更换甚至取消了原有的声明,这时候我们再也无法保证所有程序都运行正常。

dll除了上面提到的 改善计算机资源利用率、增加开发效率、隐藏实现细节外,还可以包含数据和各种资源。比如开发一个软件的多国语言版,就可以使用dll将依赖于语言的函数和 资源分离出来,然后让各地的用户安装不同对应的dll,以获取本地字符集的支持。再比如一个软件必须的图形、图标等资源,也可以直接放在dll文件中统一 安装管理。

创建一个dll

在进行后面的讲解之前,我想大家应该先清楚一个概念:例程声明的是一个指针变量,调用函数/过程,其实是通过指针转入该函数/过程的执行代码。

我们先尝试用delphi来建立一个自己的dll文件。这个dll包含一个标准的目录删除(包含子目录及文件)函数。

建立dll

通过delphi建立一个dll是很容易的。new一个新project,选择dll wizard,然后会生成一个非常简单的单元。该单元不象一般的工程文件以program开始,而是以library开始的。

该工程单元缺省引用了sysutils、classes两个单元。可以直接在该单元的uses之后,begin … end部分之前添加函数/过程代码,也可以在工程中添加包含代码的单元,然后该单元将会被自动uses。

接下来是编写dll例程的代码。如果是引用单元里的例程,需要通过声明时添加export后缀引出。假如是直接写在library单元中的,则不必再写export了。

最后一步是在library单元的begin语句之上,uses部分及函数定义之下添加exports部分,并列举需要引出的例程名称。注意仅仅是名称,不包含procedure或function关键字,也不需要参数、返回值和后缀。

exports语句后的语法有三种形式(例程指具体的函数/过程):

exports例程名;

exports例程名 index 索引值;

exports例程名 name新名称;

索引值和新名称便于其他程序确定函数地址;也可以不指定,如果没有使用index关键字,delphi将按照exports后的顺序从1开始自动分配索引号。exports后可跟多个例程,之间以逗号分隔。

编译,build最终的dll文件。

需注意的格式

为了保证生成的dll能正确与c++等语言兼容,需要注意以下几点:

尽量使用简单类型或指针作为参数及返回值的类型。 这里的简单类型是指c++的简单类型,所以string字符串类型最好转换成pchar字符指针。直接使用string的dll例程在delphi开发的 程序中调用是没有问题的(有资料指出需加入sharemem做为第一单元以确保正确),但如果使用c++或其他语言开发的程序调用,则不能保证参数传递正 确;

虽然过程是允许的,但是最好习惯全部写成函数。过程则返回执行正确与否的true/false;

对于参数的指示字比如const(只读)、out(只写)等等,为保证调用的兼容性,最好使用缺省方式(缺省var,即可读写的地址);

使用stdcall声明后缀,以保证正确的异常处理。16位dll无法通过这种方式处理异常,所以还得在例程最外层用try … except将异常处理掉;

一般不使用far后缀,除非为了保持与16位兼容。

范例代码

dll工程单元:

library fileoperate;

uses

sysutils,

classes,

udirectory in 'udirectory.pas';

{$r *.res}

exports

deletedir;

begin

end.

函数功能实现单元:

unit udirectory;

interface

uses

classes, sysutils;

function deletedir(dirname : pchar):boolean;export;stdcall;

implementation

function deletedir(dirname : pchar):boolean;

var

findfile: tsearchrec;

s : string;

begin

s := dirname;

if copy(s,length(s),1)fadirectory then begin

//文件则删除

deletefile(s + findfile.name);

end

else begin

//目录则嵌套自身

if (findfile.name'..') then

deletedir(pchar(s + findfile.name));

end;

until findnext(findfile)user32.txt

然后打开user32.txt文件,找到exports from user32.dll行,之下的部分就是dll例程定义了,比如:

rvaord. hint name

-------- ---- ---- ----

000013711 0000 activatekeyboardlayout

00005c202 0001 adjustwindowrect

0000161b3 0002 adjustwindowrectex

name列就是例程的名称,ord就是该例程索引号。注意,该工具是不能得到例程的参数表的。如果参数错误,调用dll例程会引起堆栈错误而导致调用程序崩溃。

调用代码

建立一个普通工程,在main窗体上放置一个tshelltreeview控件(samples页),再放置一个按钮,添加代码如下:

function deletedir(dirname : pchar):boolean;stdcall;external 'fileoperate.dll';

procedure tform1.button1click(sender: tobject);

begin

if directoryexists(shelltreeview.path) then

if application.messagebox(pchar('确定删除目录'+quotedstr(shelltreeview.path)+'吗?'), 'information',mb_yesno) = idyes then

if deletedir(pchar(shelltreeview.path)) then

showmessage('删除成功');

end;

该范例调用的就是前面建立的dll。

注意,声明时要包括stdcall后缀,这样才能保证调用delphi开发的dll的例程中类似pchar这样的参数值传递正确。大家有兴趣可以试验一下,不加入stdcall或者safecall后缀执行上面代码,将不能保证成功传递字符串参数给dll函数。

调试方法

在delphi主菜单run项目中选择parameters,打开“run parameters”对话框。在host application中填入一个宿主程序(该程序调用了将要调试的dll),还可以在parameters中输入参数。保存内容,然后就可以在dll工 程中设置断点、跟踪/单步执行了。

run该dll工程,然后将运行宿主程序。执行会调用dll的操作,然后就能跟踪进入该dll的代码,接下来的调试操作和普通程序是一样的。

因 为操作系统或其他软件影响的原因,可能会出现进行了上述步骤仍然无法正常跟踪/中断dll代码的情况。这时可以试试在菜单 project |options 对话框的 linker 页面里将 exe and dll options 中的include td32 debug info及include remote debug symbols两个选项选中。

假如还是不能中断 -_____-||| 那只好另外建立一个引用执行代码单元的应用程序,写代码调用例程调试完成后再编译dll了(其实该方法有时候蛮方便的,但有时候亦非常麻烦)。

引入文件

dll比较复杂时,可以为它的声明专门创建一个引入单元,这会使该dll变得更加容易维护和查看。引入单元的格式如下:

unit mydllimport; {import unit for mydll.dll }

interface

procedure mydllproc;

implementation

procedure mydllproc;external 'mydll' index 1;

end.

这样以后想要使用mydll中的例程时,只要简单的在程序模块中的uses子句中加上mydllimport即可。其实这仅仅是种方便开发的技巧,大家打开windows等引入windows api的单元,可以看到类似的做法。

动态(显式)调用dll

前面讲述静态调用dll时提到,dll会在启动调用程序时即被调入。所以这样的做法只能起到公用dll以及减小运行文件大小的作用,而且dll装载出错会立刻导致整个启动过程终止,哪怕该dll在运行中只起到微不足道的作用。

使用动态调用dll的方式,仅在调用外部例程时才将dll装载内存(引用记数为0时自动将该dll从内存中清除),从而节约了内存空间。而且可以判断装载是否正确以避免调用程序崩溃的情况,最多损失该例程功能而已。

动态调用虽然有上述优点,但是对于频繁使用的例程,因dll的调入和释放会有额外的性能损耗,所以这样的例程则适合使用静态引入。

调用范例

dll动态调用的原理是首先声明一个函数/过程类型并创建一个指针变量。为了保证该指针与外部例程指针一致以确保赋值正确,函数 /过程的声明必须和外部例程的原始声明兼容(兼容的意思是1、参数名称可以不一样;2、参数/返回值类型至少保持可以相互赋值,比如原始类型声明为 word,新的声明可以为integer,假如传递的实参总是在word的范围内,就不会出错)。

接下来通过windows api函数loadlibrary引入指定的库文件,loadlibrary的参数是dll文件名,返回一个thandle。如果该步骤成功,再通过另一 个api函数getprocaddress获得例程的入口地址,参数分别为loadlibrary的指针和例程名,最终返回例程的入口指针。将该指针赋值 给我们预先定义好的函数/过程指针,然后就可以使用这个函数/过程了。记住最后还要使用api函数freelibrary来减少dll引用记数,以保证 dll使用结束后可以清除出内存。这三个api函数的delphi声明如下:

function loadlibrary(libfilename:pchar):thandle;

function getprocaddress(module:thandle;procname:pchar):tfarproc;

procedure freelibrary(libmodule:thandle);

将前面静态调用dll例程的代码更改为动态调用,如下所示:

type

tdllproc = function (pathname : pchar):boolean;stdcall;

var

libhandle: thandle;

delpath: tdllproc;

begin

libhandle := loadlibrary(pchar('fileoperate.dll'));

if libhandle >= 32 then begin

try

delpath := getprocaddress(libhandle,pchar('deletedir'));

if directoryexists(shelltreeview.path) then

if application.messagebox(pchar('确定删除目录'+quotedstr(shelltreeview.path)+'吗?'), 'information',mb_yesno) = idyes then

if delpath(pchar(shelltreeview.path)) then

showmessage('删除成功');

finally

freelibrary(libhandle);

end;

end;

end;

16位dll的动态调入

下面将演示一个16位dll例程调用的例子,该例程是windows9x中的一个隐藏api函数。代码混合了静态、动态调用两种方式,除了进一步熟悉外,还可以看到调用16位dll的解决方法。先解释一下问题所在:

我 要实现的功能是获得win9x的“系统资源”。在winnt/2000下是没有“系统资源”这个概念的,因为winnt/2000中堆栈和句柄不再象 win9x那样被限制在64k大小。为了取该值,可以使用win9x的user dll中一个隐藏的api函数getfreesystemresources。

该dll例程必须动态引入。如果静态声明的话,在win2000里执行就会立即出错。这个兼容性不解决是不行的。所以必须先判断系统版本,如果是win9x再动态加载。检查操作系统版本的代码是:

var

osversion: _osversioninfoa;

fwinveris9x: boolean;

begin

osversion.dwosversioninfosize := sizeof(_osversioninfoa);

getversionex(osversion);

fwinveris9x := osversion.dwplatformid = ver_platform_win32_windows;

end;

以上直接调用api函数,已在windows单元中被声明。

function loadlibrary16(libraryname: pchar): thandle; stdcall; external kernel32 index 35;

procedure freelibrary16(hinstance: thandle); stdcall; external kernel32 index 36;

function getprocaddress16(hinstance: thandle; procname: pchar): pointer; stdcall; external kernel32 index 37;

function twinresmonitor.getfreesystemresources(sysresource: word): word;

type

tgetfreesysres = function (value : integer):integer;stdcall;

tqtthunk = procedure();cdecl;

var

prochandle : thandle;

getfreesysres : tgetfreesysres;

procthunkh : thandle;

qtthunk: tqtthunk;

thunktrash: array[0..$20] of word;

begin

result := 0;

thunktrash[0] := prochandle;

if fwinveris9x then begin

prochandle := loadlibrary16('user.exe');

if prochandle >= 32 then begin

getfreesysres := getprocaddress16(prochandle,pchar('getfreesystemresources'));

if assigned(getfreesysres) then begin

procthunkh :=loadlibrary(pchar('kernel32.dll'));

if procthunkh >= 32 then begin

qtthunk := getprocaddress(procthunkh,pchar('qt_thunk'));

if assigned(qtthunk) then

asm

push sysresource//push arguments

mov edx, getfreesysres//load 16-bit procedure pointer

call qtthunk//call thunk

mov result, ax//save the result

end;

end;

freelibrary(procthunkh);

end;

end;

freelibrary16(prochandle);

end

else result := 100;

end;

首 先,loadlibrary16等三个api是静态声明的(也可以动态声明,我这么做是为了减少代码)。由于loadlibrary无法正常调入16位的 例程(微软啊!),所以改用 loadlibrary16、freelibrary16、getprocaddress16,它们与loadlibrary、freelibrary、 getprocaddress的意义、用法、参数都一致,唯一不同的是必须用它们才能正确加载16位的例程。

在定义部分声明了函数指针tgetfreesysres 和tqtthunk。stdcall、cdecl参数定义堆栈的行为,必须根据原函数定义,不能更改。

假如类似一般的例程调用方式,跟踪到这一步:if assigned(getfreesysres) then begin getfreesysres已经正确加载并且有了函数地址,却无法正常使用getfreesysres(int)!!!

所以这里动态加载(理由也是在win2k下无法执行)了一个看似多余的过程qt_thunk。对于一个32位的外部例程,是不需要qt_thunk的, 但是,对于一个16位的例程,就必须使用如上汇编代码(不清楚的朋友请参考delphi语法资料)

asm

push sysresource

mov edx, getfreesysres

call qtthunk

mov result, ax

end;

它的作用是将压入参数压入堆栈,找到getfreesysres的地址,用qtthunk来转换16位地址到32位,最后才能正确的执行并返回值!

delphi开发dll常见问题

字符串参数

前面曾提到过,为了保证dll参数/返回值传递的正确性,尤其是为c++等其他语言开发的宿主程序使用时,应尽量使用指针或基本 类型,因为其他语言与delphi的变量存储分配方法可能是不一样的。c++中字符才是基本类型,串则是字符型的线形链表。所以最好将string强制转 换为pchar。

如果dll和宿主程序都用delphi开发,且使用string(还有动态数组,它们的数据结构类似)作为导出例程的参数/返回 值,那么添加sharemem为工程文件uses语句的第一个引用单元。sharemem是borland共享的内存管理器borlndmm.dll的接 口单元。引用该单元的dll的发布需要包括borlndmm.dll,否则就得避免使用string。

接口函数的大小写问题和dll的重复寻址问题:往往会导致你程序无法找到函数出口定位点.

在dll中建立及显示窗体

凡是基于窗体的delphi应用程序都自动包含了一个全局对象application,这点大家是很熟悉的。值 得注意的是delphi创建的dll同样有一个独立的application。所以若是在dll中创建的窗体要成为应用程序的模式窗体的话,就必须将该 application替换为应用程序的,否则结果难以预料(该窗体创建后,对它的操作比如最小化将不会隶属于任何主窗体)。在dll中要避免使用 showmessage而用messagebox。

创建dll中的模式窗体比较简单,把application.handle属性作为参数传递给dll例程,将该句柄赋与dll的application.handle,然后再用application创建窗体就可以了。

无 模式窗体则要复杂一些,除了创建显示窗体例程,还必须有一个对应的释放窗体例程。对于无模式窗体需要十分小心,创建和释放例程的调用都需在调用程序中得到 控制。这有两层意思:一要防止同一个窗体实例的多次创建;二由应用程序创建一个无模式窗体必须保证由应用程序释放,否则假如dll中有另一处代码先行释 放,再调用释放例程将会失败。

下面是dll窗体的代码:

unit usampleform;

interface

uses

windows, messages, sysutils, variants, classes, graphics, controls, forms,

dialogs, extctrls, stdctrls;

type

tsampleform = class(tform)

panel: tpanel;

end;

procedure createandshowmodalform(apphandle : thandle;caption : pchar);export;stdcall;

functioncreateandshowform(apphandle : thandle):longint;export;stdcall;

procedure closeshowform(aformref : longint);export;stdcall;

implementation

{$r *.dfm}

//模式窗体

procedure createandshowmodalform(apphandle : thandle;caption : pchar);

var

form : tsampleform;

str: string;

begin

application.handle := apphandle;

form := tsampleform.create(application);

try

str := caption;

form.caption := str;

form.showmodal;

finally

form.free;

end;

end;

//非模式窗体

function createandshowform(apphandle : thandle):longint;

var

form : tsampleform;

begin

application.handle := apphandle;

form := tsampleform.create(application);

result := longint(form);

form.show;

end;

procedure closeshowform(aformref : longint);

begin

if aformref > 0 then

tsampleform(aformref).release;

end;

end.

dll工程单元的引出声明:

exports

closeshowform,

createandshowform,

createandshowmodalform;

应用程序调用声明:

procedure createandshowmodalform(handle : thandle;caption : pchar);stdcall;external 'fileoperate.dll';

functioncreateandshowform(apphandle : thandle):longint;stdcall;external 'fileoperate.dll';

procedure closeshowform(aformref : longint);stdcall;external 'fileoperate.dll';

除了普通窗体外,怎么在dll中创建tmdichildform呢?其实与创建普通窗体类似,不过这次需要传递调用程序的application.mainform作为参数:

function showform(mainform:tform):integer;stdcall

var

form1: tform1;

ptr:plongint;

begin

ptr:=@(application.mainform);//先把dll的mainform句柄保存起来,也无须释放,只不过是替换一下

ptr^:=longint(mainform);//用调用程序的mainform替换dll的mainform

form1:=tform1.create(mainform);//用参数建立

end;

代码中用了一个临时指针的原因在application.mainform是只读属性。mdi窗体的formstyle不用设为fmmdichild。

初始化com库

如果在dll中使用了tadoconnection之类的com组件,或者activex控件,调用时会提示 “标记没有引用存储”等错误,这是因为没有初始化com。dll中不会调用coinitilizeex,初始化com库被认为是应用程序的责任,这是 borland的实现策略。

你需要做的是1、引用activex单元,保证coinitilizeex函数被正确调用了

2、在单元级加入初始化和退出代码:

initialization

coinitialize(nil);

finalization

couninitialize;

end.

3、 在结束时记住将连接和数据集关闭,否则也会报地址错误。

引出dll中的对象

从dll窗体的例子中可以发现,将句柄做为参数传递给dll,dll能指向这个句柄的实例。同样的道理,从dll中引出 对象,基本思路是通过函数返回dll中对象的指针,将该指针赋值到宿主程序的变量,使该变量指向内存中某对象的地址。对该变量的操作即对dll中的对象的 操作。

本文不再详解代码,仅说明需要注意的几点规则:

应用程序只能访问对象中的虚拟方法,所以要引用的对象方法必须声明为虚方法;

dll和应用程序中都需要相同的对象及方法定义,且方法定义顺序必须一致;

dll中的对象无法继承;

对象实例只能在dll中创建。

声明虚方法的目的不是为了重载,而是为了将该方法加入虚拟方法表中。对象的方法与普通例程是不同的,这样做才能让应用程序得到方法的指针。

dll毕竟是结构化编程时代的产物,基于函数级的代码共享,实现对象化已经力不从心。现在类似dll功能,但对对象提供强大支持的新方式已经得到普 遍应用,象接口(com/dcom/com+)之类的技术。进程内的服务端程序从外表看就是一个dll文件,但它不通过外部例程引出应用,而是通过注册发 布一系列接口来提供支持。它与dll从使用上有两个较大区别:需要注册,通过创建接口对象调用服务。可以看出,dll虽然通过一些技巧也可以引出对象,但 是使用不便,而且常常将对象化强制转为过程化的方式,这种情况下最好考虑新的实现方法。

注:本文代码在delphi6、7中调试通过。


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值