DLL 快速指南



DLL - 简介

动态链接是一种在运行时将应用程序链接到库的机制。库保留在其自己的文件中,不会复制到应用程序的可执行文件中。DLL 在应用程序运行时链接到应用程序,而不是在应用程序创建时链接。DLL 可能包含到其他 DLL 的链接。

很多时候,DLL 放在扩展名不同的文件中,例如 .EXE、.DRV 或 .DLL。

DLL 的优点

下面列出了使用 DLL 文件的一些优点。

使用更少的资源

DLL 文件不会与主程序一起加载到 RAM 中;除非需要,否则它们不会占用空间。当需要 DLL 文件时,它会被加载并运行。例如,只要 Microsoft Word 用户正在编辑文档,打印机 DLL 文件就不需要在 RAM 中。如果用户决定打印文档,则 Word 应用程序会导致打印机 DLL 文件被加载并运行。

促进模块化架构

DLL 有助于开发模块化程序。它可以帮助您开发需要多种语言版本的大型程序或需要模块化架构的程序。模块化程序的一个示例是会计程序,它具有许多可以在运行时动态加载的模块。

有助于轻松部署和安装

当 DLL 中的函数需要更新或修复时,DLL 的部署和安装不需要将程序重新链接到 DLL。此外,如果多个程序使用相同的 DLL,则所有程序都将受益于更新或修复。当您使用定期更新或修复的第三方 DLL 时,此问题可能会更频繁地发生。

如果 DLL 链接在模块定义文件中的 IMPORTS 部分中指定为编译的一部分,则应用程序和 DLL 可以自动链接到其他 DLL。否则,您可以使用 Windows LoadLibrary 函数显式加载它们。

重要的 DLL 文件

  • COMDLG32.DLL - 控制对话框。

  • GDI32.DLL - 包含大量用于绘制图形、显示文本和管理字体的函数。

  • KERNEL32.DLL - 包含数百个用于管理内存和各种进程的函数。

  • USER32.DLL - 包含大量用户界面函数。参与程序窗口的创建及其相互交互。

DLL - 如何编写

首先,我们将讨论在开发您自己的 DLL 时应考虑的问题和要求。

DLL 的类型

当您在应用程序中加载 DLL 时,两种链接方法允许您调用导出的 DLL 函数。两种链接方法是

  • 加载时动态链接,以及
  • 运行时动态链接。

加载时动态链接

在加载时动态链接中,应用程序像本地函数一样对导出的 DLL 函数进行显式调用。要使用加载时动态链接,请在编译和链接应用程序时提供头文件 (.h) 和导入库文件 (.lib)。当您执行此操作时,链接器将向系统提供加载 DLL 和在加载时解析导出 DLL 函数位置所需的信息。

运行时动态链接

在运行时动态链接中,应用程序调用 LoadLibrary 函数或 LoadLibraryEx 函数在运行时加载 DLL。DLL 成功加载后,使用 GetProcAddress 函数获取要调用的导出 DLL 函数的地址。当您使用运行时动态链接时,不需要导入库文件。

以下列表描述了在加载时动态链接和运行时动态链接之间进行选择时的应用程序标准

  • 启动性能:如果应用程序的初始启动性能很重要,则应使用运行时动态链接。

  • 易用性:在加载时动态链接中,导出的 DLL 函数类似于本地函数。它可以帮助您轻松调用这些函数。

  • 应用程序逻辑:在运行时动态链接中,应用程序可以根据需要分支加载不同的模块。这在开发多种语言版本时非常重要。

DLL 入口点

创建 DLL 时,您可以选择指定一个入口点函数。当进程或线程附加到 DLL 或从 DLL 分离时,会调用入口点函数。您可以使用入口点函数根据 DLL 的需要初始化或销毁数据结构。

此外,如果应用程序是多线程的,则可以在入口点函数中使用线程局部存储 (TLS) 为每个线程分配私有内存。以下代码是 DLL 入口点函数的示例。

BOOL APIENTRY DllMain(
HANDLE hModule,	// Handle to DLL module DWORD ul_reason_for_call, LPVOID lpReserved )  // Reserved
{
   switch ( ul_reason_for_call )
   {
      case DLL_PROCESS_ATTACHED:
      // A process is loading the DLL.
      break;
      case DLL_THREAD_ATTACHED:
      // A process is creating a new thread.
      break;
      case DLL_THREAD_DETACH:
      // A thread exits normally.
      break;
      case DLL_PROCESS_DETACH:
      // A process unloads the DLL.
      break;
   }
   return TRUE;
}

如果使用加载时动态链接,则入口点函数返回 FALSE 值时,应用程序将无法启动。如果使用运行时动态链接,则只有单个 DLL 不会加载。

入口点函数应仅执行简单的初始化任务,不应调用任何其他 DLL 加载或终止函数。例如,在入口点函数中,不应直接或间接调用 LoadLibrary 函数或 LoadLibraryEx 函数。此外,在进程终止时,不应调用 FreeLibrary 函数。

警告:在多线程应用程序中,确保访问 DLL 全局数据的同步(线程安全),以避免可能的数据损坏。为此,请使用 TLS 为每个线程提供唯一的数据。

导出 DLL 函数

要导出 DLL 函数,您可以向导出的 DLL 函数添加函数关键字,或创建一个列出导出 DLL 函数的模块定义 (.def) 文件。

要使用应用程序中的函数关键字,必须使用以下关键字声明要导出的每个函数

 __declspec(dllexport)

要在应用程序中使用导出的 DLL 函数,必须使用以下关键字声明要导入的每个函数

 __declspec(dllimport)

通常,您将使用一个包含 define 语句和 ifdef 语句的头文件来分隔导出语句和导入语句。

您还可以使用模块定义文件来声明导出的 DLL 函数。当您使用模块定义文件时,不必向导出的 DLL 函数添加函数关键字。在模块定义文件中,声明 DLL 的 LIBRARY 语句和 EXPORTS 语句。以下代码是定义文件的示例。

// SampleDLL.def
//
LIBRARY "sampleDLL"

EXPORTS
   HelloWorld

编写示例 DLL

在 Microsoft Visual C++ 6.0 中,您可以通过选择 Win32 动态链接库项目类型或 MFC AppWizard (dll) 项目类型来创建 DLL。

以下代码是在 Visual C++ 中使用 Win32 动态链接库项目类型创建的 DLL 的示例。

// SampleDLL.cpp

#include "stdafx.h"
#define EXPORTING_DLL
#include "sampleDLL.h"

BOOL APIENTRY DllMain( HANDLE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved )
{
   return TRUE;
}

void HelloWorld()
{
   MessageBox( NULL, TEXT("Hello World"), 
   TEXT("In a DLL"), MB_OK);
}
// File: SampleDLL.h
//
#ifndef INDLL_H
#define INDLL_H

#ifdef EXPORTING_DLL
extern __declspec(dllexport) void HelloWorld() ;
#else
extern __declspec(dllimport) void HelloWorld() ;
#endif

#endif

调用示例 DLL

以下代码是调用 SampleDLL DLL 中导出 DLL 函数的 Win32 应用程序项目的示例。

// SampleApp.cpp 

#include "stdafx.h"
#include "sampleDLL.h"

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR     lpCmdLine,  int       nCmdShow)
{ 	
   HelloWorld();
   return 0;
}

注意:在加载时动态链接中,必须链接在构建 SampleDLL 项目时创建的 SampleDLL.lib 导入库。

在运行时动态链接中,您使用类似于以下代码的代码来调用 SampleDLL.dll 导出 DLL 函数。

...
typedef VOID (*DLLPROC) (LPTSTR);
...
HINSTANCE hinstDLL;
DLLPROC HelloWorld;
BOOL fFreeDLL;

hinstDLL = LoadLibrary("sampleDLL.dll");
if (hinstDLL != NULL)
{
   HelloWorld = (DLLPROC) GetProcAddress(hinstDLL, "HelloWorld");
	
   if (HelloWorld != NULL)
   (HelloWorld);

   fFreeDLL = FreeLibrary(hinstDLL);
}
...

编译和链接 SampleDLL 应用程序时,Windows 操作系统将按以下顺序搜索以下位置中的 SampleDLL DLL

  • 应用程序文件夹

  • 当前文件夹

  • Windows 系统文件夹(GetSystemDirectory 函数返回 Windows 系统文件夹的路径)。

  • Windows 文件夹(GetWindowsDirectory 函数返回 Windows 文件夹的路径)。

DLL - 注册

为了使用 DLL,它必须通过在注册表中输入相应的引用来注册。有时会发生注册表引用损坏并且无法再使用 DLL 的函数的情况。可以通过打开“开始-运行”并输入以下命令来重新注册 DLL

regsvr32 somefile.dll

此命令假定 somefile.dll 位于 PATH 中的目录或文件夹中。否则,必须使用 DLL 的完整路径。也可以使用开关“/u”取消注册 DLL 文件,如下所示。

regsvr32 /u somefile.dll

这可用于打开和关闭服务。

DLL - 工具

有几个工具可用于帮助您解决 DLL 问题。下面讨论其中一些。

依赖项查看器

依赖项查看器工具(depends.exe)可以递归扫描程序使用的所有依赖 DLL。当您在依赖项查看器中打开程序时,依赖项查看器会执行以下检查

  • 检查缺少的 DLL。
  • 检查无效的程序文件或 DLL。
  • 检查导入函数和导出函数是否匹配。
  • 检查循环依赖错误。
  • 检查由于模块适用于不同的操作系统而无效的模块。

通过使用依赖项查看器,您可以记录程序使用的所有 DLL。这可能有助于防止和纠正将来可能发生的 DLL 问题。安装 Microsoft Visual Studio 6.0 后,依赖项查看器位于以下目录中

drive\Program Files\Microsoft Visual Studio\Common\Tools

DLL 通用问题解决器

DLL 通用问题解决器 (DUPS) 工具用于审核、比较、记录和显示 DLL 信息。以下列表描述了构成 DUPS 工具的实用程序

  • Dlister.exe - 此实用程序枚举计算机上的所有 DLL 并将信息记录到文本文件或数据库文件中。

  • Dcomp.exe - 此实用程序比较两个文本文件中列出的 DLL,并生成一个包含差异的第三个文本文件。

  • Dtxt2DB.exe - 此实用程序将使用 Dlister.exe 实用程序和 Dcomp.exe 实用程序创建的文本文件加载到 dllHell 数据库中。

  • DlgDtxt2DB.exe - 此实用程序提供 Dtxt2DB.exe 实用程序的图形用户界面 (GUI) 版本。

DLL - 提示

编写 DLL 时请记住以下提示

  • 使用正确的调用约定(C 或 stdcall)。

  • 注意传递给函数的参数的正确顺序。

  • **切勿**使用直接传递给函数的参数调整数组大小或连接字符串。请记住,您传递的参数是 LabVIEW 数据。更改数组或字符串的大小可能会导致覆盖存储在 LabVIEW 内存中的其他数据,从而导致崩溃。**如果**您传递 LabVIEW 数组句柄或 LabVIEW 字符串句柄,并且使用 Visual C++ 编译器或 Symantec 编译器编译 DLL,**则可以**调整数组大小或连接字符串。

  • 将字符串传递给函数时,选择要传递的正确字符串类型。C 或 Pascal 或 LabVIEW 字符串句柄。

  • Pascal 字符串的长度限制为 255 个字符。

  • C 字符串以 NULL 结尾。如果您的 DLL 函数以二进制字符串格式(例如,通过 GPIB 或串口)返回数字数据,它可能会将 NULL 值作为数据字符串的一部分返回。在这种情况下,传递短整数(8 位)数组最可靠。

  • 如果您正在使用数组或字符串数据,**始终**传递一个足够大的缓冲区或数组来容纳函数放入缓冲区的任何结果,除非您将它们作为 LabVIEW 句柄传递,在这种情况下,您可以使用 Visual C++ 或 Symantec 编译器下的 CIN 函数调整它们的大小。

  • 如果您使用的是 _stdcall,请在模块定义文件的 EXPORTS 部分列出 DLL 函数。

  • 在模块定义文件的 EXPORTS 部分列出其他应用程序调用的 DLL 函数,或在函数声明中包含 _declspec (dllexport) 关键字。

  • 如果您使用 C++ 编译器,请在您的头文件中使用 extern .C.{} 语句导出函数,以防止名称修饰。

  • 如果您正在编写自己的 DLL,则不应在另一个应用程序将 DLL 加载到内存中时重新编译 DLL。在重新编译 DLL 之前,请确保使用该特定 DLL 的所有应用程序都已从内存中卸载。它确保 DLL 本身不会加载到内存中。如果您忘记这一点并且您的编译器没有警告您,您可能会无法正确重建。

  • 使用另一个程序测试您的 DLL,以确保函数(和 DLL)的行为正确。使用编译器的调试器或一个简单的 C 程序(您可以在其中调用 DLL 中的函数)对其进行测试将帮助您确定可能的困难是 DLL 本身固有的还是与 LabVIEW 相关的。

DLL - 示例

我们已经了解了如何编写 DLL 以及如何创建“Hello World”程序。该示例必须让您了解创建 DLL 的基本概念。

在这里,我们将介绍使用 Delphi、Borland C++ 和 VC++ 创建 DLL 的方法。

让我们逐一查看这些示例。

广告