zoukankan      html  css  js  c++  java
  • 动态链接库和组件包  转

    DLL 是编写Windows 应用程序的关键组成部分。本章首先介绍了动态链接库(Dynamically
    Loadable Library,DLL)的基本概念,其次讲述了如何创建和使用DLL,同时还讨论了与DLL 相关
    的钩子函数和Delphi 组件包的基本知识。
    15.1 动态链接库概述
    动态可加载库(DLL)是一些过程的集合,这些过程可以被应用程序、其他的动态链接库或共享
    对象调用。和单元一样,动态可加载库含有共享的代码或资源。然而,这种库单独被编译成可执行文
    件,在运行时连接到使用它的程序。
    动态可加载库在Windows 中是指动态链接库(Dynamic Link Library,DLL),在Linux 中是指共
    享对象文件(Shared Object Library)。为区别动态可加载库和独立的可执行文件,在Windows 中,含
    有编译过的动态链接库文件命名时以.dll 为扩展文件名;在Linux 中,含有共享对象的文件命名时以.so
    为扩展文件名。Delphi 程序可以调用以其他语言编写的动态链接库(DLL)或共享对象,而用其他语
    言编写的应用程序也可以调用由Delphi 编写的动态链接库(DLL)或共享对象。
    本节主要讨论Windows 的动态链接库。
    15.1.1 动态链接库的概念
    动态链接是相对于静态链接而言的。所谓静态链接是指把要调用的函数或者过程链接到可执行文
    件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的.exe 文件中,该文件包含
    了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这
    样就造成了内存资源的浪费。而动态链接库不用重复编译或链接,一旦装入内存,DLL 函数可以被系
    统中的任何正在运行的应用程序软件所使用,而不必再次拷贝装入内存。
    DLL 是从C 语言函数库和Pascal 库单元的概念发展而来的。所有的C 语言标准库函数都存放在
    某一函数库中,同时用户也可以用LIB 程序创建自己的函数库。在链接应用程序的过程中,链接器从
    库文件中拷贝程序调用的函数代码,并把这些函数代码添加到可执行文件中。这种方法同只把函数储
    存在已编译的.obj 文件中相比更有利于代码的重用。
    但随着Windows 这样的多任务环境的出现,函数库的方法也显得过于累赘。为了完成屏幕输出、
    消息处理、内存管理、对话框等操作,每个程序都必须拥有自己的函数,这样Windows 程序将变得非
    常庞大。Windows 的发展要求允许同时运行的几个程序共享一组函数的单一拷贝,DLL 就是在这种情
    况下出现的。
    因此,在Windows 中,动态链接库是一个可以被其他应用程序共享的程序模块,其中封装了一些
    可以被共享的过程和资源。动态链接库文件的扩展名一般是dll,也有可能是drv、sys 和fon,它和可
    执行文件非常类似。实际上DLL 是一种特殊的可执行文件,说它特殊主要是因为一般它都不能直接运
    行,需要宿主程序比如*.exe 程序或其他DLL 的调用才能够使用,并且当装载DLL 时,它被映射到调
    用过程的虚拟地址空间。
    15.1.2 Windows 系统的动态链接库
    Windows 本身就是由大量的动态链接库支持的,这包括Windows API 函数,各种驱动程序文件,
    各种带有.fon 和.fot 扩展名的字体资源文件等。事实上,Kernel32.dll、User32.dll、GDI32.dll 文件就是
    核心Win32 系统的动态链接库。Kernel.dll 负责内存、进程和线程的管理。Uesr32.dll 包含了一些程序,
    是创建窗口和处理Win32 消息的用户接口。GDI32.dll 负责处理图形。还有一些其他的系统DLL,譬
    如AdvAPI32.dll 和ComDlg32.dll 文件,它们分别处理对象安全性/注册操作和通用对话框。Windows
    第15 章 动态链接库和组件包
    ·375·
    还提供了针对某一功能的专用DLL,如进行DDE 编程的ddeml.dll 文件,进行程序安装的ver.dll 文件
    等。
    虽然在编写Windows 程序时必然要涉及到DLL,但利用Delphi,用户并不会注意到这一点。这一
    方面是因为Delphi 提供了丰富的函数使用户不必直接去使用Windows API;另一方面是由于Delphi
    把API 函数和其他Windows DLL 函数重新组织到了几个库单元中,因而也不必使用特殊的调用格式。
    15.1.3 动态链接与静态链接
    静态链接是指编译器把要调用的函数和过程编译成可执行代码。在Delphi 中,函数的代码在应用
    程序的DPR 文件或一单元中。当链接用户的应用程序时,这些函数与过程便成为最终的可执行文件的
    一部分。也就是说,函数和过程都在程序的EXE 文件中。程序运行时,函数和过程随程序一起调入内
    存,它们的位置与程序的位置是相关的。当主程序需要调用函数或过程时,流程将跳转到函数或过程
    所在的位置,执行完函数或过程的代码,将返回主程序调用位置。而函数或过程的相对位置,在链接
    时就已经确定了。
    “动态链接”这几字指明了DLL 是的工作方式。对于常规的函数库,链接器从中拷贝它需要的所
    有库函数,并把确切的函数地址传送给调用这些函数的程序。而对于DLL,函数储存在一个独立的动
    态链接库文件中。在创建Windows 程序时,链接过程并不把DLL 文件链接到程序上。直到程序运行
    并调用一个DLL 中的函数时,该程序才要求传送这个函数的地址。
    动态链接相对静态链接有以下优势。
    *减小可执行文件大小。
    *可以共享代码、资源和数据。使用DLL 的主要目的就是为了共享代码,DLL 的代码可以被所
    有的Windows 应用程序共享。
    *在同一基地址调用DLL 的进程可以同时使用一个DLL,在物理内存共享DLL 代码的拷贝。这
    样可以节省内存和减少交换。
    *当DLL 中的函数变化时,只要函数参数、调用方法和返回值不变,使用它们的应用程序就不需
    要重新连接和编译。即对DLL 中函数的修改可以自动传播到所有调用它的程序中,而不必对程序作任
    何改动或处理。相对而言,静态链接对象代码要求函数变化时,应用程序就要重新链接。
    *便于维护和升级,DLL 可以提供售后技术支持。例如可以修改一个显示驱动DLL 以支持应用
    程序发布时还没有的显示器。
    *使用不同语言编写的程序可以调用相同的DLL 函数,只要程序与函数使用的调用方式一样。调
    用方式(例如C、Pascal 或标准调用)包括调用函数参数压入堆栈的顺序、是被调用函数还是调用函
    数负责清理堆栈以及参数是否都通过寄存器传递。这实际上拓展了开发工具的功能。由于DLL 是与语
    言无关的,因此可以创建一个DLL,被Visual C++、Visual Basic 或任何支持动态链接库的语言调用。
    这样如果一种语言存在不足,就可以通过访问另一种语言创建的DLL 来弥补。
    *比较安全。这里说的安全也包括很多方面,如DLL 文件遭受病毒的侵害机率要比普通的EXE
    文件低很多。另外,由于是动态链接的,这给一些从事破坏工作的“高手”们也带来了一些反汇编的
    困难。
    *隐藏实现的细节。DLL 中的过程可以被应用程序访问,而应用程序并不知道这些过程的细节。
    *DLL 不仅提供了函数重用的机制,而且提供了数据共享的机制。任何应用程序都可以共享由装
    入内存的DLL 管理的内存资源块。只包含共享数据的DLL 称为资源文件。如Windows 的字体文件等。
    使用DLL 的一个潜在缺点是应用程序不是“自包含”(Self-Contained)的,即它依赖于一个独立
    的DLL 模块而存在。使用静态调用时,当要求的DLL 在进程启动中没有找到,系统将中断进程并给
    使用者一个错误信息。使用动态调用在这种情况下系统不中断进程,但是DLL 的导出函数在程序中不
    可用。

    ·376·
    15.1.4 DLL 和系统变量
    在System 单元声明的变量中,有几个对DLL 编程有特殊影响。IsLibrary 可以检测代码是执行在
    应用程序中还是执行在DLL 中,在应用程序中IsLibrary 总是为False,在DLL 中总是为True。在DLL
    的整个生命周期中,HInstance 包含了库的实例句柄。在DLL 中,系统变量CmdLine 总是为nil。
    DLLProc 变量允许DLL 监视操作系统对DLL 的入口点的调用。这一特性通常只用于那些支持多
    线程的DLL。关于入口点函数的详细资料参见DLL 的入口点函数一节。
    15.1.5 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.入口点函数的调用
    当过程被调用时,传递到回调函数的参数的可取值,实际上也是引起调用入口点函数的事件,如
    表15-1 所示。
    表15-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 函数禁止线
    程终止时发出通知
    在过程(即回调函数)的主体中,可以根据传递到过程的是哪一个参数来指定相应的动作。
    第15 章 动态链接库和组件包
    ·377·
    系统在调用函数的进程或线程的上下文中调用入口点函数。这允许DLL 使用自己的入口点函数在
    调用进程的虚拟地址空间分配内存或打开访问进程的句柄。并且如果一个进程已经使用了LoadLibrary
    调用DLL,但没有调用FreeLibrary 函数释放该DLL,则入口点函数不会被该进程再次调用。
    15.1.6 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 中的内存管理模块代替。
    15.1.7 DLL 中的数据
    基于Win32 的DLL 可以包含全局或局部数据。
    一个DLL 可以被几个应用程序同时使用,但每个应用程序的进程空间各自拥有该DLL 的一个副
    本及自身的一套全局变量。如果需要共享内存的多个DLL 或一个DLL 的多个实例,它们必须使用内
    存映射文件(Memory-Mapped Files)。
    DLL 变量的默认作用范围与应用程序中声明的变量是一样的。DLL 源代码中的全局变量对使用
    DLL 的进程来说也是全局的。而静态变量的作用范围限制在声明它们的块中。因此默认情况下,每个
    进程都会有它自己的DLL 全局和静态变量的实例。
    当一个DLL 使用任何内存分配函数(GlobalAlloc、LocalAlloc、HeapAlloc 和VirtualAlloc)分配
    内存时,内存都是分配给调用进程的虚拟地址空间,并且只能被那个进程的线程访问。
    15.1.8 DLL 中的异常和运行时错误
    在DLL 中,当一个异常被引发但未被处理时,它将向DLL 外传播,即传播到调用者。如果调用
    该DLL 的应用程序或DLL,其自身也是由Delphi 编写,那么异常可以通过一般的try...except 语句被
    处理。
    如果调用DLL 的应用程序或DLL 是用其他语言编写的,那么异常可以作为操作系统异常被处理,
    此时的异常代码是$0EEDFACE。在操作系统的异常记录数组Exception Information 中,第1 个入口(即
    字段)包含了异常地址,第二个入口包含了一个对Delphi 异常对象的引用。
    通常,应当在DLL 内部处理所有的异常。在Windows 中,Delphi 异常映射到操作系统(OS)异
    常模块。

    ·378·
    如果一个DLL 没有使用SysUtils 单元,那么它不能支持异常。这时,当错误在DLL 中发生时,
    调用该DLL 的应用程序将终止。因为DLL 无法获悉调用它的模块是否是Delphi 程序,所以它不能调
    用应用程序的退出过程。应用程序只是简单地终止并从内存中删除。
    15.2 创建和调用DLL
    15.2.1 创建DLL 的基本步骤
    在Delphi 中编写DLL 其实不是一件困难的事。现在以MyFirstDLL.dll 的实现为例说明编写动态
    链接库的基本步骤。MyFirstDLL.dll 非常简单,只提供了一个将一个整数加10 之后返回的函数Add10。
    1.建立一个DLL 程序框架
    在Delphi 7 中,从IDE 菜单中选择“File*New*other”,在弹出窗口中选择“New”标签页,
    然后选择“DLL Wizard”,最后Delphi 7 就会自动生成一个动态链接库工程模板。细心的读者可能会
    注意到,与新建一个工程项目不同,生成的空工程文件代码如下:
    library Project1;
    { Important note about DLL memory management: ShareMem must be the
    first unit in your library’s USES clause AND your project’s (select
    Project-View Source) USES clause if your DLL exports any procedures or
    functions that pass strings as parameters or function results. This
    applies to all strings passed to and from your DLL--even those that
    are nested in records and classes. ShareMem is the interface unit to
    the BORLNDMM.DLL shared memory manager, which must be deployed along
    with your DLL. To avoid using BORLNDMM.DLL, pass string information
    using PChar or ShortString parameters. }
    uses
    SysUtils,
    Classes;
    {$R *.res}
    begin
    end.
    它的第一行与普通工程文件的不同在于它是以library 关键字开始的,而一般工程文件是以program
    关键字开始的。不同的关键字通知编译器生成不同的可执行文件,用program 关键字生成的是EXE 文
    件,而用library 关键字生成的是DLL 文件。代码中的注释说明了如果将string 类型作为参数,那么必
    须将ShareMem 作为Uses 语句中的第1 个单元。
    2.以适当的文件名保存文件
    输入合适的文件名后library 后跟的工程名自动修改,本例中保存为MyFirstDLL.dpr。
    3.输入过程、函数代码
    这个阶段与建立一个普通工程的过程是基本类似的。本例需要在动态链接库中实现一个整数加10
    的函数。
    第15 章 动态链接库和组件包
    ·379·
    首先,在工程中新建一个Unit,保存为FirstDLL.pas。
    在Unit 的interface 界面声明需要用到的常量以及Add10 函数的定义:
    interface
    function Add10(number:integer):integer; stdcall;
    在Unit 的implemention 部分加入Add10 函数的实现部分:
    function Add10(number:integer):integer;
    begin
    result := number + 10;
    end;
    4.在工程文件中建立exports 子句
    假如DLL 要输出供其他应用程序使用的函数或过程,则必须将这些函数或过程列在exports 子句
    中。列于一个exports 子句中的函数或过程是被输出的。exports 子句具有如下形式:
    exports
    entry1,...,entryn;
    这里的每个entry(入口)由过程、函数或变量(在exports 子句中必须是优先声明的)、跟随的参
    数列表(仅当输出的函数或过程是被重载的时)以及可选的name 说明符组成。在exports 子句中,可
    以用单元名称作为过程或函数名称的限定词(以更加准确地标明要输出的函数和过程)。
    exports 子句可以位于单元的interface 或implementation 部分。对于任何库,只要在其uses 子句中
    包含了这样的单元,那么库会自动输出列于这些单元内exports 子句中的所有函数或过程,而不需要在
    库的exports 子句中再次输出。
    指令local 用于标明函数或过程是不可被输出的,该指令因具体平台的不同而具有不同的意义,它
    对Windows 编程中没有影响。
    从DLL 中输出多载(overload)的函数或过程时,必须在exports 子句中指定其参数列表。例如:
    exports
    Divide(X,Y:Integer) name ’Divide_Ints’,
    Divide(X,Y:Real) name ’Divide_Reals’;
    在Windows 的多载函数或过程的入口中不要包含index 说明符。
    可以利用标准指令以方便和加速过程、函数的调用。在动态链接库的输出部分,用到了3 个指令,
    即name、index、resident。
    (1)name 指令
    name 指令由指示字name 及跟随的串常量组成。如果一个入口没有name 指令,那么该函数或过
    程将以其最初声明的名称(相同的拼写和大小写)被输出。想要以不同的名称输出一个函数或过程时,
    应使用name 说明符。如:
    exports
    DoSomethingABC name ’DoSomething’;
    其他应用程序将用新名字“DoSomething”调用该过程或函数。
    (2)index 指令
    对于且仅对于Windows,index 指令由指示字index 及跟随的数字常量组成,数字常量的范围是1~
    2147 483 647(为了提高程序的运行效率,最好使用小的索引值)。如果一个入口没有index 指令,则
    由编译器按顺序进行分配。
    注意:index 指令仅用于向后兼容,该指令已无用处并且可能致使其他开发工具发生问题。
    (3)resident 指令
    resident 指令主要是为了向后兼容,编译器将忽略它。

    ·380·
    本例中需要输出Add10 这个函数,则exports 语句如下:
    exports
    Add10;
    5.输入DLL 初始化代码
    初始化代码完成的主要工作是:初始化变量、分配全局内存块、登录窗口对象等初始化工作以及
    设置DLL 退出时的执行过程。Delphi 通过在工程文件中的begin...end 部分添加初始化代码实现了这个
    功能,一些必要的工作可以由系统自动完成。DLL 的初始化代码还可以通过ExitProc 变量装入退出过
    程,其作用就像Exit 过程的作用;当卸载DLL 时,退出过程被执行。可以设置多个退出过程,退出
    时按顺序依次被调用。
    DLL 的初始化代码可以通过设置ExitCode 变量为非零值来指明初始化时发生了错误。ExitCode
    变量在System 单元中声明,默认值为零,表示初始化成功。如果DLL 的初始化代码设置ExitCode 为
    其他值,那么DLL 将被卸载并且调用DLL 的应用程序将收到失败的通知。同样,如果在初始化代码
    执行中出现异常并且未被处理,那么调用该DLL 的应用程序也将收到加载DLL 失败的通知。
    下面是DLL 的初始化代码及退出过程的例子:
    library Test;
    var
    SaveExit:Pointer;
    procedure LibExit;
    begin
    ...//退出代码
    ExitProc:=SaveExit; //恢复退出过程链
    end;
    begin
    ...//初始化代码
    SaveExit:=ExitProc; //保存退出过程链
    ExitProc:=@LibExit; //装入退出过程LibExit
    end.
    DLL 被卸载时,其退出过程被存储在ExitProc 变量中的地址重复调用,直到ExitProc 变成nil。
    被DLL 使用的所有单元,其初始化部分在DLL 的初始化代码执行之前被执行,并且这些单元的结束
    (finalization)部分在DLL 的退出过程执行之后被执行。
    MyFirstDLL 中没有设置初始化代码。
    6.编译程序,生成动态链接库文件
    与编译普通工程一样,在代码编写完毕之后,就可以编译、生成动态链接库了。值得注意的是DLL
    不能直接运行。
    7.编写DLL 函数、过程必须注意的问题
    下面总结了一些在编写DLL 函数、过程中必须注意的一些问题。
    (1)在DLL 中编写的函数或过程最好加上stdcall 调用参数
    如果想要让由Delphi 编写的DLL 对用其他语言编写的程序也可用,那么在输出函数的声明中指
    定stdcall 调用约定是最可靠的,因为其他语言可能不支持ObjectPascal 中默认的register 调用约定。忘
    记使用stdcall 参数是常见的错误,这个错误不会影响DLL 的编译和生成,但当调用这个DLL 时会发
    生很严重的错误,可能导致操作系统的死锁。
    (2)当使用了长字符串类型的参数、变量时要引用ShareMem
    Delphi 中的string 类型功能很强大。但是如果在动态链接库中使用string 类型的参数、变量甚至
    第15 章 动态链接库和组件包
    ·381·
    是记录信息时,就必须引用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 是“线程安全”的,即只使用支持多线
    程的库,并且必须保证全局数据的同步访问。
    15.2.2 调用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 的入口点函数一节)。

    ·382·
    最后,系统修改进程代码以提供引用的起始地址。DLL 在它的初始化期间映射到进程的虚拟地址
    空间,在需要时装载到物理内存。
    引入之后,代码中就可以像使用普通函数/过程一样使用引入的函数/过程了。调用时需要注意。
    *必须用stdcall 作为调用参数。
    *大小写敏感。与Delphi 程序不同,调用动态链接库是大小写敏感的。
    下面以一个实例来解释静态调用动态链接库的过程。
    实例中使用一个输入框和一个按钮,使用者在输入框中输入一个数字,单击“加十”按钮,输入
    框显示对输入数字加10 后的数字(利用15.2.1 中开发的DLL 中的函数)。
    要能够在程序中调用DLL 中的函数,首先需要在单元的interface 部分进行声明:
    function Add10(number:integer):integer;stdcall;external ’MyFirstDLL.DLL’;
    在程序中就可以直接使用了,如“加十”按钮的click 事件处理代码:
    procedure TForm1.Button1Click(Sender: TObject);
    var
    temp:integer;
    begin
    try
    temp := StrToInt(Edit1.Text );
    Edit1.Text := IntToStr(add10(temp));
    except
    ShowMessage(’请输入一个整数!’);
    end;
    end;
    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 参数
    指定的Windows DLL 或Linux 共享对象。它简化了DLL 的装载并且使装载更加安全。Windows 中,
    SafeLoadLibrary 函数在SysUtils 单元中进行了如下声明:
    function SafeLoadLibrary(const Filename: string; ErrorMode: UINT =
    SEM_NOOPENFILEERRORBOX): HMODULE;
    (2)GetProcAddress 获得给定模块中函数的地址
    语法为:
    function GetProcAddress(Module: THandle; ProcName: PChar): TFarProc;
    第15 章 动态链接库和组件包
    ·383·
    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。
    下面是一个动态调用DLL 中的函数的例子,该例的功能与静态调用中的例子是一样的,只是将静
    态调用改为动态调用。程序的主要差别在于“加十”按钮的Click 事件的处理代码:
    procedure TForm1.Button1Click(Sender: TObject);
    var
    temp:integer;
    handle:THandle ;
    FPointer: TFarProc;
    MyFunc : TAdd10 ;
    begin
    try
    temp := StrToInt(Edit1.Text );
    handle := LoadLibrary(’MyFirstDLL’); {装载DLL 到内存}
    if handle <> 0 then
    try

    ·384·
    FPointer := GetProcAddress(handle ,’Add10’); {获得函数的入口地址}
    if FPointer <> nil then
    begin
    MyFunc := TAdd10(FPointer) ;
    temp := MyFunc(temp) ; {使用函数操作}
    Edit1.Text := IntToStr(temp);
    end;
    finally
    FreeLibrary(handle); {释放DLL}
    end
    else
    ShowMessage(’未找到动态链接库MyFirstDLL.dll’);
    except
    ShowMessage(’请输入一个整数!’);
    end;
    end;
    动态调用时,DLL 直到含有调用LoadLibrary 的代码执行时才被加载。该DLL 可以通过调用
    FreeLibrary 被卸载。当程序使用的DLL 不必在内存中继续存在时,这种动态加载的方式就可以减少内
    存消耗。
    并且当调用的DLL 不存在时,动态调用可使进程继续运行,并且允许进程使用其他的方法完成功
    能。例如进程不能定位一个DLL,它可以使用其他的方法进行查找,或者通知用户发生了一个错误。
    如果用户可以提供丢失的DLL 的完整路径,进程可以使用这个信息去装载这个DLL,即使它不在常
    规的查找路径中。这个情况可以与静态调用对比。在静态调用中,如果系统找不到DLL,它将简单的
    终止进程。
    如果DLL 使用入口点函数完成一个进程中的每个线程的初始化,动态调用将引起问题,因为入口
    点函数不能被在调用LoadLibrary 之前存在的线程调用。
    3.DLL 的两种调用方式在Delphi 中的比较
    现在简单评价一下调用DLL 的两种方法的优缺点。
    静态方法实现简单,易于掌握并且一般来说运行速度也稍微快一点,也更加安全可靠一些,与动
    态调用方式相比所需的代码较少。但是静态方法在运行时不能灵活地装卸所需的DLL,而是在主程序
    开始运行时就装载指定的DLL 直到程序结束时才释放该DLL。程序无法在运行时间里决定DLL 的调
    用。并且如果要加载的DLL 不存在或者DLL 中没有要引入的过程或函数,这时候程序就自动终止运
    行。
    动态方法较好地解决了静态方法中存在的不足,可以方便地访问DLL 中的函数和过程。它在需要
    用到DLL 时才通过LoadLibrary 函数引入,用完后通过FreeLibrary 函数从内存中卸载,而且通过调用
    GetProcAddress 函数可以指定不同的函数或过程。最重要的是,如果指定的DLL 出错,至多是API
    调用失败,不会导致程序终止。使用动态调用,即使装载一个DLL 失败了,程序仍能继续运行。
    因此,如果程序只在其中的一部分使用DLL 中的过程,或者程序使用哪个DLL、调用其中的哪
    个过程需要根据程序运行的实际状态来判断时,使用动态调用是一个很好的选择。但动态方法难以完
    全掌握,使用时根据不同的函数或过程要定义很多很复杂的类型和调用方法。对于初学者,应首先熟
    悉静态调用方法,熟练后再使用动态调用方法。
    15.3 在DLL中封装窗体
    在DLL 中可以封装窗体,需要解决的关键问题在于DLL 中的Application 对象和调用的Application
    第15 章 动态链接库和组件包
    ·385·
    对象是有区别的。对于一般的应用程序来说,Application 对象是VCL 固定的,一般不需要修改
    Application 对象指针。但是在DLL 中,使用窗体或者使用Application 对象时,必须使DLL 的Application
    和调用程序一样,这样才不至于混淆。如果不修改Application 对象,那么应用程序退出的时候,可能
    会出现错误。例如使用如下代码在调用程序中导出一个函数:
    function DllFunction(App:TApplication;PForm:TForm):TForm2;stdcall;
    begin
    Result:=TForm2.Create(PForm);
    end;
    当主程序退出时,就有可能发生错误。
    解决该问题的步骤如下:
    *按照创建DLL 的步骤新建一个DLL 项目。
    *在DLL 项目中新建一个需要封装的窗体,并根据需要添加窗体代码。
    *增加一个过程,过程以应用程序的句柄作为参数,并将此句柄赋值给DLL 的Application 对象
    的句柄,示例代码如下:
    procedure SynAPP(App:THandle );stdcall;
    begin
    Application.Handle := App;
    end;
    *编译生成DLL 文件。
    *应用程序中需要调用封装在DLL 中的窗体时,首先调用SynAPP 过程,然后进行其他操作。
    下面举例说明这个步骤。在DLL 中封装一个窗体,窗体中只有一个“随机颜色”按钮,单击该按
    钮则改变窗体的背景色。
    首先,新建一个DLL 项目工程,新建一个窗体并在该窗体上添加一个名为“随机颜色”的按钮,
    其Click 事件处理代码如下:
    procedure TfrmDLL.Button1Click(Sender: TObject);
    begin
    Color := RandomRange(0,255 * 255 * 255 );
    end;
    然后,在窗体中增加方法SynApp 和显示窗体的方法ShowForm。代码如下:
    interface
    {省略了其他代码}
    procedure SynAPP(App:THandle);stdcall;
    procedure ShowForm;stdcall;
    implementation
    procedure SynAPP(App:THandle );stdcall;
    begin
    Application.Handle := App;
    end;
    procedure ShowForm;stdcall;
    begin
    try
    frmDLL := TfrmDLL.Create (Application);
    try
    frmDLL.ShowModal;

    ·386·
    finally
    frmDLL.Free;
    end;
    except
    on E: Exception do
    MessageDlg (’Error in DLLForm: ’ +
    E.Message, mtError, [mbOK], 0);
    end;
    end;
    在DLL 项目文件中增加导出函数/过程的声明:
    exports
    SynAPP,ShowForm;
    编译之后DLL 就创建完成了。
    在使用DLL 时,首先必须调用SynApp,并且将主程序的Application 的句柄作为参数传递,否则
    主程序退出时会引起操作系统错误。
    主程序也非常简单,就是在一个窗口中加入一个按钮,直接显示封装在DLL 中的窗体。
    首先声明需要导入的过程:
    interface
    {省略了其他代码}
    procedure SynAPP(App:THandle);stdcall;external ’FormDLL.dll’;
    procedure ShowForm;stdcall;external ’FormDLL.dll’;
    调用封装在DLL 中的窗体的代码并添加在按钮的Click 事件处理过程中:
    procedure TForm1.Button1Click(Sender: TObject);
    begin
    SynApp(Application.Handle ); {首先必须调用这个过程,并且使用Application 的句柄作为参数}
    ShowForm ;
    end;
    15.4 使用DLL共享数据
    16 位的Windows 与32 位的Win32 处理DLL 内存的方式是不同的。在16 位的Windows 中,不
    同的应用程序可以共享16 位DLL 的全局数据。也就是说,如果DLL 定义了一个全局变量,所有调用
    这个DLL 的应用程序都可以访问这个变量,而其中的一个应用程序对变量的修改,将会影响其他应用
    程序。这种做法很危险,容易引起冲突。
    而Win32 就不再共享DLL 的全局变量。因为每个应用程序都是将DLL 映射到自己的地址空间,
    同时DLL 的数据也就随之被映射。这样,每个应用程序都有自己DLL 数据实例,在一个应用程序中
    修改DLL 中的全局变量,不会影响其他应用程序。因此,如果需要在DLL 中共享数据就需要使用其
    他技术实现,例如使用内存映射文件。
    15.4.1 内存映射文件的概念
    内存映射文件提供的是一种方法,就是在Win32 系统的地址空间里保留一块区域,物理存储可以
    向其中提交。这类似于分配内存并用指针来访问内存。不过,内存映射文件可以把磁盘上的文件映射
    到这个地址空间,并用指针访问该文件,就如同用指针访问内存一样。
    当多个应用程序共享DLL 的代码时,首先将该DLL 调入内存,然后每个应用程序都有该DLL 映
    像。事实上,内存中只有一份DLL,这就是内存映射文件所做的。
    内存映射文件必须先获得磁盘文件的句柄,然后将内存映射对象映射到该文件。当创建内存映射
    第15 章 动态链接库和组件包
    ·387·
    文件时,实际上是使文件与进程虚拟空间的一段区域关联。为建立关联,首先需要创建一个文件映射
    对象。要查看或编辑文件的内容,必须获得文件映射对象的视图。这样,当通过指针访问文件的内容
    时,就如同访问内存区域一样了。
    内存映射文件对象不只是磁盘文件,也可以是Win32 的页面调度文件。内存映射文件使得系统可
    以像访问一个磁盘文件那样访问内存中的一个区域。Win32 系统自己管理页面调度文件,当向文件视
    图写入数据时,系统负责处理数据的缓存、缓冲、写入和调入,当不再需要该页面调度文件时,系统
    会自动将有关区域释放。这好像是在一块内存区域中编辑数据。文件的输入/输出完全由系统来处理,
    相比其他的标准文件输入/输出技术,有了很大地简化,而且速度也得到了提高。
    使用内存映射文件时,只要映射的是相同的文件映射对象,Win32 系统就能够保证一个文件的多
    个视图是一致的。这意味着,在某一个视图下对文件所做的修改可以被其他视图看到。不过一定要保
    证映射相同的文件映射对象。如果是不同的文件映射对象,则系统就不能保证这些视图的一致性。当
    然,这主要体现在写操作上。对于只读的文件来讲,总是一致的。但是,如果在网络中的不同机器上
    进行写操作时,视图不保持一致。
    15.4.2 与内存映射文件相关的函数
    创建和访问内存映射文件主要是用Win32 API 函数和定义在Delphi SysUtils 单元中的一些函数实
    现的。
    1.创建和打开文件
    创建和打开一个内存映射文件的第一步是获取一个文件的句柄,只有获得有效文件句柄后,才能够
    获得文件映射对象。可以调用FileCreate 或FileOpen 获得文件句柄。
    FileCreate 创建一个文件名为FileName 的新文件。如果函数调用成功则返回一个有效的文件句柄。
    否则,返回INVALID_HANDLE_VALUE 常量,它在SysUtils.pas 单元中声明如下:
    function FileCreate(const FileName: string): Integer;
    FileOpen 用来以某种模式打开一个已存在的文件。如果函数调用成功则返回一个有效的文件句柄。
    否则,返回INVALID_HANDLE_VALUE 的常量。FileOpen 在SysUtils.pas 单元中声明如下:
    function FileOpen(const FileName: string; Mode: LongWord): Integer;
    参数FileName 是映射文件的名称和完全路径。参数Mode 的可取值如表15-2 所示。
    表15-2 FileOpen 函数Mode 参数的可取值
    参数取值 说明
    fmCreate 如果文件存在,则以可读/写方式打开,否则创建新文件,在Classes 单元中定义
    fmOpenRead 只读
    fmOpenWrite 只写
    fmOpenReadWrite 可读/写
    fmShareCompat 与打开FCBs 的方法兼容
    fmShareExclusive 禁止共享
    fmShareDenyWrite 不允许他人使用fmOpenWrite 模式打开文件
    fmShareDenyRead 不允许他人使用fmOpenRead 模式打开文件
    fmShareDenyNone 允许他人使用任何模式打开文件
    2.创建文件映射对象
    无论是命名的或是未命名的内存映射文件对象,都可以使用CreateFileMapping 函数来创建。函数
    声明如下:
    function CreateFileMapping(
    hFile: THandle;

    ·388·
    lpFileMappingAttributes:
    PSecurityAttributes;
    flProtect, dwMaximumSizeHigh, dwMaximumSizeLow: DWORD;
    lpName: PChar): THandle; stdcall;
    传递给CreateFileMapping 的参数为系统创建文件映射对象提供了必要的信息。
    参数hFile 是先前调用FileOpen 或FileCreate 函数返回的文件句柄。如果hFile 是0xFFFFFFFF,
    调用过程必须在参数dwMaximumSizeHigh 和dwMaximumSizeLow 中指定映射对象的大小,此时函数
    返回一个操作系统创建的指定大小的文件映射对象而不是文件系统的一个命名文件。在DLL 中共享数
    据时,常常设置hFile 参数为0xFFFFFFFF。
    lpFileMappingAttributes 参数是一个PSecurityAttributes 类型的指针,指向文件映射对象的安全属
    性。此参数通常为null。
    参数flProtect 用来指定文件视图的保护类型,这个值必须与打开文件以获取文件句柄时用的参数
    一致。flProtect 参数的可取值和含义如表15-3 所示。
    表15-3 CreateFileMapping 函数flProtect 参数可取值
    参数取值 说明
    PAGE_READONLY 文件可读。文件必须用FileCreate 或fmOpenRead 模式的FileRead 创建
    PAGE_READWRITE 文件可读/写。文件必须以fmOpenReadWrite 模式打开
    PAGE_WRITECOPY
    文件可读/写。但进行写操作时,会复制修改过的页面。这样,多个进程之间共享的映射文件
    就不会双倍消耗系统内存和其他资源。只有修改过的内存页面被复制。文件必须以
    fmOpenWrite 或fmOpenReadWrite 方式打开
    除此之外,可以使用位或运算为flProtect 参数设置多个扇区属性,其属性的可取值和含义如表15-4
    所示。
    表15-4 CreateFileMapping 函数flProtect 参数扇区属性的可取值
    参数取值 说明
    SEC_COMMIT 在内存或页交换文件中为同一扇区中的所有页面分配物理存地址(默认)
    SEC_IMAGE 文件映射信息和属性取自文件映像(Windows 95/98 忽略此属性)
    SEC_NOCACHE
    未被映射的页面被置入缓存,因此系统允许直接把数据写入磁盘上的文件。(主要应用于设备驱
    动程序,Windows95/98 忽略此属性)
    SEC_RESERVE 保留扇区中页面,不分配物理存储
    dwMaximumSizeHigh 参数用于指定文件映射对象的最大尺寸的高32 位。除非要访问的文件大于
    4GB,否则值为0。
    dwMaximumSizeLow 参数用于指定文件映射对象的最大尺寸的低32 位,值为0 时,表示文件映
    射对象的最大尺寸等于被映射文件的尺寸。
    lpName 参数用于指定文件映射对象的名称。名称中可含有除‘/’以外的所有字符。如果名称与
    一个已有的文件映射对象相同,则表示要用flProtect 参数所指定的属性访问那个同名的文件映射对象。
    该参数设为0,表示创建一个无名的文件映射对象。
    如果CreateFileMapping 调用成功,则返回文件映射对象的句柄。如果此句柄代表一个已有的文件
    映射对象,GetLastError 将返回ERROR_ALREADY_EXISTS。如果CreateFileMapping 调用失败,将返
    回0。此时,必须调用GetLastError 得到失败的原因。
    当得到有效的文件映射对象后,就可以将文件的数据映射到进程的地址空间了。
    3.映射文件的视图到进程的地址空间
    MapViewOfFile 能把文件的视图映射到进程地址空间。此函数在Windows 单元声明如下:
    第15 章 动态链接库和组件包
    ·389·
    function MapViewOfFile(
    hFileMappingObject: THandle;
    dwDesiredAccess: DWORD;
    dwFileOffsetHigh, dwFileOffsetLow, dwNumberOfBytesToMap: DWORD): Pointer; stdcall;
    参数hFileMappingObject 是通过CreateFileMapping 或OpenFileMapping 返回的文件映射对象的句
    柄。
    参数dwDesiredAccess 用于指定文件数据访问模式,它的可取值如表15-5 所示。
    表15-5 MapViewOfFile 函数dwDesiredAccess 参数的可取值
    参数取值 说明
    FILE_MAP_WRITE 可读/写。调用CreateFileMapping 时必须用PAGE_READ_WRITE 参数
    FILE_MAP_READ 只读。调用CreateFileMapping 必须用PAGE_READ_WRITE 或PAGE_READ 参数
    FILE_MAP_ALL_ACCESS 与FILE_MAP_WRITE 相同
    FILE_MAP_COPY
    允许Copy-on-Write 模式,即在文件写入的同时进行内容复制。调用CreateFileMapping
    时必须用PAGE_READ_ONLY、PAGE_READ_WRITE 或PAGE_WRITE_COPY 参数
    dwFileOffsetHigh 参数指定文件映射起始位置的偏移量的高32 位。
    dwFileOffsetLow 参数指定文件映射起始位置的偏移量的低32 位。
    dwNumberOfBytesToMap 参数指定需要映射的字节数,为0 时表示文件的全部。
    MapViewOfFile 返回视图的起始地址,函数调用失败则返回nil。这时必须调用GetLastError 函数
    确定错误原因。
    4.取消文件映射
    UnmapViewOfFile 用来解除文件视图与进程地址空间的映射关系。函数声明如下:
    function UnmapViewOfFile(lpBaseAddress: Pointer): BOOL; stdcall;
    惟一的指针类型参数lpBaseAddress 必须指向映射区域的起始地址。这个值与MapViewOfFile 的
    返回值是同一个值。
    完成对文件的处理后,必须调用UnmapViewOfFile 取消对文件的映射;否则,只能等到进程终止,
    系统才会释放映射区域所占的内存。
    5.关闭文件映射和文件内核对象
    对FileOpen 和CreateFileMapping 的调用都将打开内核对象,因此必须关闭这些内核对象。这可
    以通过调用CloseHandle 函数来实现。CloseHandle 函数声明如下:
    function CloseHandle(hObject: THandle): BOOL; stdcall;
    如果调用CloseHandle 函数成功则返回TRUE,否则返回FALSE,此时必须调用GetLastError 来
    判断出错原因。
    15.4.3 使用内存映射文件在DLL 中共享数据
    使用内存映射文件可以实现在DLL 中共享数据,实现的主要思想是:需要共享数据时则在DLL
    中申请一个无名的内存映射文件,所有共享的数据都从这个内存映射文件中读取和写入,DLL 提供读
    出和写入数据的接口方法,其他应用程序只需要调用接口方法实现对共享数据的存取;不需要使用共
    享数据时,则关闭内存映射文件。
    下面就通过一个简单的例子实现这个想法。例子中首先创建一个共享数据的DLL,其中提供了一
    个共享的整型数据,并提供了存取这个整型数值的接口。外部存在一个服务端程序,它负责向DLL 中
    写入数据,还存在一个客户端程序,它定时地从DLL 中读取数据。程序完成之后,可以看到,只要服
    务端中的数据改变,客户端的数据也随之改变。

    ·390·
    1.编写共享数据的DLL
    *首先按照建立DLL 的方法建立一个新的DLL 项目命名为DLLMem.dpr,在项目中新加入一个
    单元命名为ShareMem.pas。
    *定义共享数据的类型和指针以及共享数据大小和共享文件名的常量,代码如下:
    type
    TShareData = Integer ;
    PShareData = ^TShareData;
    const
    SHAREDATA_SIZE = SizeOf(TShareData);
    MapFileNamw = ’DLLMemTest’;
    *建立和取消内存映射文件的方法。
    在DLL 中建立内存映射文件,需要定义一个文件句柄以及文件访问指针,并在建立内存映射文件
    时给它们赋值。代码如下:
    var
    MapHandle:THandle ; //内存映射文件句柄
    ShareData:PShareData ; //共享数据指针
    procedure OpenShareData;
    procedure CloseShareData;
    procedure OpenShareData;
    begin
    MapHandle := CreateFileMapping($FFFFFFFF,nil,
    PAGE_READWRITE,0,SHAREDATA_SIZE,MapFileNamw); {建立内存映射文件}
    if MapHandle = 0 then {处理错误}
    Raise Exception.Create(’创建公用数据的Buffer 不成功!’);
    ShareData := MapViewOfFile ( MapHandle, File_Map_Write,
    0, 0, SHAREDATA_SIZE ); {获得内存映射文件的指针}
    end;
    procedure CloseShareData;
    begin
    UnMapViewOfFile(ShareData); {释放内存映射文件}
    CloseHandle(MapHandle );
    end;
    *定义存取共享数据的接口和方法。
    这时可以定义内存映射文件存取数据的接口,代码如下:
    procedure SetShareData(value:TShareData);stdcall;
    begin
    ShareData^ := value ;
    end;
    function GetShareData:TShareData ;stdcall;
    begin
    Result := ShareData^;
    end;
    第15 章 动态链接库和组件包
    ·391·
    *定义入口点函数。
    任何一个应用程序装载DLL 时都可以建立这个映射文件,为此需要在DLL 的入口点函数中调用
    建立和取消内存映射文件的方法。首先定义和实现入口点函数,代码如下:
    procedure DLLEntryPoint(Reason:DWord);
    begin
    case Reason of
    DLL_PROCESS_ATTACH:OpenSharedata;
    DLL_PROCESS_DETACH:CloseShareData ;
    end;
    end;
    然后需要在DLL 项目文件中的begin?end 部分中加入代码如下: 
    begin 
        DllProc := @DLLEntryPoint; 
        DLLEntryPoint(DLL_PROCESS_ATTACH); 
    end. 
    *导出函数。 
    最后还需要在exports 语句中加入需要导出的函数,代码如下: 
    exports 
        SetShareData,GetShareData; 
    2.实现服务器端程序 
    服务器端的程序比较简单,在一个表单中加入一个输入框和一个按钮,单击按钮时将输入框中的
    字符转换为整数并写入到DLL 中,按钮的Click 事件处理代码如下,: 
    procedure TForm1.Button1Click(Sender: TObject); 
    var 
        temp:integer; 
    begin 
        try 
            temp := StrToInt(Edit1.Text); 
            SetShareData(temp);   
        except 
            ShowMessage(’请输入一个整数’); 
            Edit1.Text := ’0’;   
        end;     
    end; 
    注意:首先必须声明使用的SetShareData 方法。 
    3.实现客户端程序 
    客户端的程序也非常简单,在一个表单中加入一个输入框、一个按钮和一个定时器,单击按钮或
    者定时器到时间时从DLL 中读取共享数据,按钮的Click 事件相应代码如下: 
    procedure TForm1.Button1Click(Sender: TObject); 
    begin 
        Edit1.Text := IntToStr(GetShareData);     
    end; 
    定时器的Timer 事件处理代码如下:

    ·392·
    procedure TForm1.Timer1Timer(Sender: TObject);
    begin
    Button1Click(Sender);
    end;
    15.5 钩子函数
    钩子(Hook)是Windows 消息处理机制中的一个点,在这里应用程序可以在消息到达目标窗口过
    程之前安装一个子进程,用于检测系统中的消息通信以及处理某种类型的消息。
    注意:钩子会减慢系统速度,因为它们增加了系统必须对每个消息执行的进程的数目,所以
    只有需要时才安装钩子。
    15.5.1 钩子链
    Windows 包含许多类型的钩子,每种类型提供对不同的Windows 消息处理机制的不同方面的访问。
    例如应用程序可以使用WH_MOUSE 钩子检测鼠标消息的消息通信。
    Windows 对每一种类型的钩子包含一个独立的钩子链(Hook Chain),应用程序的回调函数调用钩
    子函数。当一个与特定钩子联系的消息发生时,Windows 首先将消息传递给消息链中的每一个引用的
    钩子函数,一个接一个地执行。钩子函数可以采取的行动依赖于钩子的类型。一些类型的钩子函数只
    能监测信息,其他一些可以修改消息或停止它们的派送过程,即钩子链不将它们传递到下一个钩子函
    数或目标窗口。
    15.5.2 钩子函数
    要利用特定类型的钩子函数,开发者要提供一个钩子函数并使用SetWindowsHookEx 函数将它安
    装到与钩子相关的钩子链中。钩子函数必须按照下列规则定义:
    TFNHookProc = function (code: Integer; wparam: WPARAM; lparam: LPARAM): LRESULT stdcall;
    code 参数是钩子函数采取的行动的钩子代码。钩子代码的值取决于钩子类型,每种类型都有它自
    己的特定的钩子代码集。wparam 和lparam 的值依赖于钩子代码,但是比较典型的是它们包含发送的
    消息的信息。
    SetWindowsHookEx 函数将在一个钩子链的起始处安装一个钩子函数。当一个特定类型的钩子监
    测的事件触发时,Windows 调用关于这个钩子链的起始处的代码。链中的每个钩子函数决定是否将事
    件传递给下一个过程。钩子函数通过调用CallNextHookEx 函数将事件传递给下一个过程。
    一些类型的钩子函数只能监测消息,在这种情况下,Windows 将消息传递给每个钩子函数,不论
    一个过程是否调用了CallNextHookEx。
    钩子函数可以是全局的监测系统中的所有消息,也可以是与线程相关的只监测单个线程的消息。
    全局钩子函数可以在任何应用程序的上下文中调用,因此必须是在一个独立的DLL 模块中。线程相关
    的钩子只在相关线程的上下文中调用。如果应用程序安装一个自己线程的钩子函数,那么钩子函数可
    以在应用程序的一个模块中或在一个DLL 中。如果应用程序为另一个应用程序安装钩子函数,钩子函
    数必须在DLL 中。
    注意:应该只是为了调试目的才使用全局钩子,否则应该避免使用。全局钩子将降低系统性
    能并引起与使用通知类型的全局钩子的应用程序冲突。
    15.5.3 钩子类型
    每种类型的钩子都是一个应用程序能监测Windwos 消息处理机制的不同方面。下面描述了
    Windows 可用的几种钩子。
    第15 章 动态链接库和组件包
    ·393·
    1.WH_CALLWNDPROC 和WH_CALLWNDPROCRET 钩子
    WH_CALLWNDPROC 和WH_CALLWNDPROCRET 钩子使应用程序可以监测通过SendMessage
    函数发送到窗口过程的消息。Windows 在将消息传递到接收的窗口过程之前调用
    WH_CALLWNDPROC 钩子,并且在窗口过程处理这个消息之后调用WH_CALLWNDPROCRET 钩子。
    WH_CALLWNDPROCRET 钩子传递一个CWPRETSTRUCT 结构体的地址给钩子函数。这个结构
    体包含处理消息的窗口过程的返回值,以及与消息关联的消息参数。
    2.WH_CBT 钩子
    Windows 在激活、创建、销毁、最小化、最大化、移动或重新设置窗口大小前,完成系统命令前,
    从系统消息队列删除鼠标或键盘事件前,设置输入焦点前或在同步系统消息队列前调用WH_CBT 钩
    子。钩子函数的返回值决定Windows 是否允许这些操作。WH_CBT 钩子主要是用于基于计算机的训
    练(computer-based training,CBT)应用程序。
    3.WH_DEBUG 钩子
    Windows 在调用系统的其他钩子关联的钩子函数之前调用WH_DEBUG 钩子。可以使用这个钩子
    决定是否允许与其他类型钩子关联的钩子函数。
    4.WH_FOREGROUNDIDLE 钩子
    WH_FOREGROUNDIDLE 允许当前台线程空闲时执行低优先级的任务。Windows 当应用程序的前
    台线程即将空闲时调用WH_FOREGROUNDIDLE 钩子函数。
    5.WH_GETMESSAGE 钩子
    WH_GETMESSAGE 钩子使应用程序能够监测关于GetMessage 或PeekMessage 函数返回的消息。
    可以使用WH_GETMESSAGE 钩子监测鼠标和键盘输入以及提交给消息队列的其他消息。
    6.WH_JOURNALPLAYBACK 钩子
    WH_JOURNALPLAYBACK 钩子使应用程序在系统消息队列中插入一个消息。可以使用这个钩子
    回放使用WH_JOURNALRECORD 钩子记录的一系列的鼠标和键盘消息。一旦安装了
    WH_JOURNALPLAYBACK 钩子,正常的鼠标和键盘输入被禁用。WH_JOURNALPLAYBACK 钩子是
    全局钩子。它不可以在与线程相关的钩子中使用。
    WH_JOURNALPLAYBACK 钩子返回一个超时值。这个值告诉系统回放钩子处理当前消息之前要
    等待的毫秒数。这使钩子可以控制它回放事件的时间。
    7.WH_JOURNALRECORD 钩子
    WH_JOURNALRECORD 钩子能够监测和记录输入事件。可以使用这个钩子记录一系列鼠标和键
    盘的事件,以待使用WH_JOURNALPLAYBACK 钩子回放。WH_JOURNALRECORD 钩子是全局钩子,
    不能在线程相关的钩子中使用。
    8.WH_KEYBOARD 钩子
    WH_KEYBOARD 钩子使应用程序可以监测关于通过GetMessage 或PeekMessage 函数返回的
    WM_KEYDOWN 和WM_KEYUP 消息的消息通信。可以使用WH_KEYBOARD 钩子监测提交给消息
    队列的键盘输入。
    9.WH_MOUSE 钩子
    WH_MOUSE 钩子能够监测GetMessage 或PeekMessage 函数返回的关于鼠标的消息。可以使用
    WH_MOUSE 监测提交给消息队列的鼠标输入。
    10.WH_MSGFILTER 和WH_SYSMSGFILTER 钩子
    WH_MSGFILTER 和WH_SYSMSGFILTER 钩子不仅能够监测即将由一个菜单、滚动条、消息框

    ·394·
    或对话框处理的消息,还可以监测当用户使用Alt+Tab 或Alt+Esc 键即将激活不同的窗口时的消息。
    WH_MSGFILTER 钩子只能监测传递给安装钩子函数的应用程序的菜单、滚动条、消息框或对话框的
    消息。WH_SYSMSGFILTER 钩子监测所有应用程序的这类消息。
    WH_MSGFILTER 和WH_SYSMSGFILTER 钩子能在模式循环时(它和主窗口消息循环是相当的)
    执行消息过滤。例如应用程序经常在从队列中检索消息和派送消息之间在主循环中检查新消息,以便
    采取适当的行动。如果应用程序安装了WH_MSGFILTER 或WH_SYSMSGFILTER 钩子函数,系统可
    以在模式循环时调用这个过程。
    应用程序可以直接通过调用CallMsgFilter 函数调用WH_MSGFILTER 钩子。通过使用这个函数,
    应用程序可以在模式循环中使用与主循环中相同的代码。为了达到这个目的,需要在WH_MSGFILTER
    钩子函数中封装过滤操作并且在调用GetMessage 和DispatchMessage 之间调用CallMsgFilter。例如下
    面的VC 代码:
    while (GetMessage(@msg, (HWND) NIL, 0, 0))
    begin
    if ( not CallMsgFilter(@qmsg, 0))
    DispatchMessage(@qmsg);
    end;
    CallMsgFilte 的最后一个参数简单地传递给钩子函数, 钩子函数通过定义一个类似
    MSGF_MAINLOOP 的常量,然后由这个值决定从哪里调用过程。
    11.WH_SHELL 钩子
    一个Shell 应用程序可以使用WH_SHELL 钩子接收重要的通知。当一个Shell 应用程序即将被激
    活以及一个顶层窗口被创建或销毁时,Windows 调用WH_SHELL 钩子函数。
    15.5.4 与使用钩子相关的函数
    1.SetWindowsHookEx 函数
    可以通过调用SetWindowsHookEx 函数并指定钩子调用过程的类型以及一个过程入口点的指针安
    装钩子,而不论过程是与所有的线程关联还是与特定线程关联。SetWindowsHookEx 函数在Windows
    单元中声明如下:
    function SetWindowsHookEx(
    idHook: Integer;
    lpfn: TFNHookProc;
    hmod: HINST;
    dwThreadId: DWORD): HHOOK; stdcall;
    idHook 指明需要安装的钩子类型,它的取值为钩子类型一节中所描述的常量。
    lpfn 指向一个钩子函数的实例。如果一个不属于调用函数SetWindowHoodEx 的进程的线程要安装
    钩子,那么参数lpfn 必须指向一个DLL 中的钩子函数实例;否则,它可以指向一个属于当前进程的
    钩子函数实例。
    dwThreadId 指明安装钩子的线程。钩子函数在这个线程上下文中被调用。如果参数dwThreadId
    为0,那么安装系统钩子,否则安装的就是线程相关的钩子。
    hmod 指明包含lpfn 的模块句柄。如果参数dwThreadId 指定的是一个不属于调用函数
    SetWindowHookEx 的进程的线程,那么参数hmod 必须不为nil,表示包含这个钩子函数的DLL 模块
    句柄,否则的话,参数hmod 可以为nil,并且钩子函数在当前进程的代码中。
    SetWindowsHookEx 把一个应用程序定义的钩子函数安装到钩子链中。当系统中一些特定的事件
    发生时,这些钩子函数将被调用。如果函数执行时发生了错误,那么返回值是NIL。如果参数hmod
    为NIL,并且参数dwThreadId 是另一个进程的线程或者是0,那么将会产生错误。如果函数执行没有
    第15 章 动态链接库和组件包
    ·395·
    出错,那么将返回一个钩子函数的句柄,它将被作为这个特定的钩子函数的标识符。它可以作为参数
    被函数UnhookWindowsHook 和CallNextHookProc 所使用。
    注意:必须将一个全局钩子函数放置在与安装钩子函数的应用程序分开的一个DLL 中。在
    安装钩子函数之前,安装的应用程序必须拥有DLL 模块的句柄。
    2.UnhookWindowsHookEx 函数
    可以通过调用UnhookWindowsHookEx 函数释放一个线程相关的钩子函数(将它的地址从钩子链
    中删除),同时需要指明钩子函数的句柄。UnhookWindowsHookEx 的声明如下:
    function UnhookWindowsHookEx(hhk: HHOOK): BOOL; stdcall;
    参数hhk 是使用SetWindowsHookEx 安装的钩子句柄。
    一旦不需要一个钩子函数就要立即释放,但是这个函数不释放包含在钩子函数中的DLL。这是因
    为全局钩子函数在系统的每个应用程序的进程的上下文中调用,引起这些进程的LoadLibrary 函数的
    隐含调用。因为FreeLibrary 函数的调用不能作用于其他进程,因此没有其他办法释放DLL。在所有显
    式的链接到DLL 的进程终止或调用FreeLibrary 之后,或所有调用钩子函数的进程在DLL 外部重启之
    后Windows 最终释放DLL。
    安装全局钩子函数的另一个方法是在DLL 中与钩子函数一起提供一个安装函数。使用这个方法,
    安装的应用程序不需要DLL 模块的句柄。通过与DLL 链接,应用程序可以访问安装函数。安装函数
    可以提供DLL 句柄和调用SetWindowsHookEx 的其他细节。DLL 也可以包含一个释放全局钩子函数的
    函数,应用程序终止时可以调用这个释放钩子的函数。
    3.CallNextHookEx 函数
    通常情况下,钩子函数应该通过调用CallNextHookEx 函数将事件传递给钩子链中的下一个过程。
    CallNextHookEx 的声明如下:
    function CallNextHookEx(
    hhk: HHOOK;
    nCode: Integer;
    wParam: WPARAM;
    lParam: LPARAM): LRESULT; stdcall;
    *hhk 指明当前钩子的句柄。
    *nCode 指明传递给当前钩子函数的钩子类型。
    *wParam 和lParam 参数的含义与SetWindowsHookEx 函数中的类似。
    15.5.5 使用钩子函数监测所有鼠标动作的实例
    本节通过一个实例讲解钩子函数的使用,主要是记录系统中所有的鼠标动作。因为要监测系统所
    有的鼠标动作,所以必须将钩子过程放在一个DLL 中。DLL 中实现了安装和释放鼠标钩子(WH_mouse)
    的接口,同时申请了内存映射文件用以记录当前鼠标的信息,钩子函数的处理主要是向应用程序发送
    一个鼠标信息的消息。应用程序接收到钩子函数的消息之后,在一个Memo 控件中增加一行,显示鼠
    标的动作。
    1.创建DLL
    首先建立一个DLL 工程,命名为GetKey.dpr,然后在工程中加入一个单元,命名为HookMain。
    在DLL 中必须声明钩子函数的句柄,由于需要使用内存映射文件,因此必须加入与内存映射文件
    的变量声明:
    const
    VirtualFileName = ’ShareDllData’;
    DataSize = sizeof (TShareData);

    ·396·
    var
    hMapFile:THandle; //内存映射文件句柄
    ShareData:^TShareData; //共享数据指针
    InstalledHook:HHook; //安装的钩子句柄
    然后需要提供安装和取消钩子的接口函数。安装钩子的函数主要是通过调用SetWindowsHookEx
    实现,它将HookHandler 函数安装到钩子链中,并记录安装钩子的进程ID 和需要发送的消息的ID:
    function OpenGetKeyHook(sender:HWND;MessageID:WORD):BOOL;stdCall;export;
    begin
    Result:=False;
    if InstalledHook = 0 then {没有安装钩子}
    begin
    ShareData^.data1[1]:=sender;
    ShareData^.data1[2]:=MessageID ;
    InstalledHook := SetWindowsHookEx(WH_mouse,HookHandler,HInstance,0);
    Result:=InstalledHook<>0;
    end;
    end;
    取消钩子是通过调用UnhookWindowshookEx 函数来实现的:
    function CloseGetKeyHook:BOOL;stdCall;export;
    begin
    if InstalledHook<>0 then {钩子已经安装}
    begin
    UnhookWindowshookEx(InstalledHook); {将钩子从钩子链接除}
    InstalledHook:=0;
    end;
    Result:=InstalledHook=0;
    end;
    HookHandler 钩子函数的处理过程主要是向应用程序发送自定义的鼠标消息:
    function HookHandler(iCode:Integer;wParam:WPARAM;lParam:LPARAM):LRESULT;stdcall;export;
    begin
    ShareData^.data2:=pMOUSEHOOKSTRUCT(lparam)^; {将钩子消息数据保存到映射内存文件}
    SendMessage(ShareData^.data1[1],
    ShareData^.data1[2],wParam,0);{向主窗口发送鼠标消息}
    Result:=CallNextHookEx(InstalledHook,iCode,wParam,lParam); {调用钩子链中下一个过程}
    end;
    至此,钩子函数相关的过程都已经实现,另外还需要在装载DLL 时申请内存映射文件,卸载时释
    放。这可以在HookMain 的initialization 和finalization 中添加代码实现:
    initialization
    InstalledHook:=0;
    hMapFile:= CreateFileMapping ($FFFFFFFF,nil,
    Page_ReadWrite, 0, DataSize, VirtualFileName);{建立内存映射文件}
    if hMapFile = 0 then {处理错误}
    Raise Exception.Create(’创建公用数据的Buffer 不成功!’);
    ShareData := MapViewOfFile ( hMapFile, File_Map_Write,
    0, 0, DataSize); {获得内存映射文件的句柄}
    第15 章 动态链接库和组件包
    ·397·
    finalization
    UnMapViewOfFile(ShareData); {释放内存映射文件}
    CloseHandle (hMapFile);
    为了让应用程序获得鼠标的坐标,还需要提供两个接口函数:
    function GetX:integer;stdcall;export;
    begin
    result := ShareData^.data2.pt.X; {返回鼠标位置的x 坐标}
    end;
    function GetY:integer;stdcall;export;
    begin
    result := ShareData^.data2.pt.Y ; {返回鼠标位置的y 坐标}
    end;
    2.测试钩子函数的主程序
    在主程序中加入3 个按钮,Caption 属性分别设为“开始捕获”、“停止捕获”和“退出”,以及一
    个Memo 控件。
    单击“开始捕获”按钮则程序开始捕获系统所有的鼠标动作,它的Click 事件处理代码如下:
    procedure TForm1.Button1Click(Sender: TObject);
    begin
    if OpenGetKeyHook(Form1.Handle,MessageID) then {挂接钩子过程}
    Memo1.Lines.Add(’开始记录’);
    end;
    单击“停止捕获”按钮则程序停止捕获系统的鼠标动作,它的Click 事件处理代码如下:
    procedure TForm1.Button2Click(Sender: TObject);
    begin
    if CloseGetKeyHook then {解除钩子过程挂接}
    begin
    Memo1.Lines.Add(’结束记录’);
    Memo1.Lines.Add(’ ’);
    end;
    end;
    开始捕获系统鼠标动作之后,则钩子函数会向应用程序发送自定义的鼠标消息,因此在主程序中
    关键是消息处理过程,在处理消息过程中首先需要判断是不是处理鼠标消息,如果是则根据消息的附
    加信息在memo 控件中显示不同的信息。代码如下:
    procedure TForm1.WndProc(var Message:TMessage);
    begin
    if Message.Msg=MessageID then
    begin
    with Memo1.Lines do
    case Message.WParam of
    WM_LBUTTONDOWN:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)按下左键!’);

    ·398·
    end;
    WM_LBUTTONUP:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)释放左键!’);
    end;
    WM_LBUTTONDBLCLK:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)双击左键!’);
    end;
    WM_RBUTTONDOWN:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)按下右键!’);
    end;
    WM_RBUTTONUP:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)释放右键!’);
    end;
    WM_RBUTTONDBLCLK:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)双击左键!’);
    end;
    WM_MBUTTONDOWN:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)按下中键!’);
    end;
    WM_MBUTTONUP:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)释放中键!’);
    end;
    WM_MBUTTONDBLCLK:
    begin
    Add(’在(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)双击中键!’);
    end;
    WM_NCMouseMove,WM_MOUSEMOVE:
    begin
    Add(’鼠标移动到(’+ IntToStr(GetX) + ’,’+ IntToStr(GetY ) + ’)!’);
    end;
    end;
    end;
    Inherited;
    end;
    15.6 包和DLL
    包(Package)是应用程序以及IDE 使用的一个特殊的DLL。包有两种:运行时包(Runtime Package)
    和设计时包(Design Time Packages)。运行时包为应用程序提供功能;设计时包用于安装组件到IDE
    第15 章 动态链接库和组件包
    ·399·
    中以及为定制组件创建特殊属性。一个单独的包在设计时和运行时都可以使用,设计时包经常通过它
    的requires 子句引用运行时包工作。为了将它们与其他DLL 区别,包库保存在以bpl(Borland Package
    Library)为扩展名的文件中。
    通常,包在应用程序启动时被静态加载。但可以用SysUtils 单元中的LoadPackage 和UnloadPackage
    函数和过程来实现包的动态加载和卸载。
    注意:当一个应用程序使用包时,封装在包中的每个单元的单元名仍必须出现在相关源文件
    的uses 子句中。
    与其他DLL 相似,包含代码的包可以在应用程序之间共享。例如最常使用的VCL 组件就驻留在
    一个叫作VCL 的包中。每次创建一个新的默认VCL 应用程序,它自动使用VCL。当编译使用这种方
    法创建的应用程序时,应用程序的可执行镜像只包含与它不同的代码和数据。
    Delphi 本身有几个运行时包封装了VCL 和CLX 组件,同时几个设计时包在IDE 中操作组件。通
    用代码保存在vcl70.bpl 运行时包中。具有几个使用这个包的应用程序的计算机,只需安装vcl70.bpl
    的一个拷贝,即可被所有应用程序和IDE 自身共享。
    创建应用程序时可以选择是否使用包。然而,如果需要在IDE 中加入自定义组件,就必须将它们
    作为设计时包安装。
    可以创建在应用程序之间共享的运行时包。如果编写Delphi 组件,需要在安装它们之前编译为设
    计时包。
    15.6.1 为什么使用包
    首先,设计时包简化了发布和安装定制组件的任务。
    而相对于传统程序,运行时包有如下优点:通过将可重用代码编译到一个运行时包,可以实现在
    应用程序之间共享。例如所有应用程序(包括Delphi 本身)可以通过包访问标准组件。应用程序没有
    将组件库的独立地拷贝绑入它们的可执行程序中,可执行程序较小,减少了系统资源和硬盘存储消耗。
    并且包可以使编译地更快,因为只有应用程序独有的代码编译入组件中。
    15.6.2 与包有关的文件类型
    与包有关的文件类型如表15-6 所示。
    表15-6 与包有关的文件类型
    扩展名 含义
    bpl
    运行时包。这是一个具有Delphi 特征的Windows DLL 文件。*.bpl、*.dpk 或*.dpksource 的文件名必
    须相同
    dcp
    包含包头以及包中所有*.dcu 文件的联合的二进制镜像,包含所有编译器要求的符号信息。一个包有
    且仅有一个.dcp 文件。.dcp 与*.dpk 文件名一样。必须有一个.dcp 文件才能使用包构建应用程序
    dcu 和pas 包含在包中的每个单元文件及其二进制镜像。需要时系统为每个单元文件创建一个.dcu 文件
    dpk 和dpkw 列出包中包含的单元文件的列表。.dpk 与.dpkw 相同,但是开发跨平台应用程序时使用dpkw 扩展名
    可以在一个包中包含VCL 和CLX 组件,包意味着跨平台时只能包含CLX 组件。
    注意:包与应用程序中的其他模块共享全局数据。
    15.6.3 运行时包
    运行时包配置在应用程序中,当用户运行应用程序时它们提供功能。
    要运行使用包的应用程序,必须有应用程序的可执行文件和应用程序使用的包文件( .bpl 文
    件)。.bpl 文件必须在使用它们的应用程序的系统路径中。当配置应用程序时,必须保证用户已经有了

    ·400·
    要求的.bpl 的正确版本。
    1.在应用程序中装载包
    当动态装载包时,可以使用如下两种方法:
    *在IDE 菜单中选择“Project”*“Options”对话框。
    *使用LoadPackage 函数。
    (1)使用“Project|Options”对话框装载包
    使用“Project|Options”对话框装载包的步骤如下:
    *在IDE 中打开或创建一个工程。
    *在IDE 菜单中选择“Project”*“Options”启动项目选项对话框。
    *选择Packages 页,如图15-1 所示。
    图15-1 项目选项对话框Package 页
    *选择Build with Runtime Packages 单选框,并在下面的编辑框中输入一个或多个包名。每个包
    只会在需要时隐含装载(也就是说,当引用在那个包的单元中定义的一个对象时,此时与安装的设计
    时包相关联的运行时包已经列出在编辑框中)。
    *要在现存列表中加入一个包,单击“Add?”按钮并在Add Runtime Package 对话框中输入新包
    的名称。要浏览可使用的包列表,单击“Add”按钮,然后单击Add Runtime Package 对话框中紧接着
    Package Name 编辑框的“Browse?”按钮。在Add Runtime Package 对话框中编辑Search Path 编辑框,
    可以改变全局库路径。 
    在包名中不需要包含文件扩展名(或代表Delphi 发布的版本数),也就是说,在VCL 应用程序中
    vcl70.bpl 可以写为vcl。如果直接在Runtime Package 编辑框中输入多个名称则以“;”分隔,例如: 
    rtl;vcl;vcldb;vclado;vclx;vclbde; 
    Runtime  Packages 编辑框中列出的包是编译时自动链接到应用程序的。重复的包名会被忽略。如
    果未选中Build with runtime packages,则应用程序不使用包编译。 
    运行时包的选择只是对当前项目有效。要使当前的选择作为新项目的默认值,则应在对话框底部
    选择Defaults 单选框。 
    注意:当使用包创建应用程序时,必须在源文件的uses 语句中包含原始的Delphi 单元名称。 
    例如主表单源文件程序如下: 
    unit MainForm; 
    interface 
    uses 
    第15 章 动态链接库和组件包
    ·401·
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs; //在CLX 应用程序中一些单元可能不同
    在这个VCL 例子中,引用的单元都包含在vcl 和rtl 包中。在应用程序中要使用vcl 和rtl,则在
    uses 语句中必须存在这些引用,否则将产生编译错误。在表单设计器生成的源文件中,uses 语句中将
    自动加入这些单元。
    (2)使用LoadPackage 函数装载包
    可以通过调用LoadPackage 函数在运行时装载包。LoadPackage 装载在它的名称参数中指定的包、
    检查重复的单元以及调用包中包含的所有单元的初始化块。例如下面代码将在当一个文件被选中时执
    行:
    with OpenDialog1 do
    if Execute then
    with PackageList.Items do
    AddObject(FileName, Pointer(LoadPackage(FileName)));
    要动态卸载一个包,可以调用UnloadPackage。但是在销毁一个包中定义的类的实例以及解除它
    的类的注册时必须十分小心。
    2.决定使用哪个运行时包
    几个运行时包,包括rtl 和vcl(VCL 应用程序)或visualclx(CLX 应用程序),提供了基本的语
    言和组件支持。vcl 或visualclx 包含最常用的组件,rtl 包含所有非组件的系统函数和Windows 接口元
    素。它不包含数据库和其他特殊的组件(它们可以在单独的包中获得)。
    要创建一个使用包的C/S 数据库应用程序,需要几个运行时包,包括vcl、vcldb、rtl 和dbrtl(VCL)
    或visualclx 和visualdbclx(CLX)。要使用这些包,在IDE 菜单项中选择“Project”*“Options”,再
    选择Packages 页,保证下列包包含在Runtime Packages 编辑框中。对于Web 服务器应用程序,需要
    netclx 以及baseclx,可能还需要visualclx。
    注意:在VCL 应用程序中,不需要手动包含vcl 和rtl,因为它们在vcldb 的Requires 语句中
    已经引用。不论在Runtime Packages 编辑框中是否包含vcl 和rtl,应用程序编译是一
    样的。
    可以决定应用程序需要调用哪个包的方法是运行包,然后检查事件日志(在IDE 菜单中选择
    “View”*“Debug Windows”*“Event Log”)。事件日志显示了包含所有包的装载的每一个模块,
    并且显示完整的包名。例如对于vcl70.bpl,在VCL 应用程序中可能会看到与下面相似的一行:
    Module Load: vcl70.bpl Has Debug Info. Base Address $400B0000. Process Project1.exe ($22C)
    3.定制包
    一个定制的包可以是一个自己编码的bpl 文件或从第三方供应商得到的现成的包。要在应用程序
    中使用一个定制的运行时包,在IDE 菜单中选择“Project”*“Options”并在Packages 页的Runtime
    Packages 编辑框中加入包名。
    例如有一个stats.bpl 统计包,要在应用程序中使用,则在Runtime Packages 编辑框中输入类似下
    面的行:
    vcl;rtl;vcldb;stats //在VCL 应用程序中
    rtl:visualclx:visualdbclx:stats //在CLX 应用程序中
    如果创建了自己的包,可以根据需要在列表中加入。
    15.6.4 设计时包
    设计时包用于在IDE 组件面板中安装组件以及为定制组件创建特定的属性编辑器。安装哪一个包

    ·402·
    取决于使用的是Delphi 的哪个版本以及是否定制了它。可以在IDE 菜单中选择“Component”*“Install
    Packages”浏览已经安装的包的列表。
    设计时包调用在它们的Requires 语句中引用的运行时包工作。例如dclstd 引用vcl。dclstd 本身包
    含附加的功能以使标准组件可以从组件面板选取。
    除了以前安装的包,可以安装自己的组件包或来自第三方开发商的组件包到IDE 中。设计dclusr
    时包将被用于作为新组件的默认容器。
    所有的组件都是作为包安装到IDE 中。如果编写了自己的组件,就需要创建和编译一个包用来包
    含它们。
    安装或卸载自己的或来自第三方供应商的组件可按照以下步骤。
    (1)如果安装一个新组件,将包文件拷贝或移动到本地目录(如果包使用.bpl、.dcp 和.dcu 文件
    发行,必须保证拷贝所有的文件)。
    保存.dcp 和.dcu 文件(如果包含在发布包中)的目录必须在Delphi 库路径中。
    如果包是以一个.dpc(Package Collection,包集合)文件的形式发行,那么只需要拷贝一个包含
    了其他文件的dpc 文件。
    (2)从IDE 菜单中选择“Component”*“Install Packages”或选择“Project”*“Options”并
    单击Packages 页,显示如图15-1 所示的对话框。所有可用的包列表显示在Design packages 列表框中。
    要安装一个包到IDE 中,选择在包名前的单选框;要卸载一个包,就不选择这个单选框。
    要查看包含在一个已经安装包的组件列表时,选择包并单击“Components”按钮。
    要将一个包加入列表,单击“Add?”按钮并在Add Design Package 对话框中查找.bpl 文件所在的
    目录。选择.bpl 或.dpc 文件并单击“Open”按钮。如果选择.dpc 文件,将会显示一个新的对话框以处
    理从包集合中抽取.bpl 和其他文件。 
    要从列表中删除一个包,选择包并单击“Remove”按钮。 
    (3)单击“OK”按钮。 
    包中的组件被安装到组件面板上RegisterComponents 过程指定的页中, 显示的名称也在
    RegisterComponents 过程中指定。 
    新的项目与所有安装的可用的包一起创建,除非改变了默认设置。要将当前的安装选择作为新项
    目的默认选择,在如图15-1 所示的对话框的底部选择Default 单选框。 
    要从组件面板删除一个组件但是不卸载包,从IDE 菜单中选择“Component”*“Configure Palette”
    或选择“Tools”*“Environment  Options”并选择Palette 页,如图15-2 所示。Palette 页列出了每个
    标签页以及包含组件的名称。选择任意组件并单击“Hide”按钮即从组件面板删除组件。 
     
    图15-2 组件面板设置页
    第15 章 动态链接库和组件包
    ·403·
    15.6.5 创建和编辑包
    创建一个包需要指定如下内容。
    *包的名称;
    *新包需要或要链接到新包的或其他包的列表;
    *包编译时包含或绑定的单元文件的列表。包基本上是这些源代码单元的封装。Contains 语句指
    明希望编译入包中的定制组件的源代码单元。
    包编辑器产生一个包源文件(*.dpk)。
    1.创建一个包
    创建一个包的步骤如下。
    *从IDE 菜单选择“File”*“New”*“Other”,选择Package 图标并单击“OK”按钮。产生
    的包显示在包编辑器中,如图15-3 所示。包编辑器为新包显示一个Requires 和一个Contains 节点。
    图15-3 包编辑器
    *要将一个单元加入contains 语句,选择contains 节点,单击包编辑器的“Add”按钮,在Add Unit
    页的Unit file name 编辑框中输入一个.pas 文件名,或者单击“Browse?”按钮查找文件,然后单击“OK”
    按钮。选择的单元将在包编辑器的Contains 节点下显示。可以重复这个步骤加入新单元。 
    *要在requires 子句中加入包,选择requires 节点,单击“Add”按钮,在Requires 页的Package name
    编辑框输入.dcp 文件名,或单击“Browse”浏览文件,然后单击“OK”按钮。选择的单元将在包编辑
    器的Requires 节点下显示。可以重复这个步骤加入新单元。 
    *单击“Options”按钮,弹出如图15-4 所示的包选项对话框,可以在其中决定构建需要类型的
    包。 
     
    图15-4 包选项对话框
    要创建一个设计时包(不能在运行时使用的包),需选择Designtime  only 复选按钮(或在.dpk 文

    ·404·
    件中加入{$DESIGNONLY}编译器指令)。要创建一个运行时包(不能安装的包),则需选择Runtime only
    复选按钮(或在.dpk 文件中加入{$RUNONLY}编译器指令)。要创建可以在运行时也可以在设计时使
    用的包,就要同时选择Designtime and runtime 复选按钮。
    *在包编辑器中单击“Compile”按钮,编译包。
    注意:不能单击“Install”按钮强制make。
    在编写跨平台应用程序时不要在包文件中(.dpk 文件)使用IFDEF。但是可以在源代码中使
    用。
    2.编辑已经存在的包
    可以用几个方法打开已经存在的包并进行编辑。
    *从IDE 菜单中选择“File”*“Open”(或“File”*“Reopen”)并选择一个.dpk 文件。
    *从IDE 菜单中选择“Component”*“Install Packages”,从Design packages 列表中选择一个包
    并单击“Edit”按钮。
    *当包编辑器打开时,从Requires 节点中选择一个包,单击鼠标右键并选择“Open”项。
    要编辑一个包的描述或设置使用选项,单击包编辑器(如图15-3 所示)中的“Options”按钮,
    打开包选项对话框,并选择Description 标签页,如图15-4 所示。
    在对话框左下角有一个Default 单选框,当选中了这个单选框并单击“OK”按钮时,选择的选项
    将作为新项目的默认设置。要恢复原始的默认值可以删除或重命名defproj.dof 文件。
    3.包文件的结构
    每个包在单独的源文件中声明,它们应保存为.dpk 文件。包的源代码不能包含任何类型、数据或
    函数声明。包能包含:
    *包的名称。
    *在requires 语句中列出新包需要利用的包,这些包将被连接到新的包。
    *当包被编译时,列于contains 语句中的单元将作为被包含的单元联编到包中。
    包的实质就是这些源代码单元的特殊封装器,这些单元提供编译过的包的功能。
    包的声明如下:
    package packageName;
    requires Clause;
    contains Clause;
    end.
    这里的packageName 是一个有效的标识符。requires Clause 和contains Clause 都是可选的。
    (1)包的名称
    包名在一个项目中必须是惟一的。如果命名了一个包为Stats,包编辑器产生一个Stats.dpk 的源文
    件,编译器产生一个名称为Stats.bpl 的可执行文件和Stats.dcp 的二进制映像文件。在其他包的requires
    语句中或在应用程序中调用时使用Stats 引用这个包。
    也可以在包名中加入前缀、后缀和版本号。当打开包编辑器(如图15-3 所示)时,单击“Options”
    按钮,在Project Options 对话框的Description 的标签页中(如图15-4 所示)输入LIB Suffix、LIB Prefix
    或LIB Version 的值或文本。例如要在包项目中加入版本号,在LIB Version 后面输入7 则Package1 产
    生Package1.bpl.7。
    (2)Requires 语句
    Requires 语句指定在当前包中使用的其他或外部包,其作用相当于单元文件中的uses 子句。列于
    Requires 语句中的外部包自动在编译时被连接到所有应用程序中,前提是该应用程序既使用了当前包,
    又使用了包含在外部包中的某个单元。
    第15 章 动态链接库和组件包
    ·405·
    Requires 语句由Requires、随后的逗号隔开的包名称列表以及末尾分号组成。如果一个包不需要
    引用其他任何包,那么它不需要Requires 子句。
    如果包含在包中的单元文件引用了其他包的单元,那么其他的包应被包括在第1 个包的Requires
    子句中。如果没有被包括,那么编译器将从相应的.dcu(Windows)或.dpu(Linux)文件中加载被引
    用的单元。
    注意:创建的大部分包要求rtl。如果使用VCL 组件,也将需要包含vcl 包。如果使用CLX
    组件,则需要包含visualclx。
    在requires 语句中不能包含循环引用。这意味着:
    *一个包不能在自己的requires 语句中引用自己;
    *引用链必须在链中没有再次引用任何包时终止。如果包A 要求包B,那么包B 不能要求包A;
    如果包A 要求包B,包B 要求包C,那么包C 不能要求包A。
    一个包的requires 语句的重复引用(或运行时包编辑器)将会被编译器忽略。为了程序的清晰可
    读性,最好删除重复的包引用。
    (3)Contains 语句
    Contains 语句识别绑定在包中的单元文件。如果正在编写包,那么需要把源代码放入一个.pas 文
    件中然后将它们放在Contains 语句中。在Contains 子句中不能包括扩展文件名。
    Contains 子句由Contains、随后的逗号隔开的单元文件名以及末尾分号组成。其中,任何单元名
    都可以跟随保留字in 及一个源文件名,可以有也可以没有路径名,并且文件名以单引号括起来,路径
    名(如果有)可以是绝对路径,也可以是相对路径。例如:
    contains MyUnit in ’C:MyProjectMyUnit.pas’;
    使用Contains 语句时应避免冗余,需要注意下列问题。
    *一个包不能出现在另一个包的Contains 语句或单元的uses 子句中。
    *直接包含在一个包中的Contains 语句中的单元或间接包含在这些单元的单元都会在编译时绑定
    到包中。直接或间接包含于包中的这些单元,不能被包含在当前包中Requires 语句引用到任何包中。
    *在同一个应用程序使用的包中,一个单元最多只能被直接或间接包含一次,包括IDE。这意味
    着如果创建包含vcl(VCL)或Visual clx(CLX)单元中的任一个包,就不能将包安装到IDE 中。要
    使用其他包中已经包含的单元,将第1 个包放在第2 个包的requires 语句中。
    4.手工编辑包源文件
    包源文件与项目文件类似,都是根据提供的信息由Delphi 生成的。它们也可以手动编辑。包源文
    件应该保存为.dpk(Delphi Package)文件,以避免与包含其他Delphi 源代码的文件混淆。
    在代码编辑器中打开包源文件的步骤如下:
    *在包编辑器中打开包。
    *在包编辑器中单击鼠标右键并选择“View Source”。
    在一个包中,包头指明包的名称。
    Requires 语句列出了当前包使用的其他的外部包。如果一个包没有使用包含在其他包中任何单元
    的单元,则不需要Requires 语句。
    Contains 语句识别单元文件并将它们绑定到包中。如果一个单元被包含的单元引用,但没有列出
    在Contains 语句中,则该单元也将绑定到包中并且编译器将给出一个警告信息。
    例如下面VCL 代码声明了一个vcldb 包(在源文件vcldb70.bpl 中):
    package MyPack;
    {$R *.res}
    ...{删除了一些编译器指令}
    requires

    ·406·
    rtl,
    vcl;
    contains
    Db,
    NewComponent1 in ’NewComponent1.pas’;
    end.
    5.编译包
    通常,包是在IDE 中用.dpk 文件编译得到,而.dpk 文件由包编辑器产生。也可以直接使用命令行
    方式编译.dpk 文件。当在一个工程中包含一个包时,该包被隐含重编译(若需要)。
    从IDE 中编译一个包的基本步骤如下:
    *选择“File|Open”并选择一个包(.dpk 或.dpkw)。
    *单击“Open”按钮。
    *当包编辑器打开时,可以有两种方法进行编译,单击包编辑器的”Compile”按钮或在IDE 菜
    单中选择“Project”*“Build”菜单项。
    也可以从IDE 菜单中选择“File”*“New”*“Other”,然后选中Package 图标,单击“Install”
    按钮创建一个包项目,用鼠标右键单击包项目可以看到安装、编译或构建的选项。
    编译时可以在包源代码中加入编译器指令。
    (1)包特定编译器开关
    表15-7 所示为可以插入到源代码中的包特定的编译器指令。
    表15-7 包特定的编译器指令
    指令 目的
    {$IMPLICITBUILD OFF}
    不允许包以后被隐含地重新编译。当包提供底层的功能,不会经常变化并且不发
    布源代码时,可以在一个dpk 文件中使用
    {$G-}或{IMPORTEDDATA OFF}
    禁止创建导入数据的引用。这个指令增加内存访问的效率,但是不允许单元引用
    其他包的变量
    {$WEAKPACKAGEUNIT ON} 包单元是“弱的”,参见弱包一节
    {$DENYPACKAGEUNIT ON} 不允许将单元放置到一个包中
    {$DESIGNONLY ON} 编译包只用于安装到IDE 中(放到.dpk 文件中)
    {$RUNONLY ON} 编译运行时包(放到.dpk 文件中)
    在源代码中包含{$DENYPACKAGEUNIT ON} 能防止单元文件被编译到包中, 包含{$G?}或
    {$IMPORTEDDATA  OFF}能禁止一个包与其他包用在同一个应用程序中。使用{$DESIGNONLY  ON}
    指令编译的包不应该像通常一样用到应用程序中,因为它们包含IDE 要求的额外代码。其他的适当的
    编译器指令也可以在包源代码中使用。 
    (2)弱包 
    $WEAKPACKAGEUNIT 指令影响一个DCU 文件存储在一个包的dcp 和bpl 文件的方式。如果在
    单元文件中使用{$WEAKPACKAGEUNIT  ON},编译器在可能的情况下从bpl 中忽略单元并应其他应
    用程序和包要求创建一个单元的非包的本地拷贝。使用这个指令编译的一个单元被叫作“弱包”。 
    例如创建一个叫作pack1 的包,它只包含一个单元unit1。假设unit1 不使用其他任何单元但它调
    用rare.dll 。当编译包时, 如果在unit1.pas ( Delphi ) 或unit1.cpp ( Visual  C++ ) 中使用
    {$WEAKPACKAGEUNIT ON}指令,unit1 将不会包含在pack1.bpl 中,而必须与pack1 一块发布rare.dll
    的拷贝。然而unit1 将仍被包含在pack1.dcp 中。如果unit1 被使用pack1 的其他应用程序或包引用,
    它将从pack1.dcp 中拷贝并且直接编译到项目中。 
    第15 章 动态链接库和组件包
    ·407·
    现在假设加入了第2 个单元unit2 到pack1 中。假设unit2 使用unit1。这一次即使在unit1.pas 中
    使用{$WEAKPACKAGEUNIT ON}编译pack1,编译器在pack1.bpl 中包含unit1,但是其他引用unit1
    的包和应用程序仍将使用从pack1.dcp 得到的(非包)拷贝。
    注意:包含{$WEAKPACKAGEUNIT ON}指令的单元文件必须没有全局变量、initialization
    或finalization 节。
    {$WEAKPACKAGEUNIT ON}指令对于向其他程序员发布自己的包的开发者是一个高级特性。它
    可以帮助避免分发不经常使用的DLL 以及减少依赖于同一个外部库的包之间的冲突。
    例如PenWin 单元引用PenWin.dll。大部分项目不使用PenWin 并且大部分计算机没有安装
    PenWin.dll。因为这个原因,PenWin 单元在vcl 中是一个弱包。当编译使用PenWin 和vcl 包的项目时,
    PenWin 从vcl70.dcp 拷贝并且直接绑定到项目中,结果可执行程序是静态调用PenWin.dll。
    如果PenWin 不是弱包,将会产生两个问题。首先,vcl 本身会静态调用PenWin.dll,因此不能在
    没有安装PenWin.dll 的计算机上装载它。其次,如果试图创建一个包含PenWin 的包,PenWin 单元将
    会包含在vcl 和新包中从而引起编译器错误。因此,如果没有弱包,PenWin 就不能包含在标准的vcl
    发布版本中。
    (3)从命令行编译和连接
    当从命令行编译时,可以使用如表15-8 所示的包相关命令行编译器开关。
    表15-8 包相关命令行编译器开关
    开关 目的
    -$G- 禁止创建导入数据的引用。这个指令增加内存访问的效率,但是不允许单元引用其他包的变量
    -LEpath 指定包文件(.bpl)放置的目录
    -LNpath 指定包文件(.dcp)放置的目录
    -LUpackage 使用包
    -Z
    禁止包以后被隐含地重新编译。当一个编译包提供底层的功能,不会经常变化并且不发布源代码时,
    在一个.dpk 文件中使用
    其他的适当的命令行编译器开关也可以在编译包时使用。
    (4)编译时创建的包文件
    创建一个包,必须创建一个具有dpk 扩展名的源文件。.dpk 文件的名称成为编译器产生的文件的
    名称。例如编译一个叫作traypak.dpk 的包源文件,则编译器创建一个叫作traypak.bpl 的包。
    在Windows 中成功编译包之后产生的文件表如15-9 所示。这些产生的文件都默认放置在
    “Tools/Environment Options”对话框的Library 页指定的目录中。
    表15-9 在Windows 中成功编译包之后产生的文件
    扩展文件名 内容
    dcp
    二进制镜象文件,包含了包的首部以及在包中的所有.dcu(Windows)或.dpu(Linux)文件。每个包
    都会产生一个相应的.dcp 文件,主文件名与dpk 源文件的主文件名相同
    dcu
    包含在包中的所有单元文件的二进制镜象文件。如果有必要,那么编译将为每个单元文件创建一个相
    应的.dcu 文件
    bpl 运行时包。该文件是具有Borland 独有特性的特殊共享库,主文件名与.dpk 源文件的主文件名相同
    15.6.6 配置包
    配置包和配置其他应用程序是非常类似的。与一个配置包共同发布的文件可以是不同的。但.bpl
    文件和.bpl 文件要求的其他包或.dll 必须发布。

    ·408·
    1.配置使用包的应用程序
    当发布使用运行时包的应用程序时,应保证用户有应用程序的.exe 文件以及应用程序调用的所有
    库文件(.bpl 或者.dll)。如果库文件在与EXE 文件不同的目录下,它们必须可以通过用户的路径访问,
    可以根据惯例将库文件放在WindowsSystem 目录下。如果使用InstallShield Express,那么在重新安装
    之前,安装脚本将会在用户系统中检测它要求的包。
    2.将包发布给其他开发者
    如果发布运行时包或设计时包给其他Delphi 开发者,必须提供.dcp 和.bpl 文件,也可能需要提
    供.dcu 文件。
    15.6.7 包集合文件
    包集合文件提供一个向其他开发者发布包的简便方法。每个包集合包含一个或多个包,包括.bpl
    文件和其他需要一起发布的任何其他文件。当在IDE 中选择一个包集合进行安装时,它的要素文件被
    自动地从它们的pce 容器中抽取出来,并且安装对话框提供了选择安装集合中所有的包还是安装部分
    包的功能。
    创建一个包集合的步骤如下。
    *从IDE 菜单中选择“Tools”*“Package Collection Editor”,打开如图15-5 所示的包集合编辑器。
    *从菜单选择“Edit”*“Add Package”或单击“Add a package”按钮,然后选择Select Package
    对话框中的一个.bpl 文件或单击“Open”按钮。若向集合中加入更多的.bpl 文件,则可以多次单击“Add
    a package”按钮。包编辑器的左边的树显示了加入的.bpl 文件。如果要删除它,则选中后从菜单选择
    “Edit”*“Remove Package”或单击“Remove the selected package”按钮。显示了加入一个包之后的
    包编辑器,如图15-6 所示。
    图15-5 包集合编辑器 图15-6 加入一个包之后的包集合编辑器
    *在树的顶端选择Collection 节点,将会在包集合编辑器的右边显示两个域。
    在“Author/Vendor Name”编辑框中可以输入关于包集合的可选信息,这些信息在用户安装包时
    将会显示在安装对话框。
    在Directory list 下面列出了包集合安装的默认目的目录。使用“Add”、“Edit”和“Delete”按钮
    可以编辑这个列表。例如需要将所有的源代码文件拷贝到同一个目录下。在这个例子中,可以输入
    Source 作为一个目录名称,使用C:MyPackageSource 作为建议路径。安装对话框将显示此路径。
    *除了.bpl 文件,包集合可以包括.dcp、.dcu 和.pas(单元)文件、文档以及任意想要发布的文件。
    辅助文件都放置在与指定包(.bpl 文件)关联的文件组中,文件组中的文件只有当安装与它们关
    联的包时才会被安装。要将辅助文件放置到包中,在如图15-6 所示中选择一个.bpl 文件,然后单击“Add
    第15 章 动态链接库和组件包
    ·409·
    a file group”按钮,显示如图15-7 所示的界面,要求输入文件组的名称。如果需要加入多个文件组,
    方法与加入一个文件组是一样的。当选择一个文件组时,新的域将会显示在包集合编辑器的右边。
    图15-7 加入文件组的包集合编辑器
    在Install directory 下拉列表中,选择一个组安装的目的目录。下拉列表包含了在第3 步中在目录
    列表中输入的目录。
    如果希望这些组的安装是可选的,则选择Optional Group 单选框。
    在Include Files 下面,列出了需要包含在这个组中的文件。使用“Add”、“Delete”和“Auto”按
    钮可以编辑这个列表。“Auto”按钮允许选择所有在包的cotains 语句中列出的具有特定扩展名的文件,
    包集合编辑器使用全局的库路径查找这些文件。
    *可以选择集合中所有包的requires 语句中列出的包的安装路径。
    当在树中选择一个.bpl 文件时,4 个新的域将会显示在包集合编辑器的右边,如图15-6 所示。
    在Required Executables directory 下拉列表中,选择需要将安装的包的requires 语句中列出的包的
    bpl 文件的目的目录(下拉列表包含第3 步输入的目录)。包集合编辑器使用Delphi 的全局库路径查找
    并在Required Executable Files 下面列出。
    在Required Libraries directory 下拉列表中,选择需要将安装的包的requires 语句中列出的包的dcp
    文件的目的目录。
    *保存包集合源文件,可以从菜单中选择“File”*“Save.Package collection”源文件将保存(扩
    展名为pce)。
    *构建包集合可以单击“Compile the collection”按钮。包集合编辑器产生一个与源文件(.pce 文
    件)名称一样的.dpc 文件。如果没有保存源文件,编辑器将在编译之前要求输入文件名。
    *在菜单中选择“File”*“Open”并定位需要编辑的文件可以编辑或重新编译现存的.pce 文件。
    15.6.8 使用包和DLL
    对于大多数应用程序,包提供了更大的灵活性并且比DLL 容易创建。然而,有几种情况可能DLL
    比包更适合,例如:
    *代码模块将被非Delphi 应用程序调用;
    *扩展一个Web 服务器的功能时;
    *创建一个供第三方使用的代码模块时;
    *项目是一个OLE 容器。

    ·410·
    不能将Delphi 运行时类型信息(Runtime Type Information,RTTI)传递给DLL 或通过DLL 传递
    给可执行文件。如果将一个对象从一个DLL 传递给另一个DLL 或可执行程序,那么就不能对传递的
    对象使用is 或as 操作符。这是因为is 和as 操作符需要比较RTTI。如果需要从一个库传递一个对象,
    需要使用包,这些可以共享RTTI。同样地在Web Services 中应该使用包而不是DLL,因为它们依赖
    于Delphi RTTI。
    总之,开发一个在IDE 中可以访问的组件就必须创建一个包。当需要构建一个可以被所有应用程
    序调用的库时就创建一个DLL,而不论创建应用程序的开发工具是什么。

  • 相关阅读:
    linux ss 网络状态工具
    如何安装最新版本的memcached
    如何通过XShell传输文件
    mysql主从复制原理
    聊聊IO多路复用之select、poll、epoll详解
    聊聊 Linux 中的五种 IO 模型
    pytorch中使用cuda扩展
    pytorch中调用C进行扩展
    双线性插值
    python中的装饰器
  • 原文地址:https://www.cnblogs.com/luckForever/p/7255069.html
Copyright © 2011-2022 走看看