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 函数添加 function 关键字。在模块定义文件中,为 DLL 声明 LIBRARY 语句和 EXPORTS 语句。以下代码是定义文件的示例。

// SampleDLL.def
//
LIBRARY "sampleDLL"

EXPORTS
   HelloWorld

编写示例 DLL

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

以下代码是使用 Win32 动态链接库项目类型在 Visual C++ 中创建的 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 文件夹的路径)。