一. DLL和系统变量 在 System 单元声明的变量中,有几个对DLL编程有特殊影响。IsLibrary 可以检测代码是执行在应用程序中还是执行在DLL中,在应用程序中 IsLibrary 总是为 False ,在 DLL中总是为 True 。在 DLL的整个生命周期中,HInstance 包含了库的实例句柄。在DLL中,系统变量 CmdLine 总是为 nil 。 DLLProc 变量允许DLL监视操作系统对 DLL的入口点的调用。这一特性通常只用于那些支持多线程的DLL。关于入口点函数的详细资料参见DLL的入口点函数一节。
二. DLL的入口点函数 与应用程序一样,每个DLL必须有一个入口点。无论进程或线程在何时装载或卸载DLL,操作系统都调用入口点函数。如果将DLL连接到一个库中,它就可以提供一个入口点函数,并且允许提供 各自的初始化函数。
1.定义入口点函数 在 Delphi 中,全局变量 DLLProc 是一个过程的指针,该指针指定入口/出口函数。该变量初始值为nil,可以通过将函数指针赋值给该变量来指定DLL的入口/ 出口函数,赋值应该在DLL项目文件的 begin..end 之间完成。入口点函数需要一个 DWord 类型的参数。 要监视操作系统调用,需要创建一个回调函数,该函数接受一个整数参数,例如:
procedure DLLHandler(Reason:Integer);
并把该过程的地址赋给 DLLProc 变量。
2.入口点函数返回值 当一个DLL入口点函数因为进程装载被调用时,函数返回 TRUE 表示调用成功。对使用静态链接的进程,返回值为FALSE将引起进程初始化失败并且进程终止。对使用动态调用的进程,返回值为FALSE引起 LoadLibrary 返回 NULL,表示调用失败。当其他原因调用入口点函数时,返回值被抛弃。 如果系统找不到DLL或者入口点函数返回 False,LoadLibrary 返回 NULL 。如果 LoadLibrary 执行成功,它返回一个DLL模块的句柄。进程可以使用这个句柄在调用 GetProcAddress、FreeLibrary 识别DLL。
3.入口点函数的调用 当过程被调用时,传递到回调函数的参数的可取值,实际上也是引起调用入口点函数的事件,如表 1 所示。
参数取值 |
说明 |
DLL_PROCESS_ATTCH |
一个进程调用DLL。对于使用静态调用的进程,DLL在进程初始化期间调用。对于使用动态调用的进程,DLL在 LoadLibrary 或 LoadLibraryEx 返回之前调用
|
DLL_PROCESS_DETACHDLL |
一个进程卸载DLL。当进程终止或调用 FreeLibrary 函数并且引用计数为 0 时,DLL卸载。如果是由于调用 TerminateProcess 或 TerminateThread 函数的终止进程,系统不调用DLL入口点函数 |
DLL_THREAD_ATTACH |
进程创建了一个新线程 。这时,系统会调用所有和这个进程相关联的DLL入口函数。这个调用在新线程的上下文中进行,可以使用 DisableThreadLibraryCalls 函数禁止线程创建时发出通知 |
DLL_THREAD_DETACH |
一个线程卸载DLL。当一个进程卸载DLL时,入口点函数只完整地调用过程一次,而不是进程的每个线程都调用一次。可以使用 DisableThreadLibraryCalls 函数禁止线程终止时发出通知 |
在过程(即回调函数)的主体中,可以根据传递到过程的是哪一个参数来指定相应的动作。
系统在调用函数的进程或线程的上下文中调用入口点函数。这允许DLL使用自己的入口点函数在调用进程的虚拟地址空间分配内存或打开访问进程的句柄。并且如果一个进程已经使用了LoadLibrary调用 DLL,但没有调用 FreeLibrary 函数释放该DLL,则入口点函数不会被该进程再次调用。
三. DLL和内存管理
调用 DLL的每个进程将它映射到自己的虚拟地址空间。在进程将DLL装载到它的虚拟地址空间之后,它就可以调用导出的DLL函数。 系统维护每个DLL的引用计数。当一个线程调用 DLL,它的引用计数加 1。当进程终止时,或引用计数为 0 时(只由动态调用),DLL从虚拟地址空间卸载。 与其他所有函数一样,一个导出的DLL函数在调用它的线程上下文中运行。因此,必须满足下列条件。 • 调用 DLL的进程的线程可以通过DLL函数使用句柄。相似地,调用DLL的进程的任何线程打开的句柄都能在DLL函数内使用。 • DLL使用调用线程的堆栈和调用进程的虚拟地址空间。 • DLL从调用进程的虚拟地址空间分配内存。
在 Windows中,如果DLL输出的函数和过程把长字符串(String 类型)或动态数组作为参数传递或者作为函数结果(不管直接存在还是嵌套于记录或对象中),那么 DLL及其应用程序(或 DLL)都必需使用 ShareMem 单元。同样,如果一个应用程序或DLL使用 New 或 GetMem 函数分配内存,而分配的内存被另一个模块中的 Dispose 或 FreeMem 函数释放,那么它们也必须都使用 ShareMem 单元。如果一个 uses 子句中需要出现 ShareMem 单元,那么该单元总是列于首位。 ShareMem 单元是内存管理模块 BORLANDMM.DLL的接口单元,它允许模块之间共享动态分配BORLANDMM.DLL必须被使用了 ShareMem 单元的应用程序或DLL从内存中销毁。当一个的内存。应用程序或 DLL使用了 ShareMem 时,其内存管理被 BORLANDMM.DLL中的内存管理模块代替。
四. DLL中的数据
基于Win32的DLL可以包含全局或局部数据。 一个DLL可以被几个应用程序同时使用,但每个应用程序的进程空间各自拥有该DLL的一个副本及自身的一套全局变量。如果需要共享内存的多个DLL或一个DLL的多个实例,它们必须使用内存映射文件(Memory-Mapped Files)。 DLL变量的默认作用范围与应用程序中声明的变量是一样的。DLL源代码中的全局变量对使用DLL的进程来说也是全局的。而静态变量的作用范围限制在声明它们的块中。因此默认情况下,每个进程都会有它自己的DLL全局和静态变量的实例。 当一个DLL使用任何内存分配函数(GlobalAlloc、LocalAlloc 、HeapAlloc 和 VirtualAlloc)分配内存时,内存都是分配给调用进程的虚拟地址空间,并且只能被那个进程的线程访问。
五. DLL中的异常和运行时错误
在 DLL中,当一个异常被引发但未被处理时,它将向DLL外传播,即传播到调用者。如果调用该DLL的应用程序或DLL,其自身也是由Delphi编写,那么异常可以通过一般的 try...except 语句被处理。 如果调用DLL的应用程序或DLL是用其他语言编写的,那么异常可以作为操作系统异常被处理,此时的异常代码是$0EEDFACE。在操作系统的异常记录数组 Exception Information 中,第 1 个入口(即字段)包含了异常地址,第二个入口包含了一个对 Delphi 异常对象的引用。 通常,应当在DLL内部处理所有的异常。在 Windows 中,Delphi 异常映射到操作系统(OS)异常模块。 如果一个DLL没有使用 SysUtils 单元,那么它不能支持异常。这时,当错误在DLL中发生时,调用该DLL的应用程序将终止。因为DLL无法获悉调用它的模块是否是 Delphi 程序,所以它不能调用应用程序的退出过程。应用程序只是简单地终止并从内存中删除。
六. 编写DLL函数、过程必须注意的问题
下面总结了一些在编写DLL函数、过程中必须注意的一些问题。 (1)在DLL中编写的函数或过程最好加上 stdcall 调用参数 如果想要让由Delphi编写的DLL对用其他语言编写的程序也可用,那么在输出函数的声明中指定 stdcall 调用约定是最可靠的,因为其他语言可能不支持 ObjectPascal 中默认的 register 调用约定。忘记使用 stdcall 参数是常见的错误,这个错误不会影响DLL的编译和生成,但当调用这个 DLL时会发生很严重的错误,可能导致操作系统的死锁。 (2)当使用了长字符串类型的参数、变量时要引用 ShareMem Delphi 中的 string 类型功能很强大。但是如果在动态链接库中使用 string 类型的参数、变量甚至是记录信息时,就必须引用 ShareMem 单元,而且必须是第 1 个引用,即在 uses 语句后是第 1 个引用的单元。例如:
Uses ShareMem,SysUtils,Classes;
在工程文件(*.dpr)中而不是在单元文件(*.pas)中也要做同样的工作。也可以将 String 类型的参数、变量等声明为 PChar 或 ShortString(如s:string[10])类型。同样的问题会出现在使用了动态数组时,解决的方法是一样的 (3)参数传递 动态链接库中参数类型最好与 Visual C++ 的参数类型一致,不要用Delphi的数据类型。并且最好有返回值(即使是一个过程),来报出调用成功、失败或状态。返回值最好与 Visual C++兼容。 (4)全局变量的使用 在 Widnows 32 位程序中,两个应用程序的地址空间是相互没有联系的。DLL在内存中是一份拷贝,而变量是在各进程的地址空间中,因此不能借助DLL的全局变量来达到两个应用程序间的数据传递,除非使用内存映像文件。 (5)DLL中的运行时错误和处理 同一般的应用程序相比,DLL中运行时错误的处理是很困难的,而造成的后果也更为严重。因此要求程序设计者在编写代码时要有充分、周到的考虑。 如果DLL可以在多线程应用程序中使用,必须保证DLL是“线程安全”的,即只使用支持多线程的库,并且必须保证全局数据的同步访问。 七. 调用DLL
调用一个DLL比写一个DLL要容易一些。有两种方法可用于调用一个储存在DLL中的过程和函数:静态调用和动态调用。 本节首先介绍静态调用方法,稍后将介绍动态调用方法,并就两种方法做一个比较。
1.静态调用 从DLL引入过程或函数最简单的方法是,使用 external 指令将其声明为外部函数和过程。例如:
procedure DoSomething;external ’MYLIB.DLL’;
如果在程序中包括了上面的声明,那么程序启动时 MYLIB.DLL将被加载一次。在程序执行的全过程中,标识符 DoSomething 总是指向相同的入口点。 引入的函数和过程声明可以直接放置在其被调用的程序或单元中。不过,为了减少维护的工作量,可以将外部(External )声明收集在一个单独的引入单元中,该单元还可以包括对库请求接口时的所有常量和类型。如此一来,使用了引入单元的其他模块就可以调用在该单元中声明的任何函数和过程。 当系统启动静态调用的程序时,它使用文件中的信息定位要求的DLL的名称。然后系统按照下列顺序查找DLL文件的位置。 • 包含当前进程模块的目录; • 当前目录; • Windows系统目录,GetSystemDirectory 可以检索这个目录的路径; • Windows目录,GetWindowsDirectory 可以检索这个目录的路径; • 在PATH环境变量中列出的目录。 如果系统找不到指定的 DLL,它将终止进程并显示报告错误的对话框。否则,系统将DLL模块映射到进程的虚拟地址空间,并增加DLL引用计数。 操作系统找到指定的DLL之后,将调用入口点函数。函数参数为指明进程正在装载DLL的代码。如果入口点函数不返回 True,系统终止进程并报告错误(参见DLL的入口点函数一节)。 最后,系统修改进程代码以提供引用的起始地址。DLL在它的初始化期间映射到进程的虚拟地址空间,在需要时装载到物理内存。 引入之后,代码中就可以像使用普通函数/过程一样使用引入的函数/过程了。调用时需要注意。 • 必须用 stdcall 作为调用参数。 • 大小写敏感。与 Delphi 程序不同,调用动态链接库是大小写敏感的。
2.动态调用 动态调用DLL相对复杂很多,但非常灵活。使用 Windows API函数可以实现在运行时动态调用DLL并调用其中的过程。动态调用中使用的 Windows API 函数主要有 Loadlibrary、GetProcAddress 和Freelibrary3 个函数。它们都在 Windows.pas 中声明。 (1)LoadLibrary 和 SafeLoadLibrary 把指定库模块装入内存 语法为:
function LoadLibrary(LibFileName: PChar): THandle;
LibFileName 指定了要装载DLL的文件名。如果函数执行成功,则返回装载库模块的实例句柄。否则,返回一个小于 HINSTANCE_ERROR 的错误代码。 当应用程序调用 LoadLibrary 函数时,系统将按照在静态调用时的查找顺序定位DLL。如果查找成功,系统将DLL映射到进程的虚拟地址空间,并增加引用计数。如果调用 LoadLibrary 指明DLL的代码已经映射到另一个进程的虚拟地址空间,函数简单返回DLL的句柄并增加DLL引用计数。 注意:具有相同文件名和扩展名但是在不同目录的两个DLL不会被认为是同一个 DLL。 Delphi 中还提供了 SafeLoadLibrary 函数,它封装了 Loadlibrary 函数,可以装载由 Filename 参数指定的 WindowsDLL或 Linux 共享对象。它简化了DLL的装载并且使装载更加安全。Windows 中,SafeLoadLibrary 函数在 SysUtils 单元中进行了如下声明:
function SafeLoadLibrary(const Filename: string; ErrorMode: UINT = SEM_NOOPENFILEERRORBOX): HMODULE;
(2)GetProcAddress 获得给定模块中函数的地址 语法为:
function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc;
Module 包含被调用的函数库模块的句柄,这个值由 Loadlibrary 返回。如果把 Module 设置为 nil ,则表示要引用当前模块。 ProcName 是指向含有函数名的以nil 结尾的字符串的指针,或者也可以是函数的次序值。如果ProcName 参数是索引值,则如果该索引值的函数在模块中并不存在时,GetProcAddress 仍返回一个非nil 的值,这将引起混乱。因此大部分情况下用函数名是一种更好的选择。如果用函数名,则函数名的拼写必须与动态链接库文件 exports 中的对应拼写相一致。 如果 GetProcAddress 执行成功,则返回模块中函数入口处的地址,否则返回 nil 。 进程可以使用 LoadLibrary、LoadLibraryEx 或 GetModuleHandle 返回的句柄调用 GetProcAddress获得一个DLL模块中导出函数的地址。 (3)Freelibrary 从内存中移出库模块 语法为:
procedure Freelibrary(Module : THandle);
参数 Module 为DLL模块的句柄。这个值由 LoadLibrary 返回。 当DLL模块不再需要时,进程可以调用 FreeLibrary 函数。它将模块引用计数减 1,如果引用计数为 0 将解除DLL代码到进程的虚拟空间的映射。 在程序代码中,每调用一次 Loadlibrary 就应相应地调用一次 FreeLibray,以保证不会有多余的 DLL模块在应用程序运行结束后仍留在内存中。 (4)与动态调用DLL相关的其他 Windows API 函数 GetModuleHandle 函数可以返回在 GetProcAddress、FreeLibrary 或 FreeLibraryAndExit 线程中使用的句柄。只有DLL模块已经通过静态调用或动态调用映射到进程的虚拟地址空间时,GetModuleHandle函数才会执行成功。与 LoadLibrary 不同,GetModuleHandle 不增加模块引用计数。GetModuleFileName函数可以检索与 GetModuleHandle、LoadLibrary 返回的句柄相关联的模块的完整路径。 (5)使用动态调用引入DLL中的函数/过程 在程序中使用动态调用引入DLL中的函数/过程的基本步骤可以总结如下: • 声明需要引入的函数/类型,如:
type TAdd10 = function(number:integer):integer; stdCall;
• 在需要装载DLL的过程中,使用 LoadLibrary 或 SafeLoadLibrary 函数装载DLL。 • 使用 GetProcAddress 获得需要引用的函数/过程的地址之后就可以调用该函数/过程了。 • 使用完成后,调用 Freelibrary 释放DLL。
3.DLL的两种调用方式在 Delphi中的比较 现在简单评价一下调用DLL的两种方法的优缺点。 静态方法实现简单,易于掌握并且一般来说运行速度也稍微快一点,也更加安全可靠一些,与动态调用方式相比所需的代码较少。但是静态方法在运行时不能灵活地装卸所需的 DLL,而是在主程序开始运行时就装载指定的DLL直到程序结束时才释放该DLL。程序无法在运行时间里决定DLL的调用。并且如果要加载的DLL不存在或者DLL中没有要引入的过程或函数,这时候程序就自动终止运行。 动态方法较好地解决了静态方法中存在的不足,可以方便地访问DLL中的函数和过程。它在需要用到DLL时才通过 LoadLibrary 函数引入,用完后通过 FreeLibrary 函数从内存中卸载,而且通过调用GetProcAddress 函数可以指定不同的函数或过程。最重要的是,如果指定的 DLL出错,至多是 API 调用失败,不会导致程序终止。使用动态调用,即使装载一个DLL失败了,程序仍能继续运行。 因此,如果程序只在其中的一部分使用DLL中的过程,或者程序使用哪个 DLL、调用其中的哪个过程需要根据程序运行的实际状态来判断时,使用动态调用是一个很好的选择。但动态方法难以完全掌握,使用时根据不同的函数或过程要定义很多很复杂的类型和调用方法。对于初学者,应首先熟悉静态调用方法,熟练后再使用动态调用方法。
|
请发表评论