Delphi下的DLL编程Word文件下载.docx
《Delphi下的DLL编程Word文件下载.docx》由会员分享,可在线阅读,更多相关《Delphi下的DLL编程Word文件下载.docx(12页珍藏版)》请在冰豆网上搜索。
![Delphi下的DLL编程Word文件下载.docx](https://file1.bdocx.com/fileroot1/2022-12/7/d74b110b-ceea-4344-a205-c4990126d52a/d74b110b-ceea-4344-a205-c4990126d52a1.gif)
在进行后面的讲解之前,我想大家应该先清楚一个概念:
例程声明的是一个指针变量,调用函数/过程,其实是通过指针转入该函数/过程的执行代码。
我们先尝试用Delphi来建立一个自己的DLL文件。
这个DLL包含一个标准的目录删除(包含子目录及文件)函数。
建立DLL
通过Delphi建立一个DLL是很容易的。
New一个新Project,选择DLLWizard,然后会生成一个非常简单的单元。
该单元不象一般的工程文件以program开始,而是以library开始的。
该工程单元缺省引用了SysUtils、Classes两个单元。
可以直接在该单元的uses之后,begin…end部分之前添加函数/过程代码,也可以在工程中添加包含代码的单元,然后该单元将会被自动uses。
接下来是编写DLL例程的代码。
如果是引用单元里的例程,需要通过声明时添加export后缀引出。
假如是直接写在library单元中的,则不必再写export了。
最后一步是在library单元的begin语句之上,uses部分及函数定义之下添加exports部分,并列举需要引出的例程名称。
注意仅仅是名称,不包含procedure或function关键字,也不需要参数、返回值和后缀。
exports语句后的语法有三种形式(例程指具体的函数/过程):
exports例程名;
exports例程名
index
索引值;
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工程单元:
libraryFileOperate;
uses
SysUtils,
Classes,
uDirectoryin‘uDirectory.pas’;
{$R*.res}
exports
DeleteDir;
begin
end.
函数功能实现单元:
unituDirectory;
interface
Classes,SysUtils;
functionDeleteDir(DirName:
Pchar):
boolean;
export;
stdcall;
implementation
functionDeleteDir(DirName:
var
FindFile:
TSearchRec;
s:
string;
=DirName;
ifcopy(s,length(s),1)<
>
‘\’thens:
=s+‘\’;
ifDirectoryExists(s)thenbegin
ifFindFirst(s+‘*.*’,faAnyFile,FindFile)=0thenbegin
repeat
ifFindFile.Attr<
faDirectorythenbegin
//文件则删除
DeleteFile(s+FindFile.Name);
end
elsebegin
//目录则嵌套自身
if(FindFile.Name<
‘.’)and(FindFile.Name<
‘..’)then
DeleteDir(Pchar(s+FindFile.Name));
end;
untilFindNext(FindFile)<
0;
FindCLose(FindFile);
Result:
=RemoveDir(s);
end;
初始化及释放资源
Delphi中初始化有几种方法。
一种是利用Unit的Initalization与Finalization这两个小节(不知道“单元小节”?
你该先去恶补Delphi语法了)进行该单元中变量的初始化工作。
注意,DLL虽然在内存中只有一个副本,但是例程隶属于调用者的不同进程空间。
如果想初始化公共变量来达到多进程共享是不可行的,同时也要注意公共变量带来的冲突问题。
二是在library单元的begin…end部分进行DLL的初始化。
假如想在DLL结束时有对应代码,则可以利用DLL自动创建的一个ExitProc过程变量,这是一个退出过程的指针。
建立一个自己的过程,并将该过程的地址赋与ExitProc。
因为ExitProc是DLL创建时就存在的,所以在begin…end部分就应该进行此步操作。
同时建立一个临时指针变量保存最初的ExitProc值,在自己的退出过程中将ExitProc值赋回来。
这样做是为了进行自己的退出操作后,能完成缺省的DLL退出操作(与在重载的Destory方法中inherated的意义是一样的,完成了自己的destory,还需要进行缺省的父类destory才完整)。
示例如下:
library
MyDLL;
OldExitProc:
pointer;
//公共变量,为的保存最初的ExitProc指针以便赋回
procedure
MyExitProc;
//对应初始化的结束代码
ExitProc
:
=
OldExitProc;
//自己的退出过程中要记住将ExitProc赋回
//初始化代码
OldExitProc
ExitProc;
ExitProc
@MyExitProc;
end.
第三种方法和ExitProc类似,在System单元中预定义了一个指针变量DllProc(该方法需要引用Windows单元)。
在使用DLLProc时,
必须先写好一个具有以下原型的程序:
procedure
DLLHandler(dwReason:
DWORD);
stdcall;
并在library的begin..end.之间,
将这个DLLHandler程序的执行地址赋给DLLProc中,
这时就可以根据参数Reason的值分别作出相应的处理。
示例如下:
library
…
MyDLLHandler(dwReason:
begin
case
dwReasonof
DLL_Process_Attach:
//进程进入时
DLL_Process_Detach:
//进程退出时
DLL_Thread_Attach:
//线程进入时
DLL_Thread_Detach:
//线程退出时
end;
end;
…
DLLProc
@MyDLLHandler;
MyDLLHandle(DLL_Process_Attach);
end.
可见,通过DLLProc
在处理多进程时比ExitProc更加强大和灵活。
静态(隐式)调用DLL
DLL已经有了,接下来我们看如何调用并调试它。
普通的DLL是不需要注册的,但是要包含在windows搜索路径中才能被找到。
搜索路径的顺序是:
当前目录;
Path路径;
windows目录;
widows系统目录(system、system32)。
引入DLL例程的声明方法
在需要使用外部例程(DLL函数/过程)的代码之前预定义该函数/过程。
即按DLL中的定义原样声明,且仅需要声明。
同时加上external后缀引入,与export引出相对应。
根据exports的三种索引语法,也有三种确定例程的方式(以函数声明为例):
function函数名(参数表):
返回值;
external’DLL文件名’;
external’DLL文件名’index索引号;
external’DLL文件名’name新名称;
如果不确定例程名称,可以用索引方式引入。
如果按原名引入会发生冲突,则可以用“新名称”引入。
进行声明后DLL函数的使用就和一般函数相同了。
静态调用方式简单,但在启动调用程序时即调入DLL作为备用过程。
如果此DLL文件不存在,那么启动时即会提示错误并立刻终止程序,不管定义是否使用。
快速查看DLL例程定义可以使用Borland附带的工具tdump.exe(在Delphi或BCB的bin目录下),示例如下:
Tdumpc:
\windows\system\user32.dll>
user32.txt
然后打开user32.txt文件,找到ExportsfromUSER32.dll行,之下的部分就是DLL例程定义了,比如:
RVA
Ord.HintName
——–—-—-—-
00001371
10000ActivateKeyboardLayout
00005C20
20001AdjustWindowRect
0000161B
30002AdjustWindowRectEx
Name列就是例程的名称,Ord就是该例程索引号。
注意,该工具是不能得到例程的参数表的。
如果参数错误,调用DLL例程会引起堆栈错误而导致调用程序崩溃。
调用代码
建立一个普通工程,在Main窗体上放置一个TShellTreeView控件(Samples页),再放置一个按钮,添加代码如下:
external‘FileOperate.dll’;
procedureTForm1.Button1Click(Sender:
TObject);
ifDirectoryExists(ShellTreeView.Path)then
ifApplication.MessageBox(Pchar(‘确定删除目录’+QuotedStr(ShellTreeView.Path)+’吗?
’),‘Information’,MB_YESNO)=IDYesthen
ifDeleteDir(PChar(ShellTreeView.Path))then
showmessage(‘删除成功’);
该范例调用的就是前面建立的DLL。
注意,声明时要包括stdcall后缀,这样才能保证调用Delphi开发的DLL的例程中类似PChar这样的参数值传递正确。
大家有兴趣可以试验一下,不加入stdcall或者safecall后缀执行上面代码,将不能保证成功传递字符串参数给DLL函数。
调试方法
在Delphi主菜单Run项目中选择Parameters,打开“RunParameters”对话框。
在HostApplication中填入一个宿主程序(该程序调用了将要调试的DLL),还可以在Parameters中输入参数。
保存内容,然后就可以在DLL工程中设置断点、跟踪/单步执行了。
Run该DLL工程,然后将运行宿主程序。
执行会调用DLL的操作,然后就能跟踪进入该DLL的代码,接下来的调试操作和普通程序是一样的。
因为操作系统或其他软件影响的原因,可能会出现进行了上述步骤仍然无法正常跟踪/中断DLL代码的情况。
这时可以试试在菜单Project
|Options
对话框的
Linker
页面里将
EXE
andDLLOptions
中的Include
TD32
debug
info及include
remote
symbols两个选项选中。
假如还是不能中断-_____-|||那只好另外建立一个引用执行代码单元的应用程序,写代码调用例程调试完成后再编译DLL了(其实该方法有时候蛮方便的,但有时候亦非常麻烦)。
引入文件
DLL比较复杂时,可以为它的声明专门创建一个引入单元,这会使该DLL变得更加容易维护和查看。
引入单元的格式如下:
unit
MyDllImport;
{Import
unit
for
MyDll.dll}
interface
procedure
MyDllProc;
…
implementation
procedure
external
’MyDll’
1;
…
这样以后想要使用MyDll中的例程时,只要简单的在程序模块中的uses子句中加上MyDllImport即可。
其实这仅仅是种方便开发的技巧,大家打开Windows等引入windowsAPI的单元,可以看到类似的做法。
动态(显式)调用DLL
前面讲述静态调用DLL时提到,DLL会在启动调用程序时即被调入。
所以这样的做法只能起到公用DLL以及减小运行文件大小的作用,而且DLL装载出错会立刻导致整个启动过程终止,哪怕该DLL在运行中只起到微不足道的作用。
使用动态调用DLL的方式,仅在调用外部例程时才将DLL装载内存(引用记数为0时自动将该DLL从内存中清除),从而节约了内存空间。
而且可以判断装载是否正确以避免调用程序崩溃的情况,最多损失该例程功能而已。
动态调用虽然有上述优点,但是对于频繁使用的例程,因DLL的调入和释放会有额外的性能损耗,所以这样的例程则适合使用静态引入。
调用范例
DLL动态调用的原理是首先声明一个函数/过程类型并创建一个指针变量。
为了保证该指针与外部例程指针一致以确保赋值正确,函数/过程的声明必须和外部例程的原始声明兼容(兼容的意思是1、参数名称可以不一样;
2、参数/返回值类型至少保持可以相互赋值,比如原始类型声明为Word,新的声明可以为Integer,假如传递的实参总是在Word的范围内,就不会出错)。
接下来通过windowsAPI函数LoadLibrary引入指定的库文件,LoadLibrary的参数是DLL文件名,返回一个THandle。
如果该步骤成功,再通过另一个API函数GetProcAddress获得例程的入口地址,参数分别为LoadLibrary的指针和例程名,最终返回例程的入口指针。
将该指针赋值给我们预先定义好的函数/过程指针,然后就可以使用这个函数/过程了。
记住最后还要使用API函数FreeLibrary来减少DLL引用记数,以保证DLL使用结束后可以清除出内存。
这三个API函数的Delphi声明如下:
FunctionLoadLibrary(LibFileName:
PChar):
THandle;
FunctionGetProcAddress(Module:
ProcName:
TfarProc;
ProcedureFreeLibrary(LibModule:
THandle);
将前面静态调用DLL例程的代码更改为动态调用,如下所示:
type
TDllProc=function(PathName:
LibHandle:
THandle;
DelPath
:
TDllProc;
LibHandle:
=LoadLibrary(PChar(‘FileOperate.dll’));
ifLibHandle>
=32thenbegin
try
DelPath:
=GetProcAddress(LibHandle,PChar(‘DeleteDir’));
ifDelPath(PChar(ShellTreeView.Path))then
finally
FreeLibrary(LibHandle);
16位DLL的动态调入
下面将演示一个16位DLL例程调用的例子,该例程是windows9x中的一个隐藏API函数。
代码混合了静态、动态调用两种方式,除了进一步熟悉外,还可以看到调用16位DLL的解决方法。
先解释一下问题所在:
我要实现的功能是获得win9x的“系统资源”。
在winNT/2000下是没有“系统资源”这个概念的,因为winNT/2000中堆栈和句柄不再象win9X那样被限制在64K大小。
为了取该值,可以使用win9x的userdll中一个隐藏的API函数GetFreeSystemResources。
该DLL例程必须动态引入。
如果静态声明的话,在win2000里执行就会立即出错。
这个兼容性不解决是不行的。
所以必须先判断系统版本,如果是win9x再动态加载。
检查操作系统版本的代码是:
OSversion
_OSVERSIONINFOA;
FWinVerIs9x:
Boolean;
OSversion.dwOSVersionInfoSize :
= sizeof(_OSVERSIONINFOA);
GetVersionEx(OSversion);
FWinVerIs9x :
= OSversion.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS;
End;
以上直接调用API函数,已在Windows单元中被声明。
functionLoadLibrary16(LibraryName:
PChar):
externalkernel32index35;
procedureFreeLibrary16(HInstance:
THandle);
externalkernel32index36;
functionGetProcAddress16(Hinstance:
ProcName:
Pointer;
externalkernel32index37;
functionTWinResMonitor.GetFr