• Delphi深入DLL编程(2) (转载)


    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位,最后才能正确的执行并返回值!
    以上16位DLL的部分在小仓系列中曾经提到过
    Delphi开发DLL常见问题
    字符串参数
    前面曾提到过,为了保证DLL参数/返回值传递的正确性,尤其是为C++等其他语言开发的宿主程序使用时,应尽量使用指针或基本类型,因为其他语言与Delphi的变量存储分配方法可能是不一样的。C++中字符才是基本类型,串则是字符型的线形链表。所以最好将string强制转换为Pchar。
    如果DLL和宿主程序都用Delphi开发,且使用string(还有动态数组,它们的数据结构类似)作为导出例程的参数/返回值,那么添加ShareMem为工程文件uses语句的第一个引用单元。ShareMem是Borland共享的内存管理器Borlndmm.dll的接口单元。引用该单元的DLL的发布需要包括Borlndmm.dll,否则就得避免使用string。
    初始化COM库
    如果在DLL中使用了TADOConnection之类的COM组件,或者ActiveX控件,调用时会提示 “标记没有引用存储”等错误,这是因为没有初始化COM。DLL中不会调用CoInitilizeEx,初始化COM库被认为是应用程序的责任,这是Borland的实现策略。
    你需要做的是1、引用Activex单元,保证CoInitilizeEx函数被正确调用了
    2、在单元级加入初始化和退出代码:
    initialization
    Coinitialize(nil);
    finalization
    CoUninitialize;
    end.
    3、 在结束时记住将连接和数据集关闭,否则也会报地址错误。
    在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;
    function CreateAndShowForm(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';
    function CreateAndShowForm(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。
    引出DLL中的对象
    从DLL窗体的例子中可以发现,将句柄做为参数传递给DLL,DLL能指向这个句柄的实例。同样的道理,从DLL中引出对象,基本思路是通过函数返回DLL中对象的指针,将该指针赋值到宿主程序的变量,使该变量指向内存中某对象的地址。对该变量的操作即对DLL中的对象的操作。
    本文不再详解代码,仅说明需要注意的几点规则:
    1、 应用程序只能访问对象中的虚拟方法,所以要引用的对象方法必须声明为虚方法;
    2、 DLL和应用程序中都需要相同的对象及方法定义,且方法定义顺序必须一致;
    3、 DLL中的对象无法继承;
    4、 对象实例只能在DLL中创建。
    声明虚方法的目的不是为了重载,而是为了将该方法加入虚拟方法表中。对象的方法与普通例程是不同的,这样做才能让应用程序得到方法的指针。
    DLL毕竟是结构化编程时代的产物,基于函数级的代码共享,实现对象化已经力不从心。现在类似DLL功能,但对对象提供强大支持的新方式已经得到普遍应用,象接口(COM/DCOM/COM+)之类的技术。进程内的服务端程序从外表看就是一个dll文件,但它不通过外部例程引出应用,而是通过注册发布一系列接口来提供支持。它与DLL从使用上有两个较大区别:需要注册,通过创建接口对象调用服务。可以看出,DLL虽然通过一些技巧也可以引出对象,但是使用不便,而且常常将对象化强制转为过程化的方式,这种情况下最好考虑新的实现方法。
  • 相关阅读:
    【带着canvas去流浪(14)】Three.js中凹浮雕模型的生成方式
    Stanford公开课《编译原理》学习笔记(1~4课)
    Vue源码中compiler部分逻辑梳理(内有彩蛋)
    Vue+ElementUI项目使用webpack输出MPA
    Vue-Router中History模式
    Vue中拆分视图层代码的5点建议
    如何正确使用Java泛型
    ZooKeeper的三种典型应用场景
    Tomcat多实例部署
    Tomcat常用的过滤器
  • 原文地址:https://www.cnblogs.com/ghd2004/p/1340711.html
Copyright © 2020-2023  润新知