zoukankan      html  css  js  c++  java
  • 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虽然通过一些技巧也可以引出对象,但是使用不便,而且常常将对象化强制转为过程化的方式,这种情况下最好考虑新的实现方法。
  • 相关阅读:
    Atitit s2018.6 s6 doc list on com pc.docx Atitit s2018.6 s6 doc list on com pc.docx  Aitit algo fix 算法系列补充.docx Atiitt 兼容性提示的艺术 attilax总结.docx Atitit 应用程序容器化总结 v2 s66.docx Atitit file cms api
    Atitit s2018.5 s5 doc list on com pc.docx  v2
    Atitit s2018.5 s5 doc list on com pc.docx  Acc 112237553.docx Acc baidu netdisk.docx Acc csdn 18821766710 attilax main num.docx Atiitt put post 工具 开发工具dev tool test.docx Atiitt 腾讯图像分类相册管家.docx
    Atitit s2018 s4 doc list dvchomepc dvccompc.docx .docx s2018 s4 doc compc dtS44 s2018 s4 doc dvcCompc dtS420 s2018 s4f doc homepc s2018 s4 doc compc dtS44(5 封私信 _ 44 条消息)WebSocket 有没有可能取代 AJAX
    Atitit s2018 s3 doc list alldvc.docx .docx s2018 s3f doc compc s2018 s3f doc homepc sum doc dvcCompc dtS312 s2018 s3f doc compcAtitit PathUtil 工具新特性新版本 v8 s312.docx s2018 s3f doc compcAtitit 操作日
    Atitit s2018.2 s2 doc list on home ntpc.docx  Atiitt uke制度体系 法律 法规 规章 条例 国王诏书.docx Atiitt 手写文字识别 讯飞科大 语音云.docx Atitit 代码托管与虚拟主机.docx Atitit 企业文化 每日心灵 鸡汤 值班 发布.docx Atitit 几大研发体系对比 Stage-Gat
    Atitit 文员招募规范 attilax总结
    Atitit r2017 r6 doc list on home ntpc.docx
    atitit r9 doc on home ntpc .docx
    Atitit.如何文章写好 论文 文章 如何写好论文 技术博客 v4
  • 原文地址:https://www.cnblogs.com/ghd2004/p/1340711.html
Copyright © 2011-2022 走看看