msdn之dll

来源:互联网 发布:熊猫看书软件 编辑:程序博客网 时间:2024/06/05 15:39

http://msdn.microsoft.com/zh-cn/library/d1587c1h(v=vs.80).aspx

尽管 DLL 和应用程序都是可执行的程序模块,但它们之间有若干不同之处。对于最终用户来说,最明显的差异在于 DLL 不是可直接执行的程序。从系统角度讲,应用程序和 DLL 之间有两个基本差异:

  • 应用程序可有多个同时在系统上运行的实例,而 DLL 只能有一个实例。

  • 应用程序可以拥有堆栈、共用内存、文件句柄、消息队列这样的事物,而 DLL 不能。

    动态链接具有下列优点:

    • 节省内存和减少交换操作。很多进程可以同时使用一个 DLL,在内存中共享该 DLL 的一个副本。相反,对于每个用静态链接库生成的应用程序,Windows 必须在内存中加载库代码的一个副本。

    • 节省磁盘空间。许多应用程序可在磁盘上共享 DLL 的一个副本。相反,每个用静态链接库生成的应用程序均具有作为单独的副本链接到其可执行图像中的库代码。

    • 升级到 DLL 更为容易。当 DLL 中的函数发生更改时,只要函数的参数和返回值没有更改,就不需重新编译或重新链接使用它们的应用程序。相反,静态链接的对象代码要求在函数更改时重新链接应用程序。

    • 提供售后支持。例如,可修改显示器驱动程序 DLL 以支持当初交付应用程序时不可用的显示器。

    • 支持多语言程序。只要程序遵循函数的调用约定,用不同编程语言编写的程序就可以调用相同的 DLL 函数。程序与 DLL 函数在下列方面必须是兼容的:函数期望其参数被推送到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及寄存器中是否传递了任何参数。

    • 提供了扩展 MFC 库类的机制。可以从现有 MFC 类派生类,并将它们放到 MFC 扩展 DLL 中供 MFC 应用程序使用。

    • 使国际版本的创建轻松完成。通过将资源放到 DLL 中,创建应用程序的国际版本变得容易得多。可将用于应用程序的每个语言版本的字符串放到单独的 DLL 资源文件中,并使不同的语言版本加载合适的资源。

    使用 DLL 的一个潜在缺点是应用程序不是独立的;它取决于是否存在单独的 DLL 模块。

    使用 Visual C++ 时,可以用 C 或 C++ 生成不使用 Microsoft 基础类 (MFC) 库的 Win32 DLL。

    非 MFC DLL 是内部不使用 MFC 的 DLL,这类 DLL 中的导出函数可由 MFC 或非 MFC 可执行文件调用。函数通常是通过标准 C 接口从非 MFC DLL 导出的。

    如果您已为 Windows 3.x 生成了 16 位 DLL,应发现生成 32 位 DLL 更为方便。编译器提供了更直接的支持,它可以省去 DLL 创建过程中的若干步骤。Win16 DLL 和 Win32 DLL 之间的具体差异包括:

    • 没有必须要链接的单独的启动模块。DLL 启动序列直接由链接到 DLL 中的 C/C++ 运行时库代码处理。

    • 运行时库代码通过调用适当的构造函数初始化任何静态非本地 C++ 对象。每个进程均获取自己的所有 DLL 静态数据(包括对象)的副本。

    • 不再需要 LibMain 或 WEP 函数(Windows 退出过程)。在何处为 DLL 添加初始化代码和终止代码取决于所生成的 DLL 类型。提供 DllMain 而非LibMain,而当进入和退出时都要调用前者。

    • 可以直接在源代码中导入或导出符号。如果使用 __declspec(dllexport) 属性(类似于 Windows 3.x 中的 __export),则无需为导出使用单独的模块定义文件。

    • 使用 __declspec(dllimport) 从 DLL 导入数据、对象和函数的可执行文件使编辑器生成更有效的代码。

    • 对用 atexit 注册的例程的调用执行时间可以不同。

    • 除了 Win32 非 MFC DLL 外,Visual C++ 还提供了三种 MFC DLL 类型。

    如果您已为 Windows 3.x 生成了 16 位 DLL,应发现生成 32 位 DLL 更为方便。Visual C++ 提供了更直接的支持,它可以省去 DLL 创建过程中的若干步骤。有关更多信息,请参见 Win16 DLL 与 Win32 DLL 之间的差异。

    Win16 DLL 与 Win32 DLL 之间的差异所要求的不仅仅是简单的重新编译,即将 Win16 DLL 转换为 Win32 DLL。有关如何将 16 位 DLL 移植到 Win32 DLL 的更多信息,请参见知识库文章 Q125688“How to Port a 16-bit DLL to a Win32 DLL”(如何将 16 位 DLL 移植到 Win32 DLL)。


    基于 Win32 的应用程序能够按名称从可执行文件内直接寻址 DLL 全局变量。此操作是用类似于导出 DLL 函数名的方法导出全局数据名来完成的。有关如何从 DLL 导出数据的更多信息,请参见从 DLL 导出和知识库文章 Q90530“Exporting Data from a DLL or an Application”(从 DLL 或应用程序导出数据)。

    Win32 DLL 映射到调用进程的地址空间中。默认情况下,每个使用 DLL 的进程都有自己的所有 DLL 全局变量和静态变量的实例。如果 DLL 需要与它的由其他应用程序加载的其他实例共享数据,则可使用下列方法之一:

    • 使用 data_seg 杂注创建命名数据节。

    • 使用内存映射文件。请参见有关内存映射文件的 Win32 文档。

    以下是一个使用 data_seg 杂注的示例:

    #pragma data_seg (".myseg")   int i = 0;    char a[32]n = "hello world";#pragma data_seg()

    data_seg 可用于创建新的命名节(在此示例中为 .myseg)。为清楚起见,最典型的用法是调用数据段 .shared。然后必须在 .def 文件中或者使用链接器选项/SECTION:.MYSEC,RWS 为这个新的命名数据节指定正确的共享属性。

    在使用共享数据段之前要考虑下列限制:

    • 必须静态初始化共享数据段中的所有变量。在上面的示例中,i 初始化为 0,而 a 是初始化为“hello world”的 32 个字符。

    • 所有共享变量放在编译 DLL 的指定数据段中。很大的数组可产生很大的 DLL。对于所有已初始化的全局变量都是如此。

    • 永远不要将特定于进程的信息存储在共享数据段中。大多数 Win32 数据结构或值(如 HANDLE)仅在单个进程的上下文内才真正有效。

    • 每个进程都将获取它自己的地址空间。永远不要将指针存储在共享数据段包含的变量中,这一点很重要。指针可能在某个应用程序中完全有效,但在另一个应用程序中却无效。

    • DLL 本身有可能加载到每个进程的虚拟地址空间中的不同地址。具有指向 DLL 中的函数或指向其他共享变量的指针是不安全的。

    请注意,上述最后三点适用于内存映射文件和共享数据段。

    内存映射文件优于共享数据节,原因是内存映射文件的起始位置是已知的。开发人员通过使用距离位于共享内存内的所有数据中的“共享内存节的起始位置的偏移量”,可以实现类似于指针的行为。为使此操作快速简便,强烈建议使用 __based 指针。但一定要记住:在每个进程中,基(或内存映射文件的起始位置)可能不同,因此存储 __based 指针的基的变量自身不能位于共享内存中。

    这些限制对 C++ 类有重要的含义。

    • 具有虚函数的类总是包含函数指针。具有虚函数的类永远不应存储在共享数据段中,也不应存储在内存映射文件中。这对于 MFC 类或从 MFC 继承的类尤其重要。

    • 静态数据成员以全局变量的等效形式实现。这意味着每个进程都具有它自己的该类静态数据成员的副本。不应共享具有静态数据成员的类。

    • 对于 C++ 类,共享数据段的初始化要求引起一个特定问题。如果共享数据段中有类似 CTest Counter(0); 的内容,则当每个进程加载 DLL 时,Counter 对象将在该进程中初始化,从而有可能每次都将对象的数据清零。这与内部数据类型(由链接器在创建 DLL 时初始化)非常不同。

    由于存在这些限制,Microsoft 不建议在进程之间共享 C++ 对象。一般情况下,如果希望使用 C++ 在进程之间共享数据,请编写在内部使用内存映射文件来共享数据的类,但不要共享类实例本身。在开发这样的类时,可能需要特别小心,但它使应用程序开发人员能够完全控制共享数据的副作用。

    有关创建命名数据节的更多信息,请参见位于 http://support.microsoft.com/default.aspx?ln=zh-cn 上的下列知识库文章:

    • “How to Share Data Between Different Mappings of a DLL”(如何在 DLL 的不同映射之间共享数据)(Q125677)。

    • “Specifying Shared and Nonshared Data in a DLL”(指定 DLL 中的共享数据和非共享数据)(Q100634)。

    • “Sharing All Data in a DLL”(共享 DLL 中的所有数据)(Q109619)。

    • “Memory in Shared Code Sections Is Not Shared Across Terminal Server Sessions”(共享代码节中的内存不在终端服务器会话间共享)(Q251045)

    若要使用 Visual C++ 调试 DLL,必须生成 DLL 的调试版本,并从应用程序调用此版本。但是,不必用 Visual C++ 生成调用应用程序的调试版本或生成调用应用程序。有关调试 DLL 的更多信息,请参见调试 DLL 或下列知识库文章:

    • “Debugging a Dynamic-Link Library (DLL) in Windows”(Q85221)。

    • “Problems Loading a Debuggee That Uses a DLL”(Q119518)。

    如果 DLL 具有大量导出函数,则使用 .def 文件导出函数,而不是使用 __declspec(dllexport),并在每个导出函数上使用 .def 文件 NONAME 属性。NONAME 属性使得仅在 DLL 的导出表中存储序号值而不存储函数名,从而减小文件大小。

    隐式链接到应用程序的 DLL 在程序加载时被加载。若要提高加载时的性能,请试着将 DLL 分割为不同的 DLL。将调用应用程序加载后立即需要的所有函数放在一个 DLL 中,并使调用应用程序隐式链接到该 DLL。将调用应用程序当时不需要的其他函数放在另一个 DLL 中,并使应用程序显式链接到此 DLL。有关更多信息,请参见确定要使用的链接方法。

    可执行文件以下列两种方式之一链接到(或加载)DLL:

    • 隐式链接

    • 显式链接

    隐式链接有时称为静态加载或加载时动态链接。显式链接有时称为动态加载或运行时动态链接。

    在隐式链接下,使用 DLL 的可执行文件链接到该 DLL 的创建者所提供的导入库(.lib 文件)。使用 DLL 的可执行文件加载时,操作系统加载此 DLL。客户端可执行文件调用 DLL 的导出函数,就好像这些函数包含在可执行文件内一样。

    在显式链接下,使用 DLL 的可执行文件必须进行函数调用以显式加载和卸载该 DLL,并访问该 DLL 的导出函数。客户端可执行文件必须通过函数指针调用导出函数。

    可执行文件对两种链接方法可以使用同一个 DLL。另外,由于一个可执行文件可隐式链接到某个 DLL,而另一个可显式附加到此 DLL,故这些机制不是互斥的。


    有两种类型的链接:隐式链接和显式链接。

    隐式链接

    应用程序的代码调用导出 DLL 函数时发生隐式链接。当调用可执行文件的源代码被编译或被汇编时,DLL 函数调用在对象代码中生成一个外部函数引用。若要解析此外部引用,应用程序必须与 DLL 的创建者所提供的导入库(.LIB 文件)链接。

    导入库仅包含加载 DLL 的代码和实现 DLL 函数调用的代码。在导入库中找到外部函数后,会通知链接器此函数的代码在 DLL 中。要解析对 DLL 的外部引用,链接器只需向可执行文件中添加信息,通知系统在进程启动时应在何处查找 DLL 代码。

    系统启动包含动态链接引用的程序时,它使用程序的可执行文件中的信息定位所需的 DLL。如果系统无法定位 DLL,它将终止进程并显示一个对话框来报告错误。否则,系统将 DLL 模块映射到进程的地址空间中。

    如果任何 DLL 具有(用于初始化代码和终止代码的)入口点函数,操作系统将调用此函数。在传递到入口点函数的参数中,有一个指定用以指示 DLL 正在附带到进程的代码。如果入口点函数没有返回 TRUE,系统将终止进程并报告错误。

    最后,系统修改进程的可执行代码以提供 DLL 函数的起始地址。

    与程序代码的其余部分一样,DLL 代码在进程启动时映射到进程的地址空间中,且仅当需要时才加载到内存中。因此,由 .def 文件用来在 Windows 的早期版本中控制加载的 PRELOAD 和 LOADONCALL 代码属性不再具有任何意义。

    显式链接

    大部分应用程序使用隐式链接,因为这是最易于使用的链接方法。但是有时也需要显式链接。下面是一些使用显式链接的常见原因:

    • 直到运行时,应用程序才知道需要加载的 DLL 的名称。例如,应用程序可能需要从配置文件获取 DLL 的名称和导出函数名。

    • 如果在进程启动时未找到 DLL,操作系统将终止使用隐式链接的进程。同样是在此情况下,使用显式链接的进程则不会被终止,并可以尝试从错误中恢复。例如,进程可通知用户所发生的错误,并让用户指定 DLL 的其他路径。

    • 如果使用隐式链接的进程所链接到的 DLL 中有任何 DLL 具有失败的 DllMain 函数,该进程也会被终止。同样是在此情况下,使用显式链接的进程则不会被终止。

    • 因为 Windows 在应用程序加载时加载所有的 DLL,故隐式链接到许多 DLL 的应用程序启动起来会比较慢。为提高启动性能,应用程序可隐式链接到那些加载后立即需要的 DLL,并等到在需要时显式链接到其他 DLL。

    • 显式链接下不需将应用程序与导入库链接。如果 DLL 中的更改导致导出序号更改,使用显式链接的应用程序不需重新链接(假设它们是用函数名而不是序号值调用 GetProcAddress),而使用隐式链接的应用程序必须重新链接到新的导入库。

    下面是需要注意的显式链接的两个缺点:

    • 如果 DLL 具有 DllMain 入口点函数,则操作系统在调用 LoadLibrary 的线程上下文中调用此函数。如果由于以前调用了 LoadLibrary 但没有相应地调用FreeLibrary 函数而导致 DLL 已经附加到进程,则不会调用此入口点函数。如果 DLL 使用 DllMain 函数为进程的每个线程执行初始化,显式链接会造成问题,因为调用 LoadLibrary(或 AfxLoadLibrary)时存在的线程将不会初始化。

    • 如果 DLL 将静态作用域数据声明为 __declspec(thread),则在显式链接时 DLL 会导致保护错误。用 LoadLibrary 加载 DLL 后,每当代码引用此数据时 DLL 就会导致保护错误。(静态作用域数据既包括全局静态项,也包括局部静态项。)因此,创建 DLL 时应避免使用线程本地存储区,或者应(在用户尝试动态加载时)告诉 DLL 用户潜在的缺陷。


    为隐式链接到 DLL,可执行文件必须从 DLL 的提供程序获取下列各项:

    • 包含导出函数和/或 C++ 类的声明的头文件(.h 文件)。类、函数和数据均应具有 __declspec(dllimport),有关更多信息,请参见 dllexport, dllimport。

    • 要链接的导入库(.LIB files)。(生成 DLL 时链接器创建导入库。)

    • 实际的 DLL(.dll 文件)。

    使用 DLL 的可执行文件必须包括头文件,此头文件包含每个源文件中的导出函数(或 C++ 类),而这些源文件包含对导出函数的调用。从编码的角度讲,导出函数的函数调用与任何其他函数调用一样。

    若要生成调用可执行文件,必须与导入库链接。如果使用的是外部生成文件,请指定导入库的文件名,此导入库中列出了要链接到的其他对象 (.obj) 文件或库。

    操作系统在加载调用可执行文件时,必须能够定位 DLL 文件。


    在显式链接下,应用程序必须进行函数调用以在运行时显式加载 DLL。为显式链接到 DLL,应用程序必须:

    • 调用 LoadLibrary(或相似的函数)以加载 DLL 和获取模块句柄。

    • 调用 GetProcAddress,以获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接。

    • 使用完 DLL 后调用 FreeLibrary

    例如:

    typedef UINT (CALLBACK* LPFNDLLFUNC1)(DWORD,UINT);...HINSTANCE hDLL;               // Handle to DLLLPFNDLLFUNC1 lpfnDllFunc1;    // Function pointerDWORD dwParam1;UINT  uParam2, uReturnVal;hDLL = LoadLibrary("MyDLL");if (hDLL != NULL){   lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL,                                           "DLLFunc1");   if (!lpfnDllFunc1)   {      // handle the error      FreeLibrary(hDLL);             return SOME_ERROR_CODE;   }   else   {      // call the function      uReturnVal = lpfnDllFunc1(dwParam1, uParam2);   }}

    DLL 通常具有在 DLL 加载时必须执行的初始化代码(如分配内存)。使用 Visual C++ 时,在何处添加初始化 DLL 的代码取决于生成的 DLL 类型。如果不需要添加初始化代码或终止代码,则在生成 DLL 时没有什么特别的事情要做。如果需要初始化 DLL,则下表描述了应在何处添加代码。

    DLL 类型添加初始化代码和终止代码的位置

    规则 DLL

    在 DLL 的 CWinApp 对象的 InitInstance 和 ExitInstance 中。

    扩展 DLL

    在“MFC DLL 向导”生成的 DllMain 函数中。

    非 MFC DLL

    在您提供的称为 DllMain 的函数中。

    在 Win32 中,所有 DLL 都可能包含一个可选的入口点函数(通常称为 DllMain),初始化和终止时都要调用此函数。这使您有机会在需要时分配或释放其他资源。Windows 在四种情况下调用入口点函数:进程附加、进程分离、线程附加和线程分离。

    C 运行时库提供了一个名为 _DllMainCRTStartup 的入口点函数,并调用 DllMain。根据 DLL 类型的不同,应在源代码中包含一个名为 DllMain 的函数,或应用 MFC 库中提供的 DllMain


    为初始化非 MFC DLL,DLL 源代码必须包含一个名为 DllMain 的函数。下列代码显示了一个基本主干,说明 DllMain 定义的大概样子:

    BOOL APIENTRY DllMain(HANDLE hModule,                       DWORD  ul_reason_for_call,                       LPVOID lpReserved){    switch( ul_reason_for_call ) {    case DLL_PROCESS_ATTACH:    ...    case DLL_THREAD_ATTACH:    ...    case DLL_THREAD_DETACH:    ...    case DLL_PROCESS_DETACH:    ...    }    return TRUE;}
    Note注意

    DllEntryPoint 的 Platform SDK 文档指出,必须在链接器命令行上用 /ENTRY 选项指定入口点函数的实际名称。使用 Visual C++ 时,如果入口点函数的名称为 DllMain,则无需使用 /ENTRY 选项。实际上,如果使用了 /ENTRY 选项,但没有将入口点函数命名为 DllMain,则 C 运行时库将不会正确初始化。

    • DllMain 的函数规范 (Platform SDK)

    • 动态链接库入口点函数 (Platform SDK)

    • C 运行时库行为和 _DllMainCRTStartup


    C/C++ 运行时库代码执行 DLL 启动序列,从而不必像 Windows 3.x 中那样必须链接到单独的模块。C/C++ 运行时库代码中包含的是名为 _DllMainCRTStartup的 DLL 入口点函数。_DllMainCRTStartup 函数执行若干操作,其中包括调用 _CRT_INIT,此操作初始化 C/C++ 运行时库并在静态非局部变量上调用 C++ 构造函数。如果没有此函数,运行时库将保持未初始化状态。_CRT_INIT 既可以用于静态链接的 CRT,也可以从用户 DLL 链接到 CRT DLL Msvcrt.dll。

    虽然可以使用 /ENTRY: 链接器选项指定其他入口点函数,但不建议这样做,因为新的入口点函数将不得不重复 _DllMainCRTStartup 执行的所有操作。用 Visual C++ 生成 DLL 时,系统自动链接 _DllMainCRTStartup,您无需使用 /ENTRY: 链接器选项指定入口点函数。

    除了初始化 C 运行时库外,_DllMainCRTStartup 还调用名为 DllMain 的函数。根据生成的 DLL 类型的不同,Visual C++ 为您提供 DllMain 并使它被链接,以便 _DllMainCRTStartup 始终有东西可以调用。这样,如果不需要初始化 DLL,则在生成 DLL 时没有什么特别的事情要做。如果需要初始化 DLL,添加代码的位置取决于编写的 DLL 类型。有关更多信息,请参见初始化 DLL。

    C/C++ 运行时库代码在静态非局部变量上调用构造函数和析构函数。例如,在以下 DLL 源代码中,Equus 和 Sugar 是 CHorse 类的两个静态非本地对象,它们都是在 Horses.h 中定义的。由于这些对象是在所有函数的外部定义的,源代码中没有任何函数包含对 CHorse 的构造函数或对析构函数的调用。因此,必须由运行时代码执行对这些构造函数和析构函数的调用。应用程序的运行时库代码也执行此函数。

    #include "horses.h"CHorse  Equus( ARABIAN, MALE );CHorse  Sugar( THOROUGHBRED, FEMALE );BOOL    WINAPI   DllMain (HANDLE hInst,                             ULONG ul_reason_for_call,                            LPVOID lpReserved)...

    每当新进程尝试使用 DLL 时,操作系统就为 DLL 数据创建一个单独的副本:这称作“进程附加”。DLL 的运行时库代码调用所有全局对象的构造函数(如果有的话),然后通过选定进程附加来调用 DllMain 函数。相反的情况是线程分离:运行时库代码通过选定进程分离来调用 DllMain,然后调用一系列终止函数,其中包括 atexit 函数、全局对象的析构函数和静态对象的析构函数。请注意,进程附加中的事件顺序与进程分离中的相反。

    线程附加和线程分离时也会调用运行时库代码,但是运行时库代码不主动执行任何初始化和终止操作。


    进程调用 LoadLibrary(或 AfxLoadLibrary)以显式链接到 DLL。如果成功,函数将指定的 DLL 映射到调用进程的地址空间中并返回此 DLL 的句柄,该句柄可与用于显式链接的其他函数(如 GetProcAddress 和 FreeLibrary)一起使用。

    LoadLibrary 尝试使用用于隐式链接的同一搜索序列来定位 DLL。如果系统无法找到 DLL 或者入口点函数返回 FALSE,LoadLibrary 将返回 NULL。如果对LoadLibrary 的调用所指定的 DLL 模块已映射到调用进程的地址空间中,则函数仅返回 DLL 的句柄并递增模块的引用数。

    如果 DLL 有入口点函数,则操作系统在调用 LoadLibrary 的进程上下文中调用此函数。如果由于以前调用了 LoadLibrary 但没有相应地调用 FreeLibrary 函数而导致 DLL 已经附加到进程,则不会调用此入口点函数。

    加载扩展 DLL 的 MFC 应用程序应使用 AfxLoadLibrary,而不应使用 LoadLibraryAfxLoadLibrary 在调用 LoadLibrary 之前将处理线程同步。AfxLoadLibrary 的接口(函数原型)与 LoadLibrary 相同。

    如果出于某种原因 Windows 无法加载 DLL,进程可以尝试从错误恢复。例如,进程可通知用户所发生的错误,并让用户指定 DLL 的其他路径。

    Security note安全注意

    如果代码要在 Windows NT 4、Windows 2000 或 Windows XP(SP1 以前的版本)下运行,则请确保为所有 DLL 指定完整路径名。在这些操作系统中,加载文件时会首先搜索当前目录。如果没有限定文件的路径,则可能会加载没有计划加载的文件。

    • 隐式链接

    • 确定要使用的链接方法

    显式链接到 DLL 的进程调用 GetProcAddress 来获取 DLL 导出函数的地址。使用返回的函数指针调用 DLL 函数。GetProcAddress 将(由LoadLibraryAfxLoadLibrary 或 GetModuleHandle 返回的)DLL 模块句柄和要调用的函数名或函数的导出序号用作参数。

    由于是通过指针调用 DLL 函数并且没有编译时类型检查,需确保函数的参数是正确的,以便不会超出在堆栈上分配的内存和不会导致访问冲突。帮助提供类型安全的一种方法是查看导出函数的函数原型,并创建函数指针的匹配 typedef。例如:

    typedef UINT (CALLBACK* LPFNDLLFUNC1)(DWORD,UINT);...HINSTANCE hDLL;               // Handle to DLLLPFNDLLFUNC1 lpfnDllFunc1;    // Function pointerDWORD dwParam1;UINT  uParam2, uReturnVal;hDLL = LoadLibrary("MyDLL");if (hDLL != NULL){   lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL,                                           "DLLFunc1");   if (!lpfnDllFunc1)   {      // handle the error      FreeLibrary(hDLL);      return SOME_ERROR_CODE;   }   else   {      // call the function      uReturnVal = lpfnDllFunc1(dwParam1, uParam2);   }}

    调用 GetProcAddress 时指定所需函数的方式取决于 DLL 的生成方式。

    仅当要链接到的 DLL 是用模块定义 (.def) 文件生成的,并且序号在 DLL 的 .def 文件的 EXPORTS 部分中与函数一起列出时,才能获取导出序号。如果 DLL 具有许多导出函数,则相对于使用函数名,使用导出序号调用 GetProcAddress 的速度稍快一些,因为导出序号是 DLL 导出表的索引。使用导出序号,GetProcAddress 可直接定位函数,而不是将指定名称与 DLL 导出表中的函数名进行比较。但是,仅当有权控制 .def 文件中导出函数的序号分配时,才应使用导出序号调用 GetProcAddress


    不再需要 DLL 模块时,显式链接到 DLL 的进程调用 FreeLibrary 函数。此函数递减模块的引用数,如果引用数为零,此函数便从进程的地址空间中取消模块的映射。

    MFC 应用程序应使用 AfxFreeLibrary 而非 FreeLibrary 卸载扩展 DLL。AfxFreeLibrary 的接口(函数原型)与 FreeLibrary 相同。

    通过隐式和显式链接,Windows 首先搜索“已知 DLL”,如 Kernel32.dll 和 User32.dll。Windows 然后按下列顺序搜索 DLL:

    1. 当前进程的可执行模块所在的目录。

    2. 当前目录。

    3. Windows 系统目录。GetSystemDirectory 函数检索此目录的路径。

    4. Windows 目录。GetWindowsDirectory 函数检索此目录的路径。

    5. PATH 环境变量中列出的目录。

      Note注意

      未使用 LIBPATH 环境变量。


    纯资源 DLL 是仅包含资源(如图标、位图、字符串和对话框)的 DLL。使用纯资源 DLL 是在多个程序之间共享同一组资源的好方法。提供其资源被针对多种语言进行本地化的应用程序也是一种好方法(请参见 MFC 应用程序中的本地化资源:附属 DLL)。

    若要创建纯资源 DLL,请创建一个新的 Win32 DLL(非 MFC)项目,并将资源添加到此项目。

    • 在“新建项目”对话框中选择“Win32 项目”,并在“Win32 项目向导”中指定 DLL 项目类型。

    • 为 DLL 创建一个包含资源(如字符串或菜单)的新资源脚本,并保存该 .rc 文件。

    • “项目”菜单上单击“添加现有项”,然后在项目中插入这个新的 .rc 文件。

    • 指定 /NOENTRY 链接器选项。/NOENTRY 防止链接器将 _main 引用链接到 DLL 中;此选项是创建纯资源 DLL 所必需的。

    • 生成 DLL。

    使用纯资源 DLL 的应用程序应调用 LoadLibrary 来显式链接到 DLL。若要访问资源,请调用一般函数 FindResource 和 LoadResource,这两个函数对任何类型的资源都有效,或调用下列资源特定的函数之一:

    • FormatMessage

    • LoadAccelerators

    • LoadBitmap

    • LoadCursor

    • LoadIcon

    • LoadMenu

    • LoadString

    使用完资源后,应用程序应调用 FreeLibrary


    可以使用两种方法将公共符号导入到应用程序中或从 DLL 导出函数:

    • 生成 DLL 时使用模块定义 (.def) 文件

    • 在主应用程序的函数定义中使用关键字 __declspec(dllimport) 或 __declspec(dllexport)

    使用 .def 文件

    模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。如果不使用 __declspec(dllimport) 或 __declspec(dllexport) 导出 DLL 函数,则 DLL 需要 .def 文件。

    可以使用 .def 文件导入到应用程序中或从 DLL 导出。

    使用 __declspec

    Visual C++ 用 __declspec(dllimport) 和 __declspec(dllexport) 取代以前在 16 位版的 Visual C++ 中使用的 __export 关键字。

    不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用__declspec(dllimport) 才能导入 DLL 中使用的变量。

    如果有正确的 .def 文件 EXPORTS 节,则不需要 __declspec(dllexport)。添加 __declspec(dllexport) 是为了提供不使用 .def 文件从 .exe 或 .dll 文件导出函数的简单方法。

    Win32 可移植可执行文件格式旨在最小化为修改导入而必须访问的页数。为此,它将所有程序的所有导入地址都放在一个称为“导入地址表”的位置。这使得加载程序在访问这些导入时可以只修改一两页。

    可以使用两种方法将函数导入到应用程序中:

    • 在主应用程序的函数定义中使用 __declspec(dllimport) 关键字。

    • 将模块定义 (.def) 文件与 __declspec(dllimport) 一起使用。

    如果一个程序使用 DLL 定义的公共符号,就说该程序是在导入公共符号。为使用 DLL 生成的应用程序创建头文件时,在公共符号的声明上使用__declspec(dllimport)。不论是用 .def 文件导出还是用 __declspec(dllexport) 关键字导出,__declspec(dllimport) 关键字均有效。

    若要提高代码的可读性,请为 __declspec(dllimport) 定义一个宏,然后使用此宏声明每个导入的符号:

    #define DllImport   __declspec( dllimport )DllImport int  j;DllImport void func();

    在函数声明上使用 __declspec(dllimport) 是可选操作,但如果使用此关键字,编译器将生成更有效的代码。但是,为使导入的可执行文件能够访问 DLL 的公共数据符号和对象,必须使用 __declspec(dllimport)。请注意,DLL 的用户仍然需要与导入库链接。

    对 DLL 和客户端应用程序可以使用相同的头文件。为此,请使用特殊的预处理器符号来指示是生成 DLL 还是生成客户端应用程序。例如:

    #ifdef _EXPORTING   #define CLASS_DECLSPEC    __declspec(dllexport)#else   #define CLASS_DECLSPEC    __declspec(dllimport)#endifclass CLASS_DECLSPEC CExampleA : public CObject{ ... class definition ... };

    下面的代码示例显示如何使用 _declspec(dllimport) 将函数调用从 DLL 导入到应用程序中。假定 func1 是驻留在某个 DLL 中的函数,而此 DLL 与包含“主”函数的 .exe 文件是分开的。

    不使用 __declspec(dllimport),给出此代码:

    int main(void) {   func1();}

    编译器生成类似下面的代码:

    call func1

    链接器将调用翻译成下面的内容:

    call 0x4000000         ; The address of 'func1'.

    如果 func1 存在于另一个 DLL 中,链接器将无法直接解析此函数,因为它无法得知 func1 的地址。在 16 位环境中,链接器将此代码地址添加到 .exe 文件中的某个列表中,而加载程序在运行时会用正确的地址修补该列表。在 32 位和 64 位环境中,链接器可生成一个知道其地址的 thunk。在 32 位环境中,thunk 类似如下所示:

    0x40000000:    jmp DWORD PTR __imp_func1

    其中,imp_func1 是 func1 的槽在 .exe 文件的导入地址表中的地址。这样,链接器就知道了所有的地址。加载程序只需在加载时更新 .exe 文件的导入地址表,一切就会正常进行。

    因此,使用 __declspec(dllimport) 更好,因为链接器不生成不必要的 thunk。thunk 使代码变大(在 RISC 系统上代码它可能是若干指令),并且会降低缓存性能。如果通知编译器函数在 DLL 中,编译器会为您生成间接调用。

    因此,现在此代码:

    __declspec(dllimport) void func1(void);int main(void) {   func1();}

    生成此指令:

    call DWORD PTR __imp_func1

    没有 thunk 和 jmp 指令,所以代码更小且更快。

    另一方面,对于 DLL 内部的函数调用,您希望不必使用间接调用。您已经知道函数的地址。由于间接调用前需要时间和空间来加载和存储函数的地址,因此直接调用总是更快,而且所需的空间也总是更小。当从 DLL 本身外部调用 DLL 时,您仅希望使用 __declspec(dllimport)。生成某个 DLL 时不要对该 DLL 的内部函数使用 __declspec(dllimport)


    就数据而言,使用 __declspec(dllimport) 是移除间接层的方便手段。从 DLL 导入数据时,仍需要仔细检查导入地址表。在 __declspec(dllimport) 之前,这意味着在访问从 DLL 导出的数据时必须记住进行额外的间接寻址:

    // project.h#ifdef _DLL   // If accessing the data from inside the DLL   ULONG ulDataInDll;#else         // If accessing the data from outside the DLL   ULONG *ulDataInDll;#endif

    然后导出 .DEF 文件中的数据:

    // project.defLIBRARY projectEXPORTS   ulDataInDll   CONSTANT

    并从 DLL 外部访问这些数据:

    if (*ulDataInDll == 0L) {   // Do stuff here}

    将数据标记为 __declspec(dllimport) 时,编译器自动为您生成间接代码。您不必再为上述步骤操心了。如前所述,生成 DLL 时不要在数据上使用__declspec(dllimport) 声明。DLL 内部的函数不使用导入地址表访问数据对象;因此,不会再有额外的间接寻址了。

    若要自动从 DLL 导出数据,请使用此声明:

    __declspec(dllexport) ULONG ulDataInDLL;

    如果选择与 .def 文件一起使用 __declspec(dllimport),则应更改 .def 文件,用 DATA 取代 CONSTANT,以减少因不正确的编码导致问题的可能性:

    // project.defLIBRARY projectEXPORTS   ulDataInDll   DATA

    下表说明了原因。

    关键字在导入库中发出导出

    CONSTANT

    _imp_ulDataInDll_ulDataInDll

    _ulDataInDll

    DATA

    _imp_ulDataInDll

    _ulDataInDll

    使用 __declspec(dllimport) 和 CONSTANT,在为允许显式链接而创建的 .lib DLL 导入库中同时列出 imp 版本和未修饰名。使用 __declspec(dllimport) 和 DATA 列出的只是 imp 版本的名称。

    如果使用 CONSTANT,则可以使用以下任一代码构造访问 ulDataInDll

    __declspec(dllimport) ULONG ulDataInDll; /*prototype*/if (ulDataInDll == 0L)   /*sample code fragment*/

    - 或 -

    ULONG *ulDataInDll;      /*prototype*/if (*ulDataInDll == 0L)  /*sample code fragment*/

    但是,如果在 .def 文件中使用 DATA,则只有用下面的定义编译的代码才可以访问 ulDataInDll 变量:

    __declspec(dllimport) ULONG ulDataInDll;if (ulDataInDll == 0L)   /*sample code fragment*/

    使用 CONSTANT 的风险更大,因为如果忘记使用额外级别的间接寻址,则访问的有可能是指向变量的导入地址表的指针,而不是变量本身。由于导入地址表当前被编译器和链接器设置成了只读,因此这类问题经常表现为访问冲突。

    如果当前的 Visual C++ 链接器发现 .def 文件中的 CONSTANT 是导致上述问题的原因,它将发出警告。使用 CONSTANT 的唯一真正原因是:无法对头文件未在原型上列出 __declspec(dllimport) 的某对象文件进行重新编译。



    .DLL 文件的布局与 .exe 文件非常相似,但有一个重要的差异:DLL 文件包含导出表。导出表包含 DLL 导出到其他可执行文件的每个函数的名称。这些函数是 DLL 中的入口点;只有导出表中的函数可由其他可执行文件访问。DLL 中的任何其他函数都是 DLL 私有的。通过使用带 /EXPORTS 选项的 Dumpbin 工具,可以查看 DLL 的导出表。

    有两种从 DLL 导出函数的方法:

    • 在生成 DLL 时,创建一个模块定义 (.def) 文件并使用该 .def 文件。如果希望按序号而不是按名称从 DLL 导出函数,则请使用此方法。

    • 在函数的定义中使用 __declspec(dllexport) 关键字。

    用上述任何方法导出函数时,确保使用 __stdcall 调用约定。


    模块定义 (.def) 文件是包含一个或多个描述 DLL 各种属性的 Module 语句的文本文件。如果不使用 __declspec(dllexport) 关键字导出 DLL 的函数,则 DLL 需要 .def 文件。

    .def 文件必须至少包含下列模块定义语句:

    • 文件中的第一个语句必须是 LIBRARY 语句。此语句将 .def 文件标识为属于 DLL。LIBRARY 语句的后面是 DLL 的名称。链接器将此名称放到 DLL 的导入库中。

    • EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 1 到 N,其中 N 是 DLL 导出函数的个数。如果希望按序号导出函数,请参见按序号而不是按名称从 DLL 导出函数以及本主题。

    例如,包含实现二进制搜索树的代码的 DLL 看上去可能像下面这样:

    LIBRARY   BTREEEXPORTS   Insert   @1   Delete   @2   Member   @3   Min   @4

    如果导出 C++ 文件中的函数,必须将修饰名放到 .def 文件中,或者通过使用外部“C”定义具有标准 C 链接的导出函数。如果需要将修饰名放到 .def 文件中,则可以通过使用 DUMPBIN 工具或 /MAP 链接器选项来获取修饰名。请注意,编译器产生的修饰名是编译器特定的。如果将 Visual C++ 编译器产生的修饰名放到 .def 文件中,则链接到 DLL 的应用程序必须也是用相同版本的 Visual C++ 生成的,这样调用应用程序中的修饰名才能与 DLL 的 .def 文件中的导出名相匹配。


    Microsoft 在 Visual C++ 的 16 位编译器版本中引入了 __export,使编译器得以自动生成导出名并将它们放到一个 .lib 文件中。然后,此 .lib 文件就可以像静态 .lib 那样用于与 DLL 链接。

    在更新的编译器版本中,可以使用 __declspec(dllexport) 关键字从 DLL 导出数据、函数、类或类成员函数。__declspec(dllexport) 会将导出指令添加到对象文件中,因此您不需要使用 .def 文件。

    当试图导出 C++ 修饰函数名时,这种便利最明显。由于对名称修饰没有标准规范,因此导出函数的名称在不同的编译器版本中可能有所变化。如果使用__declspec(dllexport),仅当解决任何命名约定更改时才必须重新编译 DLL 和依赖 .exe 文件。

    许多导出指令(如序号、NONAME 和 PRIVATE)只能在 .def 文件中创建,并且必须使用 .def 文件来指定这些属性。不过,在 .def 文件的基础上另外使用__declspec(dllexport) 不会导致生成错误。

    若要导出函数,__declspec(dllexport) 关键字必须出现在调用约定关键字的左边(如果指定了关键字)。例如:

    __declspec(dllexport) void __cdecl Function1(void);

    若要导出类中的所有公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示:

    class __declspec(dllexport) CExampleExport : public CObject{ ... class definition ... };

    生成 DLL 时,通常创建一个包含正在导出的函数原型和/或类的头文件,并将 __declspec(dllexport) 添加到头文件中的声明中。若要提高代码的可读性,请为__declspec(dllexport) 定义一个宏并对正在导出的每个符号使用该宏:

    #define DllExport   __declspec( dllexport ) 

    __declspec(dllexport) 将函数名存储在 DLL 的导出表中。如果希望优化表的大小,请参见按序号而不是按名称从 DLL 导出函数。

    Note注意

    将 DLL 源代码从 Win16 移植到 Win32 时,请用 __declspec(dllexport) 替换 __export 的每个实例。

    作为参考,请在 Win32 Winbase.h 头文件中搜索。它包含 __declspec(dllimport) 的用法示例。



    如果在用 C++ 编写的 DLL 中有希望从 C 语言模块访问的函数,应使用 C 链接而不是 C++ 链接来声明这些函数。除非另外指定,C++ 编译器使用 C++ 类型安全命名约定(也称作名称修饰)和 C++ 调用约定(使用此调用约定从 C 调用会很困难)。

    若要指定 C 链接,请为函数声明指定 extern "C"。例如:

    extern "C" __declspec( dllexport ) int MyFunc(long parm1);


    如果在用 C 编写的 DLL 中有希望从 C 语言或 C++ 语言模块访问的函数,则应使用 __cplusplus 预处理器宏确定正在编译的语言,然后,如果是从 C++ 语言模块使用,则用 C 链接声明这些函数。如果使用此技术并为 DLL 提供头文件,则这些函数可以原封不动地由 C 和 C++ 用户使用。

    以下代码演示可由 C 和 C++ 客户端应用程序使用的头文件:

    // MyCFuncs.h#ifdef __cplusplusextern "C" {  // only need to export C interface if              // used by C++ source code#endif__declspec( dllimport ) void MyCFunc();__declspec( dllimport ) void AnotherCFunc();#ifdef __cplusplus}#endif

    如果需要将 C 函数链接到 C++ 可执行文件,并且函数声明头文件没有使用上面的技术,则在 C++ 源文件中添加下列内容以防止编译器修饰 C 函数名:

    extern "C" {#include "MyCHeader.h"}


    若要确定用于导出函数的方法(.def 文件或 __declspec(dllexport) 关键字),请回答下列问题:

    • 是否要一直添加附加的导出函数?

    • 谁要使用 DLL?例如,是由许多无法重新生成的可执行文件使用的第三方 DLL 还是仅由可以轻松重新生成的应用程序使用的 DLL?

    使用 .DEF 文件的优缺点

    在 .def 文件中导出函数使您得以控制导出序号。当将附加的导出函数添加到 DLL 时,可以给它们分配更高的序号值(高于任何其他导出函数)。当您进行此操作时,使用隐式链接的应用程序不必与包含新函数的新导入库重新链接。这非常重要,例如,在设计将由许多应用程序使用的第三方 DLL 时。可以通过添加附加功能不断地增强 DLL,同时确保现有应用程序继续正常使用新的 DLL。MFC DLL 是使用 .def 文件生成的。

    使用 .def 文件的另一个优点是:可以使用 NONAME 属性导出函数,该属性仅将序号放到 DLL 的导出表中。对具有大量导出函数的 DLL,使用 NONAME 属性可以减小 DLL 文件的大小。有关编写模块定义语句的信息,请参见模块定义语句的规则。有关序号导出的更多信息,请参见按序号而不是按名称从 DLL 导出函数。

    使用 .def 文件的主要缺点是:在 C++ 文件中导出函数时,必须将修饰名放到 .def 文件中,或者通过使用外部“C”用标准 C 链接定义导出函数,以避免编译器进行名称修饰。

    如果需要将修饰名放到 .def 文件中,则可以通过使用 DUMPBIN 工具或 /MAP 链接器选项来获取修饰名。请注意,编译器产生的修饰名是编译器特定的。如果将 Visual C++ 编译器产生的修饰名放到 .def 文件中,则链接到 DLL 的应用程序必须也是用相同版本的 Visual C++ 生成的,这样调用应用程序中的修饰名才能与 DLL 的 .def 文件中的导出名相匹配。

    使用 __declspec(dllexport) 的优缺点

    使用 __declspec(dllexport) 非常方便,因为不必考虑维护 .def 文件和获取导出函数的修饰名。例如,如果您设计的 DLL 供自己控制的应用程序使用,则此方法很适用。如果通过新的导出函数重新生成 DLL,还必须重新生成应用程序,因为如果使用不同版本的编译器进行重新编译,则导出的 C++ 函数的修饰名可能会发生变化。



    从 DLL 导出函数的最简单方法是按名称导出它们。例如,使用 __declspec(dllexport) 时所采用的就是这种方法。但也可以按序号导出函数。使用此技术时,必须使用 .def 文件而不是 __declspec(dllexport)。若要指定函数的序号值,请将其序号追加到 .def 文件中的函数名。有关指定序号的信息,请参见使用 .def 文件从 DLL 导出。

    Note提示

    如果希望优化 DLL 文件的大小,请对每个导出函数使用 NONAME 属性。使用 NONAME 属性时,序号存储在 DLL 的导出表中而非函数名中。如果导出许多函数,这样做可以节省相当多的空间。




0 0