找回密码
 加入我们

QQ登录

只需一步,快速开始

搜索
查看: 7054|回复: 2

【分享】深入Delphi下的DLL编程

[复制链接]

1214

主题

352

回帖

11

精华

管理员

菜鸟

积分
93755

贡献奖关注奖人气王精英奖乐于助人勋章

发表于 2009-7-26 13:00:43 | 显示全部楼层 |阅读模式
<font face="宋体">相信有些计算机知识的朋友都应该听说过“DLL”。尤其是那些使用过windows操作系统的人,都应该有过多次重装系统的“悲惨”经历——无论再怎样小心,没有驱动损坏,没有病毒侵扰,仍然在使用(安装)了一段时间软件后,发现windows系统越来越庞大,操作越来越慢,还不时的出现曾经能使用的软件无法使用的情况,导致最终不得不重装系统。这种情况常常是由于dll文件的大量安装和冲突造成的。这一方面说明DLL的不足,另一方面也说明DLL的重要地位,以至我们无法杜绝它的使用。<br/>DLL(动态链接库,Dynamic Link Library)简单来说是一种可通过调用执行的已编译的代码模块。DLL是windows系统的早期产物。当时的主要目的是为了减少应用程序对内存的使用。只有当某个函数或过程需要被使用时,才从硬盘调用它进入内存,一旦没有程序再调用该DLL了,才将其从内存中清除。光说整个windows系统,就包括了成百上千个dll文件,有些dll文件的功能是比较专业(比如网络、数据库驱动)甚至可以不安装的。假如这些功能全部要包括在一个应用程序(Application program)里,windows将是一个数百M大小的exe文件。这个简单的例子很容易解释DLL的作用,而调用DLL带来的性能损失则变得可被忽略不计。<br/>多个应用程序调用同一个DLL,在内存里只有一个代码副本。而不会象静态编译的程序那样每一个都必须全部的被装入。装载DLL时,它将被映射到进程的地址空间,同时使用DLL的动态链接并非将库代码拷贝,而仅仅记录函数的入口点和接口。<br/>同时DLL还能带来的共享的好处。一家公司开发的不同软件可能需要一些公用的函数/过程,这些函数/过程可能是直接的使用一些内部开发的DLL;一些常用的功能则可以直接使用windows的标准DLL,我们常说的windows API就是包含在windows几个公用DLL文件里的函数/过程;理论上(如果不牵涉作者的版权),知道一个DLL的声明及作用(函数定义的输入参数及返回值),我们完全可以在不清楚其实现(算法或编译方式)的情况下直接使用它。<br/>假如一个DLL中函数/过程的算法得到了更新,BUG得到了修正,整个dll文件会得到升级。一般来说为了保证向下兼容,调用声明与返回结果应该保持不变。但实际上,即使是同一家开发的DLL,随着功能的改善,也很难保证某个调用执行完全不变。在使用其他人开发的DLL时这种糟糕情况更加的严重。比如我在一个绘图程序里使用了某著名图形软件商旧版本的DLL包,我所有的调用都是根据他发布的旧版的声明来执行的。假设用户安装了该软件商的一个新软件,导致其中部分DLL被更新升级,假如这些DLL已经有过改动,直接后果将是我的软件不再稳定甚至无法运行!不要轻视这种情况,事实上它是很普遍的,比如windows在修正BUG和升级过程中,就不断改动它包含的那些DLL。往往新版DLL不是简单的增加新的函数/过程,而是更换甚至取消了原有的声明,这时候我们再也无法保证所有程序都运行正常。<br/>DLL除了上面提到的改善计算机资源利用率、增加开发效率、隐藏实现细节外,还可以包含数据和各种资源。比如开发一个软件的多国语言版,就可以使用DLL将依赖于语言的函数和资源分离出来,然后让各地的用户安装不同对应的DLL,以获取本地字符集的支持。再比如一个软件必须的图形、图标等资源,也可以直接放在dll文件中统一安装管理。</font><br/><br/><font face="宋体">创建一个DLL</font><br/><font face="宋体">在进行后面的讲解之前,我想大家应该先清楚一个概念:例程声明的是一个指针变量,调用函数/过程,其实是通过指针转入该函数/过程的执行代码。<br/>我们先尝试用Delphi来建立一个自己的DLL文件。这个DLL包含一个标准的目录删除(包含子目录及文件)函数。</font><br/><font face="宋体">建立DLL<br/>通过Delphi建立一个DLL是很容易的。New一个新Project,选择DLL Wizard,然后会生成一个非常简单的单元。该单元不象一般的工程文件以program开始,而是以library开始的。<br/>该工程单元缺省引用了SysUtils、Classes两个单元。可以直接在该单元的uses之后,begin … end部分之前添加函数/过程代码,也可以在工程中添加包含代码的单元,然后该单元将会被自动uses。<br/>接下来是编写DLL例程的代码。如果是引用单元里的例程,需要通过声明时添加export后缀引出。假如是直接写在library单元中的,则不必再写export了。<br/>最后一步是在library单元的begin语句之上,uses部分及函数定义之下添加exports部分,并列举需要引出的例程名称。注意仅仅是名称,不包含procedure或function关键字,也不需要参数、返回值和后缀。<br/>exports语句后的语法有三种形式(例程指具体的函数/过程):<br/>exports例程名;<br/>exports例程名 index 索引值;<br/>exports例程名 name新名称;<br/>索引值和新名称便于其他程序确定函数地址;也可以不指定,如果没有使用Index关键字,Delphi将按照exports后的顺序从1开始自动分配索引号。Exports后可跟多个例程,之间以逗号分隔。<br/>编译,build最终的dll文件。</font><br/><font face="宋体">需注意的格式<br/>为了保证生成的DLL能正确与C++等语言兼容,需要注意以下几点:<br/>尽量使用简单类型或指针作为参数及返回值的类型。这里的简单类型是指C++的简单类型,所以string字符串类型最好转换成Pchar字符指针。直接使用string的DLL例程在Delphi开发的程序中调用是没有问题的(有资料指出需加入ShareMem做为第一单元以确保正确),但如果使用C++或其他语言开发的程序调用,则不能保证参数传递正确;<br/>虽然过程是允许的,但是最好习惯全部写成函数。过程则返回执行正确与否的true/false;<br/>对于参数的指示字比如const(只读)、out(只写)等等,为保证调用的兼容性,最好使用缺省方式(缺省var,即可读写的地址);<br/>使用stdcall声明后缀,以保证正确的异常处理。16位DLL无法通过这种方式处理异常,所以还得在例程最外层用Try … Except将异常处理掉;<br/>一般不使用far后缀,除非为了保持与16位兼容。</font><br/><font face="宋体">范例代码<br/>DLL工程单元:<br/>library FileOperate;</font><br/><font face="宋体">uses<br/>SysUtils,<br/>Classes,<br/>uDirectory in 'uDirectory.pas';</font><br/><font face="宋体">{$R *.res}</font><br/><font face="宋体">exports<br/>DeleteDir;</font><br/><font face="宋体">begin<br/>end.</font><br/><font face="宋体">函数功能实现单元:<br/>unit uDirectory;</font><br/><font face="宋体">interface</font><br/><font face="宋体">uses<br/>Classes, SysUtils;</font><br/><font face="宋体">function DeleteDir(DirName : Pchar):boolean;export;stdcall;</font><br/><font face="宋体">implementation</font><br/><font face="宋体">function DeleteDir(DirName : Pchar):boolean;<br/>var<br/>FindFile: TSearchRec;<br/>s : string;<br/>begin<br/>s := DirName;<br/>if copy(s,length(s),1) &lt;&gt; '\' then s := s+ '\';<br/>if DirectoryExists(s) then begin<br/>&nbsp;&nbsp;&nbsp; if FindFirst(s + '*.*', faAnyFile, FindFile) = 0 then begin<br/>&nbsp;&nbsp; repeat<br/>&nbsp;&nbsp; &nbsp;&nbsp; if FindFile.Attr &lt;&gt; faDirectory then begin<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //文件则删除<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; DeleteFile(s + FindFile.Name);<br/>&nbsp;&nbsp; &nbsp;&nbsp; end<br/>&nbsp;&nbsp; &nbsp;&nbsp; else begin<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //目录则嵌套自身<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; if (FindFile.Name &lt;&gt; '.') and (FindFile.Name &lt;&gt; '..') then<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; DeleteDir(Pchar(s + FindFile.Name));<br/>&nbsp;&nbsp; &nbsp;&nbsp; end;<br/>&nbsp;&nbsp; until FindNext(FindFile) &lt;&gt; 0;<br/>&nbsp;&nbsp; FindCLose(FindFile);<br/>&nbsp;&nbsp;&nbsp; end;<br/>end;</font><br/><font face="宋体">Result := RemoveDir(s);<br/>end;</font><br/><font face="宋体">end.</font><br/><font face="宋体">初始化及释放资源<br/>Delphi中初始化有几种方法。一种是利用Unit的Initalization与Finalization这两个小节(不知道“单元小节”?你该先去恶补Delphi语法了在4楼我找的关于Initalization Finalization的帖子)进行该单元中变量的初始化工作。注意,DLL虽然在内存中只有一个副本,但是例程隶属于调用者的不同进程空间。如果想初始化公共变量来达到多进程共享是不可行的,同时也要注意公共变量带来的冲突问题。<br/>二是在library单元的begin … end部分进行DLL的初始化。假如想在DLL结束时有对应代码,则可以利用DLL自动创建的一个ExitProc过程变量,这是一个退出过程的指针。建立一个自己的过程,并将该过程的地址赋与ExitProc。因为ExitProc是DLL创建时就存在的,所以在begin … end部分就应该进行此步操作。同时建立一个临时指针变量保存最初的ExitProc值,在自己的退出过程中将ExitProc值赋回来。这样做是为了进行自己的退出操作后,能完成缺省的DLL退出操作(与在重载的Destory方法中inherated的意义是一样的,完成了自己的destory,还需要进行缺省的父类destory才完整)。<br/>示例如下:<br/>library MyDLL;<br/>  ...<br/>var<br/>OldExitProc: pointer; //公共变量,为的保存最初的ExitProc指针以便赋回<br/>procedure MyExitProc;<br/>  begin<br/>…//对应初始化的结束代码<br/>  ExitProc := OldExitProc; //自己的退出过程中要记住将ExitProc赋回<br/>end;<br/>  ...<br/>  begin<br/>   ... //初始化代码<br/>  OldExitProc := ExitProc;<br/>   ExitProc := @MyExitProc;<br/>  end.<br/>第三种方法和ExitProc类似,在System单元中预定义了一个指针变量DllProc(该方法需要引用 Windows单元)。在使用DLLProc时, 必须先写好一个具有以下原型的程序:<br/>  procedure DLLHandler(dwReason: DWORD); stdcall;<br/>并在library的begin..end.之间, 将这个DLLHandler程序的执行地址赋给DLLProc中, 这时就可以根据参数Reason的值分别作出相应的处理。示例如下:<br/>  library MyDLL;<br/>  ...<br/>  procedure MyDLLHandler(dwReason: DWORD);<br/>  begin<br/>   case dwReason of<br/>    DLL_Process_Attach: //进程进入时<br/>    DLL_Process_Detach: //进程退出时<br/>    DLL_Thread_Attach: //线程进入时<br/>    DLL_Thread_Detach: //线程退出时<br/>   end;<br/>  end;<br/>  ...<br/>  begin<br/>   ... //初始化代码<br/>  DLLProc := @MyDLLHandler;<br/>   MyDLLHandle(DLL_Process_Attach);<br/>  end.<br/>可见,通过DLLProc 在处理多进程时比ExitProc更加强大和灵活。</font><br/><font face="宋体"><br/></font><br/><br/><font face="宋体">&nbsp;&nbsp;</font><font face="宋体"> &nbsp;&nbsp;&nbsp;</font>
<div class="comment">
<div class="comment_list">
<div class="comment_list_caption">
<div class="box_l"><a class="author" href="http://wghai.com/space.php?uid=2643534">梦回千秋</a> (2009-3-03 12:26:28)</div></div>
<div class="comment_content">静态(隐式)调用DLL<br/><br/>DLL已经有了,接下来我们看如何调用并调试它。普通的DLL是不需要注册的,但是要包含在windows搜索路径中才能被找到。搜索路径的顺序是:当前目录;Path路径;windows目录;widows系统目录(system、system32)。<br/><br/>引入DLL例程的声明方法<br/>在需要使用外部例程(DLL函数/过程)的代码之前预定义该函数/过程。即按DLL中的定义原样声明,且仅需要声明。同时加上external后缀引入,与export引出相对应。根据exports的三种索引语法,也有三种确定例程的方式(以函数声明为例):<br/>function 函数名(参数表):返回值;external ’DLL文件名’;<br/>function 函数名(参数表):返回值;external ’DLL文件名’ index 索引号;<br/>function 函数名(参数表):返回值;external ’DLL文件名’ name 新名称;<br/>如果不确定例程名称,可以用索引方式引入。如果按原名引入会发生冲突,则可以用“新名称”引入。<br/>进行声明后DLL函数的使用就和一般函数相同了。静态调用方式简单,但在启动调用程序时即调入DLL作为备用过程。如果此DLL文件不存在,那么启动时即会提示错误并立刻终止程序,不管定义是否使用。<br/>快速查看DLL例程定义可以使用Borland附带的工具tdump.exe(在Delphi或BCB的bin目录下),示例如下:<br/>Tdump c:\windows\system\user32.dll &gt; user32.txt<br/>然后打开user32.txt文件,找到Exports from USER32.dll行,之下的部分就是DLL例程定义了,比如:<br/>&nbsp;&nbsp;&nbsp; RVA &nbsp;&nbsp; Ord. Hint Name<br/>&nbsp;&nbsp;&nbsp; -------- ---- ---- ----<br/>&nbsp;&nbsp;&nbsp; 00001371 &nbsp;&nbsp;&nbsp; 1 0000 ActivateKeyboardLayout<br/>&nbsp;&nbsp;&nbsp; 00005C20 &nbsp;&nbsp;&nbsp; 2 0001 AdjustWindowRect<br/>&nbsp;&nbsp;&nbsp; 0000161B &nbsp;&nbsp;&nbsp; 3 0002 AdjustWindowRectEx<br/>Name列就是例程的名称,Ord就是该例程索引号。注意,该工具是不能得到例程的参数表的。如果参数错误,调用DLL例程会引起堆栈错误而导致调用程序崩溃。<br/><br/>调用代码<br/>建立一个普通工程,在Main窗体上放置一个TShellTreeView控件(Samples页),再放置一个按钮,添加代码如下:<br/>function DeleteDir(DirName : Pchar):boolean;stdcall;external 'FileOperate.dll';<br/><br/>procedure TForm1.Button1Click(Sender: TObject);<br/>begin<br/>if DirectoryExists(ShellTreeView.Path) then<br/>&nbsp;&nbsp;&nbsp; if Application.MessageBox(Pchar('确定删除目录'+QuotedStr(ShellTreeView.Path)+'吗?'), 'Information',MB_YESNO) = IDYes then<br/>&nbsp;&nbsp; if DeleteDir(PChar(ShellTreeView.Path)) then<br/>&nbsp;&nbsp; &nbsp;&nbsp; showmessage('删除成功');<br/>end;<br/>该范例调用的就是前面建立的DLL。<br/>注意,声明时要包括stdcall后缀,这样才能保证调用Delphi开发的DLL的例程中类似PChar这样的参数值传递正确。大家有兴趣可以试验一下,不加入stdcall或者safecall后缀执行上面代码,将不能保证成功传递字符串参数给DLL函数。<br/><br/>调试方法<br/>在Delphi主菜单Run项目中选择Parameters,打开“Run Parameters”对话框。在Host Application中填入一个宿主程序(该程序调用了将要调试的DLL),还可以在Parameters中输入参数。保存内容,然后就可以在DLL工程中设置断点、跟踪/单步执行了。<br/>Run该DLL工程,然后将运行宿主程序。执行会调用DLL的操作,然后就能跟踪进入该DLL的代码,接下来的调试操作和普通程序是一样的。<br/>因为操作系统或其他软件影响的原因,可能会出现进行了上述步骤仍然无法正常跟踪/中断DLL代码的情况。这时可以试试在菜单Project |Options 对话框的 Linker 页面里将 EXE and DLL Options 中的Include TD32 debug info及include remote debug symbols两个选项选中。<br/>假如还是不能中断 -_____-||| 那只好另外建立一个引用执行代码单元的应用程序,写代码调用例程调试完成后再编译DLL了(其实该方法有时候蛮方便的,但有时候亦非常麻烦)。<br/><br/>引入文件<br/>DLL比较复杂时,可以为它的声明专门创建一个引入单元,这会使该DLL变得更加容易维护和查看。引入单元的格式如下:<br/>  unit MyDllImport; {Import unit for MyDll.dll }<br/>  interface<br/>  procedure MyDllProc;<br/>…<br/>implementation<br/>   procedure MyDllProc;external 'MyDll' index 1;<br/>…<br/>end.<br/>这样以后想要使用MyDll中的例程时,只要简单的在程序模块中的uses子句中加上MyDllImport即可。其实这仅仅是种方便开发的技巧,大家打开Windows等引入windows API的单元,可以看到类似的做法。<br/><br/><br/>动态(显式)调用DLL<br/><br/>前面讲述静态调用DLL时提到,DLL会在启动调用程序时即被调入。所以这样的做法只能起到公用DLL以及减小运行文件大小的作用,而且DLL装载出错会立刻导致整个启动过程终止,哪怕该DLL在运行中只起到微不足道的作用。<br/>使用动态调用DLL的方式,仅在调用外部例程时才将DLL装载内存(引用记数为0时自动将该DLL从内存中清除),从而节约了内存空间。而且可以判断装载是否正确以避免调用程序崩溃的情况,最多损失该例程功能而已。<br/>动态调用虽然有上述优点,但是对于频繁使用的例程,因DLL的调入和释放会有额外的性能损耗,所以这样的例程则适合使用静态引入。<br/><br/>调用范例<br/>DLL动态调用的原理是首先声明一个函数/过程类型并创建一个指针变量。为了保证该指针与外部例程指针一致以确保赋值正确,函数/过程的声明必须和外部例程的原始声明兼容(兼容的意思是1、参数名称可以不一样;2、参数/返回值类型至少保持可以相互赋值,比如原始类型声明为Word,新的声明可以为Integer,假如传递的实参总是在Word的范围内,就不会出错)。<br/>接下来通过windows API函数LoadLibrary引入指定的库文件,LoadLibrary的参数是DLL文件名,返回一个THandle。如果该步骤成功,再通过另一个API函数GetProcAddress获得例程的入口地址,参数分别为LoadLibrary的指针和例程名,最终返回例程的入口指针。将该指针赋值给我们预先定义好的函数/过程指针,然后就可以使用这个函数/过程了。记住最后还要使用API函数FreeLibrary来减少DLL引用记数,以保证DLL使用结束后可以清除出内存。这三个API函数的Delphi声明如下:<br/>Function LoadLibrary(LibFileNameChar):THandle;<br/>Function GetProcAddress(Module:THandle;ProcNameChar):TfarProc;<br/>Procedure FreeLibrary(LibModule:THandle);<br/><br/>将前面静态调用DLL例程的代码更改为动态调用,如下所示:<br/>type<br/>TDllProc = function (PathName : Pchar):boolean;stdcall;<br/>var<br/>LibHandle: THandle;<br/>DelPath : TDllProc;<br/>begin<br/>LibHandle := LoadLibrary(PChar('FileOperate.dll'));<br/>if LibHandle &gt;= 32 then begin<br/>&nbsp;&nbsp;&nbsp; try<br/>&nbsp;&nbsp; DelPath := GetProcAddress(LibHandle,PChar('DeleteDir'));<br/>&nbsp;&nbsp; if DirectoryExists(ShellTreeView.Path) then<br/>&nbsp;&nbsp; &nbsp;&nbsp; if Application.MessageBox(Pchar('确定删除目录'+QuotedStr(ShellTreeView.Path)+'吗?'), 'Information',MB_YESNO) = IDYes then<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; if DelPath(PChar(ShellTreeView.Path)) then<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; showmessage('删除成功');<br/>&nbsp;&nbsp;&nbsp; finally<br/>&nbsp;&nbsp; FreeLibrary(LibHandle);<br/>&nbsp;&nbsp;&nbsp; end;<br/>end;<br/>end;<br/><br/>16位DLL的动态调入<br/>下面将演示一个16位DLL例程调用的例子,该例程是windows9x中的一个隐藏API函数。代码混合了静态、动态调用两种方式,除了进一步熟悉外,还可以看到调用16位DLL的解决方法。先解释一下问题所在:<br/>我要实现的功能是获得win9x的“系统资源”。在winNT/2000下是没有“系统资源”这个概念的,因为winNT/2000中堆栈和句柄不再象win9X那样被限制在64K大小。为了取该值,可以使用win9x的user dll中一个隐藏的API函数GetFreeSystemResources。<br/>该DLL例程必须动态引入。如果静态声明的话,在win2000里执行就会立即出错。这个兼容性不解决是不行的。所以必须先判断系统版本,如果是win9x再动态加载。检查操作系统版本的代码是:<br/>var<br/>OSversion : _OSVERSIONINFOA;<br/>FWinVerIs9x: Boolean;<br/>begin<br/>OSversion.dwOSVersionInfoSize := sizeof(_OSVERSIONINFOA);<br/>GetVersionEx(OSversion);<br/>FWinVerIs9x := OSversion.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS;<br/>End;<br/>以上直接调用API函数,已在Windows单元中被声明。<br/><br/>function LoadLibrary16(LibraryName: PChar): THandle; stdcall; external kernel32 index 35;<br/>procedure FreeLibrary16(HInstance: THandle); stdcall; external kernel32 index 36;<br/>function GetProcAddress16(Hinstance: THandle; ProcName: PChar): Pointer; stdcall; external kernel32 index 37;<br/><br/>function TWinResMonitor.GetFreeSystemResources(SysResource: Word): Word;<br/>type<br/>TGetFreeSysRes = function (value : integer):integer;stdcall;<br/>TQtThunk = procedure();cdecl;<br/>var<br/>ProcHandle : THandle;<br/>GetFreeSysRes : TGetFreeSysRes;<br/>ProcThunkH : THandle;<br/>QtThunk : TQtThunk;<br/>ThunkTrash: array[0..$20] of Word;<br/>begin<br/>Result := 0;<br/>ThunkTrash[0] := ProcHandle;<br/>if FWinVerIs9x then begin<br/>&nbsp;&nbsp;&nbsp; ProcHandle := LoadLibrary16('user.exe');<br/>&nbsp;&nbsp;&nbsp; if ProcHandle &gt;= 32 then begin<br/>&nbsp;&nbsp; GetFreeSysRes := GetProcAddress16(ProcHandle,Pchar('GetFreeSystemResources'));<br/>&nbsp;&nbsp; if assigned(GetFreeSysRes) then begin<br/>&nbsp;&nbsp; &nbsp;&nbsp; ProcThunkH := LoadLibrary(Pchar('kernel32.dll'));<br/>&nbsp;&nbsp; &nbsp;&nbsp; if ProcThunkH &gt;= 32 then begin<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; QtThunk := GetProcAddress(ProcThunkH,Pchar('QT_Thunk'));<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; if assigned(QtThunk) then<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; asm<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; push SysResource &nbsp;&nbsp; &nbsp;&nbsp; //push arguments<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; mov edx, GetFreeSysRes //load 16-bit procedure pointer<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; call QtThunk &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp;&nbsp; //call thunk<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; mov Result, ax &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; //save the result<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; end;<br/>&nbsp;&nbsp; &nbsp;&nbsp; end;<br/>&nbsp;&nbsp; &nbsp;&nbsp; FreeLibrary(ProcThunkH);<br/>&nbsp;&nbsp; end;<br/>&nbsp;&nbsp;&nbsp; end;<br/>&nbsp;&nbsp;&nbsp; FreeLibrary16(ProcHandle);<br/>end<br/>else Result := 100;<br/>end;<br/>首先,LoadLibrary16等三个API是静态声明的(也可以动态声明,我这么做是为了减少代码)。由于LoadLibrary无法正常调入16位的例程(微软啊!),所以改用 LoadLibrary16、FreeLibrary16、GetProcAddress16,它们与LoadLibrary、FreeLibrary、GetProcAddress的意义、用法、参数都一致,唯一不同的是必须用它们才能正确加载16位的例程。<br/>在定义部分声明了函数指针TGetFreeSysRes 和TQtThunk。Stdcall、cdecl参数定义堆栈的行为,必须根据原函数定义,不能更改。<br/>假如类似一般的例程调用方式,跟踪到这一步:if assigned(GetFreeSysRes) then begin GetFreeSysRes已经正确加载并且有了函数地址,却无法正常使用GetFreeSysRes(int)!!!<br/>所以这里动态加载(理由也是在win2k下无法执行)了一个看似多余的过程QT_Thunk。对于一个32位的外部例程,是不需要QT_Thunk的, 但是,对于一个16位的例程,就必须使用如上汇编代码(不清楚的朋友请参考Delphi语法资料)<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; asm<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; push SysResource <br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; mov edx, GetFreeSysRes <br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; call QtThunk<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; mov Result, ax<br/>&nbsp;&nbsp; &nbsp;&nbsp; &nbsp;&nbsp; end;<br/>它的作用是将压入参数压入堆栈,找到GetFreeSysRes的地址,用QtThunk来转换16位地址到32位,最后才能正确的执行并返回值!</div></div>
<div class="comment_list">
<div class="comment_list_caption">
<div class="box_l"><a class="author" href="http://wghai.com/space.php?uid=2643534">梦回千秋</a> (2009-3-03 12:27:02)</div></div>
<div class="comment_content">Delphi开发DLL常见问题<br/><br/>字符串参数<br/>前面曾提到过,为了保证DLL参数/返回值传递的正确性,尤其是为C++等其他语言开发的宿主程序使用时,应尽量使用指针或基本类型,因为其他语言与Delphi的变量存储分配方法可能是不一样的。C++中字符才是基本类型,串则是字符型的线形链表。所以最好将string强制转换为Pchar。<br/>如果DLL和宿主程序都用Delphi开发,且使用string(还有动态数组,它们的数据结构类似)作为导出例程的参数/返回值,那么添加ShareMem为工程文件uses语句的第一个引用单元。ShareMem是Borland共享的内存管理器Borlndmm.dll的接口单元。引用该单元的DLL的发布需要包括Borlndmm.dll,否则就得避免使用string。<br/><br/>在DLL中建立及显示窗体<br/>凡是基于窗体的Delphi应用程序都自动包含了一个全局对象Application,这点大家是很熟悉的。值得注意的是Delphi创建的DLL同样有一个独立的Application。所以若是在DLL中创建的窗体要成为应用程序的模式窗体的话,就必须将该Application替换为应用程序的,否则结果难以预料(该窗体创建后,对它的操作比如最小化将不会隶属于任何主窗体)。在DLL中要避免使用ShowMessage而用MessageBox。<br/>创建DLL中的模式窗体比较简单,把Application.Handle属性作为参数传递给DLL例程,将该句柄赋与Dll的Application.Handle,然后再用Application创建窗体就可以了。<br/>无模式窗体则要复杂一些,除了创建显示窗体例程,还必须有一个对应的释放窗体例程。对于无模式窗体需要十分小心,创建和释放例程的调用都需在调用程序中得到控制。这有两层意思:一要防止同一个窗体实例的多次创建;二由应用程序创建一个无模式窗体必须保证由应用程序释放,否则假如DLL中有另一处代码先行释放,再调用释放例程将会失败。<br/>下面是DLL窗体的代码:<br/>unit uSampleForm;<br/><br/>interface<br/><br/>uses<br/>Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,<br/>Dialogs, ExtCtrls, StdCtrls;<br/><br/>type<br/>TSampleForm = class(TForm)<br/>&nbsp;&nbsp;&nbsp; Panel: TPanel;<br/>end;<br/><br/>procedure CreateAndShowModalForm(AppHandle : THandle;Caption : PChar);export;stdcall;<br/>function CreateAndShowForm(AppHandle : THandle):LongInt;export;stdcall;<br/>procedure CloseShowForm(AFormRef : LongInt);export;stdcall;<br/><br/>implementation<br/><br/>{$R *.dfm}<br/>//模式窗体<br/>procedure CreateAndShowModalForm(AppHandle : THandle;Caption : PChar);<br/>var<br/>Form : TSampleForm;<br/>str : string;<br/>begin<br/>Application.Handle := AppHandle;<br/>Form := TSampleForm.Create(Application);<br/>try<br/>&nbsp;&nbsp;&nbsp; str := Caption;<br/>&nbsp;&nbsp;&nbsp; Form.Caption := str;<br/>&nbsp;&nbsp;&nbsp; Form.ShowModal;<br/>finally<br/>&nbsp;&nbsp;&nbsp; Form.Free;<br/>end;<br/>end;<br/><br/>//非模式窗体<br/>function CreateAndShowForm(AppHandle : THandle):LongInt;<br/>var<br/>Form : TSampleForm;<br/>begin<br/>Application.Handle := AppHandle;<br/>Form := TSampleForm.Create(Application);<br/>Result := LongInt(Form);<br/>Form.Show;<br/>end;<br/><br/>procedure CloseShowForm(AFormRef : LongInt);<br/>begin<br/>if AFormRef &gt; 0 then<br/>&nbsp;&nbsp;&nbsp; TSampleForm(AFormRef).Release;<br/>end;<br/><br/>end.<br/><br/>DLL工程单元的引出声明:<br/>exports<br/>CloseShowForm,<br/>CreateAndShowForm,<br/>CreateAndShowModalForm;<br/><br/>应用程序调用声明:<br/>procedure CreateAndShowModalForm(Handle : THandle;Caption : PChar);stdcall;external 'FileOperate.dll';<br/>function CreateAndShowForm(AppHandle : THandle):LongInt;stdcall;external 'FileOperate.dll';<br/>procedure CloseShowForm(AFormRef : LongInt);stdcall;external 'FileOperate.dll';<br/><br/>除了普通窗体外,怎么在DLL中创建TMDIChildForm呢?其实与创建普通窗体类似,不过这次需要传递调用程序的Application.MainForm作为参数:<br/>function ShowForm(mainForm:TForm):integer;stdcall<br/>var<br/>Form1: TForm1;<br/>ptrLongInt;<br/>begin<br/>ptr:=@(Application.MainForm);//先把DLL的MainForm句柄保存起来,也无须释放,只不过是替换一下<br/>ptr^:=LongInt(mainForm);//用调用程序的mainForm替换DLL的MainForm<br/>Form1:=TForm1.Create(mainForm);//用参数建立<br/>end;<br/>代码中用了一个临时指针的原因在Application.MainForm是只读属性。MDI窗体的FormStyle不用设为fmMDIChild。<br/><br/>初始化COM库<br/>如果在DLL中使用了TADOConnection之类的COM组件,或者ActiveX控件,调用时会提示 “标记没有引用存储”等错误,这是因为没有初始化COM。DLL中不会调用CoInitilizeEx,初始化COM库被认为是应用程序的责任,这是Borland的实现策略。<br/>你需要做的是1、引用Activex单元,保证CoInitilizeEx函数被正确调用了<br/>2、在单元级加入初始化和退出代码:<br/>initialization<br/>Coinitialize(nil);<br/>finalization<br/>CoUninitialize;<br/>end.<br/>3、 在结束时记住将连接和数据集关闭,否则也会报地址错误。<br/><br/>引出DLL中的对象<br/>从DLL窗体的例子中可以发现,将句柄做为参数传递给DLL,DLL能指向这个句柄的实例。同样的道理,从DLL中引出对象,基本思路是通过函数返回DLL中对象的指针,将该指针赋值到宿主程序的变量,使该变量指向内存中某对象的地址。对该变量的操作即对DLL中的对象的操作。<br/>本文不再详解代码,仅说明需要注意的几点规则:<br/>应用程序只能访问对象中的虚拟方法,所以要引用的对象方法必须声明为虚方法;<br/>DLL和应用程序中都需要相同的对象及方法定义,且方法定义顺序必须一致;<br/>DLL中的对象无法继承;<br/>对象实例只能在DLL中创建。<br/>声明虚方法的目的不是为了重载,而是为了将该方法加入虚拟方法表中。对象的方法与普通例程是不同的,这样做才能让应用程序得到方法的指针。<br/><br/>DLL毕竟是结构化编程时代的产物,基于函数级的代码共享,实现对象化已经力不从心。现在类似DLL功能,但对对象提供强大支持的新方式已经得到普遍应用,象接口(COM/DCOM/COM+)之类的技术。进程内的服务端程序从外表看就是一个dll文件,但它不通过外部例程引出应用,而是通过注册发布一系列接口来提供支持。它与DLL从使用上有两个较大区别:需要注册,通过创建接口对象调用服务。可以看出,DLL虽然通过一些技巧也可以引出对象,但是使用不便,而且常常将对象化强制转为过程化的方式,这种情况下最好考虑新的实现方法。<br/><br/>注:本文代码在Delphi6、7中调试通过。<br/>附:本文参考了“Delphi5开发人员指南”等书及资料。</div></div>
<div class="comment_list">
<div class="comment_list_caption">
<div class="box_l"><a class="author" href="http://wghai.com/space.php?uid=2643534">梦回千秋</a> (2009-3-03 12:41:36)</div></div>
<div class="comment_content">Delphi中initialization和finalization2008-10-25 16:01首先说明initialization 和 finalization 在代码单元中的位置:<br/>************************************************************<br/>unit Unit1; <br/>interface<br/><br/>uses<br/>Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,<br/>Dialogs;<br/><br/>type<br/>TForm1 = class(TForm)<br/>Button1: TButton;<br/>procedure FormCreate(Sender: TObject);<br/>procedure FormDestroy(Sender: TObject);<br/>private<br/>{ Private declarations }<br/>public<br/>{ Public declarations }<br/>end;<br/><br/>var<br/>Form1: TForm1;<br/><br/>implementation<br/><br/>uses Unit2;<br/><br/>{$R *.dfm}<br/><br/>procedure TForm1.FormCreate(Sender: TObject);<br/>begin<br/>ShowMessage('1 FormCreate');<br/>end;<br/><br/>procedure TForm1.FormDestroy(Sender: TObject);<br/>begin<br/>ShowMessage('1 Destroy');<br/>end;<br/><br/>initialization<br/>ShowMessage('1 ini');{单元初始化代码}<br/>finalization<br/>ShowMessage('1 final');{单元退出时的代码}<br/><br/>end.<br/>************************************************************<br/>initialization —— 在单元中放在文件结尾前,包含用来初始化单元的代码,它在主程序运行前运行并且只运行一次。<br/>finalization —— 在单元中放在 initialization 和 end. 之间,包含了单元退出时的代码。在程序退出时运行并且只运行一次。<br/>OnClose —— 在点击关闭按钮或执行该事件时调用。<br/>OnCreate —— 在窗体创建时调用。(我的感觉:该事件是在窗体创建之后,即执行构造函数后才执行的)<br/>OnDestroy —— 在窗体销毁时调用,一般用来销毁程序员自己定义(自己定义、创建,而不是直接从控件板拖拉过来的控件)的对象、变量、指针等等。(与析构函数的关系我还没弄清)<br/><br/>下面是执行顺序(如果程序中没有 initialization、 OnCreate、 OnClose、 OnDestroy、 finalization 中的任何一项,则相应跳过即可):<br/>程序启动 --&gt; 执行 initialization 处的代码 --&gt; 执行窗体的构造函数并创建相应窗体和单元中的对象(如拖拉的控件对象,全局变量等) --&gt; 执行 OnCreate 事件 --&gt; 程序运行 --&gt; 关闭主窗体(此处指调用主窗体的Close或点击主窗体的关闭按钮) --&gt; 执行 OnClose 事件 --&gt; 执行 OnDestroy 事件 --&gt; 执行 finalization 处的代码<br/><br/>以上是对于单个窗体而言,下面是对于多个窗体:<br/>新建一工程,创建3个Form,分别是form1、form2、form3,默认情况下form1为主窗体。<br/>&lt;1 begin&gt;<br/>程序启动 --&gt; 执行 initialization 处的代码(先 form1 然后 form2 最后 form3 ) --&gt; 执行窗体的构造函数并创建相应窗体和单元中的对象(如拖拉的控件对象,全局变量等) --&gt; 执行 OnCreate 事件(先 form1 然后 form2 最后 form3 ) --&gt; 程序运行 --&gt; 关闭主窗体(此处指调用主窗体的Close或点击主窗体的关闭按钮) --&gt; 执行主窗体的 OnClose 事件 --&gt; 执行 OnDestroy 事件(先 form3 然后 form2 最后 form1 ,注意哟,顺序倒过来了。) --&gt; 执行 finalization 处的代码(先 form3 然后 form2 最后 form1 ,注意哟,顺序倒过来了。)<br/>&lt;1 end&gt;<br/>为什么 OnDestroy 和 OnCreate 的顺序相反?为什么 finalization和 initialization 的顺序相反?大家好好想想吧!<br/><br/><br/>另外:如果我在form1中uses 一下Unit2(form2的代码单元)执行顺序如下:<br/>&lt;2 begin&gt;<br/>程序启动 --&gt; 执行 initialization 处的代码(先 form2 然后 form1 最后 form3 ) --&gt; 执行窗体的构造函数并创建相应窗体和单元中的对象(如拖拉的控件对象,全局变量等) --&gt; 执行 OnCreate 事件(先 form1 然后 form2 最后 form3 ) --&gt; 程序运行 --&gt; 关闭主窗体(此处指调用主窗体的Close或点击主窗体的关闭按钮) --&gt; 执行主窗体的 OnClose 事件 --&gt; 执行 OnDestroy 事件(先 form3 然后 form1 最后 form2 ,注意注意,不一样了。) --&gt; 执行 finalization 处的代码(先 form3 然后 form1 最后 form2 ,注意注意,不一样了。)<br/>&lt;2 end&gt;<br/><br/><br/>多个窗体(单元)一些细节性的东西:<br/>1)initialization<br/>initialization 的执行顺序取决于哪个窗体先被调用,对于&lt;1&gt;,由于3个窗体之间没有 uses 和被 uses 的关系,所以按照工程单元uses 中的顺序执行(在工程单元中,如下):<br/>************************************************************<br/>program Project1;<br/><br/>uses<br/>Forms,<br/>Unit1 in 'Unit1.pas' {Form1},<br/>Unit2 in 'Unit2.pas' {Form2},<br/>Unit3 in 'Unit3.pas' {Form3};<br/><br/>{$R *.res}<br/><br/>begin<br/>Application.Initialize;<br/>Application.CreateForm(TForm1, Form1);<br/>Application.CreateForm(TForm2, Form2);<br/>Application.CreateForm(TForm3, Form3);<br/>Application.Run;<br/>end.<br/>************************************************************<br/>如果改变<br/>uses<br/>Forms,<br/>Unit1 in 'Unit1.pas' {Form1},<br/>Unit2 in 'Unit2.pas' {Form2},<br/>Unit3 in 'Unit3.pas' {Form3};<br/><br/>成为<br/>uses<br/>Forms,<br/>Unit2 in 'Unit2.pas' {Form2},<br/>Unit3 in 'Unit3.pas' {Form3},<br/>Unit1 in 'Unit1.pas' {Form1};<br/>那么就是(先form2后form3最后form1)<br/>对于&lt;2&gt;,由于form1中uses了Unit2,那么在编译Unit1之前必须先编译Unit2。顺序自然是(先 form2 然后 form1 最后 form3)<br/>所以 initialization 的执行顺序首先看彼此单元之间是否有uses和被uses关系(简单的说就是:调用关系), 然后再看工程文件的uses的顺序。(呵呵,不要被uses弄混了!)<br/><br/>2)OnCreate<br/>OnCreate 的执行顺序由工程单元中的如下类似代码决定<br/>************************************************************<br/>Application.CreateForm(TForm1, Form1);<br/>Application.CreateForm(TForm2, Form2);<br/>Application.CreateForm(TForm3, Form3);<br/>************************************************************<br/>改变之后则相应发生改变。<br/><br/>3)OnDestroy<br/>与OnCreate的顺序相反。(相反时一种对应,创建的顺序与销毁的顺序是相反的,这样是为了避免发生错误,这样也体现了编程要求思维严谨。)<br/><br/>4)finalization<br/>与 initialization 的顺序相反。<br/><br/>再说说OnClose,<br/>当调用窗体的Close或点击关闭按钮时调用该事件。<br/>主窗体Close后,程序就退出。<br/>非主窗体Close,仅仅是Close本窗体。(别说你在OnClose中调用其他窗体的关闭,那些不再讨论范围)<br/><br/>直接用Application.Terminate强制退出程序时,不调用OnClose事件。但 OnDestroy 和 finalization 要执行。</div></div></div>
【VB】QQ群:1422505加的请打上VB好友
【易语言】QQ群:9531809  或 177048
【FOXPRO】QQ群:6580324  或 33659603
【C/C++/VC】QQ群:3777552
【NiceBasic】QQ群:3703755

1214

主题

352

回帖

11

精华

管理员

菜鸟

积分
93755

贡献奖关注奖人气王精英奖乐于助人勋章

 楼主| 发表于 2009-7-26 13:02:52 | 显示全部楼层

Delphi中高级DLL的编写和调用(2)

{---------- DLL中的单元 Unit2.PAS ----------} <br/><br/>  unit Unit2; <br/><br/>  interface <br/><br/>  uses <br/>   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, <br/>   Db, ADODB, StdCtrls, Menus; <br/><br/>  type <br/>   TForm1 = class(TForm) <br/>   ADOConnection1: TADOConnection;{ 本地数据库连接 } <br/>   Memo1: TMemo; { 用于显示信息 } <br/>   private <br/>   public <br/>   end; <br/><br/>  { 该过程向外提供 } <br/>  procedure DoTest(H: THandle; { 获得调用者的句柄 } <br/>   AConn: TADOConnection;{ 获得调用者的数据库连接 } <br/>   S: string; { 获得一些文本信息 } <br/>   N: Integer); { 获得一些数值信息 } <br/>   cdecl; { 指定调用协议 }&nbsp;&nbsp;<br/><br/>  implementation <br/><br/>  {$R *.DFM} <br/><br/>  procedure DoTest(H: THandle; AConn: TADOConnection; S: string; N: Integer); <br/>  begin <br/>   Application.Handle := H; { 将过程的句柄赋值为调用者的句柄 } <br/>   { 上面语句的作用在于, DLL的句柄和调用者的句柄相同,在任务栏中就不会 } <br/>   { 各自出现一个任务标题了。 } <br/>   with TForm1.Create(Application) do try{ 创建窗体 } <br/>   Memo1.Lines.Append(‘成功调用‘); { 显示一行信息 } <br/>   ADOConnection1 := AConn; { 获得数据库连接的实例 } <br/>   Memo1.Lines.Append( <br/>   ADOConnection1.ConnectionString + <br/>   ‘ - ‘ + S + ‘ - ‘ + IntToStr(N)); { 根据得到的参数显示另一行信息 } <br/>   ShowModal; { 模式化显示窗体 } <br/>   finally <br/>   Free; { 调用结束时销毁窗口 } <br/>   end; <br/>  end; <br/><br/>  end. <br/><br/><br/>  {---------- 调用者 Project1.DPR,很普通的工程文件 ----------} <br/><br/>  program Project1; <br/><br/>  uses&nbsp;&nbsp;<br/>Forms, <br/>   Unit1 in ‘Unit1.pas‘ {Form1}; <br/><br/>  {$R *.RES} <br/><br/>  begin <br/>   Application.Initialize; <br/>   Application.CreateForm(TForm1, Form1); <br/>   Application.Run; <br/>  end. <br/><br/><br/>  {---------- 调用者单元Unit1.PAS ----------} <br/><br/>  unit Unit1; <br/><br/>  interface <br/><br/>  uses <br/>   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, <br/>   StdCtrls, Db, ADODB; <br/><br/>  type <br/>   TForm1 = class(TForm) <br/>   Button1: TButton; { 按此按钮进行调用 } <br/>   ADOConnection1: TADOConnection; { 本地数据库连接,将传递给 DLL } <br/>   procedure Button1Click(Sender: TObject);{ 调用 DLL} <br/>   private <br/>   public <br/>   end; <br/><br/>  var <br/>   Form1: TForm1; <br/><br/>  implementation <br/><br/>  {$R *.DFM} <br/><br/>  { 外部声明必须和 DLL中的参数列表一致,否则会运行时错误 } <br/>  procedure DoTest(H: THandle; { 传递句柄 } <br/>   AConn: TADOConnection; { 传递数据库连接 } <br/>   S: string; { 传递文本信息 } <br/>   N: Integer); { 传递数值信息 } <br/>   cdecl; { 指定调用协议 } <br/>   external ‘Project2.dll‘;{ 指定过程来源 } <br/><br/>  { 调用过程 } <br/>  procedure TForm1.Button1Click(Sender: TObject); <br/>  begin <br/>   DoTest(Application.Handle, <br/>   ADOConnection1, <br/>   ‘Call OK‘, <br/>   256); <br/>  end; <br/><br/>  end. <br/>
【VB】QQ群:1422505加的请打上VB好友
【易语言】QQ群:9531809  或 177048
【FOXPRO】QQ群:6580324  或 33659603
【C/C++/VC】QQ群:3777552
【NiceBasic】QQ群:3703755

30

主题

693

回帖

0

精华

钻石会员

积分
2815
发表于 2015-5-30 05:11:46 | 显示全部楼层
看这么长的代码 就知道楼主讲的够深入了
您需要登录后才可以回帖 登录 | 加入我们

本版积分规则

快速回复 返回顶部 返回列表