TN033:MFC 的 DLL 版本

本说明介绍如何将 MFCxx.DLLMFCxxD.DLL(其中 xx 是 MFC 版本号)共享动态链接库用于 MFC 应用程序和 MFC 扩展 DLL。 有关规则 MFC DLL 的详细信息,请参阅将 MFC 用作 DLL 的一部分

此技术说明涵盖 DLL 的三个方面。 后两个方面适用于更高级的用户:

如果你有兴趣生成使用 MFC 并且可用于非 MFC 应用程序的 DLL(称为规则 MFC DLL),请参阅技术说明 11

MFCxx.DLL 支持概述:术语和文件

规则 MFC DLL:使用规则 MFC DLL 可生成使用一些 MFC 类的独立 DLL。 跨应用/DLL 边界的接口是“C”接口,客户端应用程序不必是 MFC 应用程序。

规则 MFC DLL 是 MFC 1.0 中支持的 DLL 版本。 它们在技术说明 11 和 MFC 高级概念示例 DLLScreenCap 中进行了介绍。

注意

从 Visual C++ 版本 4.0 开始,术语 USRDLL 已过时,替换为静态链接到 MFC 的规则 MFC DLL。 还可以生成动态链接到 MFC 的规则 MFC DLL。

MFC 3.0(及更高版本)支持具有所有新功能(包括 OLE 和数据库类)的规则 MFC DLL。

AFXDLL:也称为 MFC 库的共享版本。 它是 MFC 2.0 中添加的新 DLL 支持。 MFC 库本身处于多个 DLL 中(如下所述)。 客户端应用程序或 DLL 会动态链接所需的 DLL。 跨应用程序/DLL 边界的接口是 C++/MFC 类接口。 客户端应用程序必须是 MFC 应用程序。 此 DLL 支持所有 MFC 3.0 功能(例外:数据库类不支持 UNICODE)。

注意

从 Visual C++ 版本 4.0 开始,此类型的 DLL 称为“扩展 DLL”。

本说明会使用 MFCxx.DLL 引用整个 MFC DLL 集,其中包括:

  • 调试:MFCxxD.DLL(合并)和 MFCSxxD.LIB(静态)。

  • 发布:MFCxx.DLL(合并)和 MFCSxx.LIB(静态)。

  • Unicode 调试:MFCxxUD.DLL(合并)和 MFCSxxD.LIB(静态)。

  • Unicode 发布:MFCxxU.DLL(合并)和 MFCSxxU.LIB(静态)。

注意

MFCSxx[U][D].LIB 库可与 MFC 共享 DLL 结合使用。 这些库包含必须静态链接到应用程序或 DLL 的代码。

应用程序链接到对应的导入库:

  • 调试:MFCxxD.LIB

  • 发布:MFCxx.LIB

  • Unicode 调试:MFCxxUD.LIB

  • Unicode 发布:MFCxxU.LIB

MFC 扩展 DLL 是扩展 MFCxx.DLL(或其他 MFC 共享 DLL)的 DLL。 MFC 组件体系结构在这里开始发挥作用。 如果从 MFC 类派生有用的类,或生成另一个类似于 MFC 的工具包,则可以将它放置在 DLL 中。 DLL 会使用 MFCxx.DLL,最终客户端应用程序也是如此。 MFC 扩展 DLL 允许可重用叶类、可重用基类以及可重用视图和文档类。

优点和缺点

为何应使用 MFC 的共享版本

  • 使用共享库可能会生成较小的应用程序。 (使用大多数 MFC 库的最小应用程序小于 10K)。

  • MFC 的共享版本支持 MFC 扩展 DLL 和规则 MFC DLL。

  • 生成使用共享 MFC 库的应用程序比静态链接 MFC 应用程序速度更快。 这是因为不需要链接 MFC 本身。 在链接器必须压缩调试信息的 DEBUG 生成中更是如此。 当应用程序链接到已包含调试信息的 DLL 时,要压缩的调试信息较少。

为何不应使用 MFC 的共享版本:

  • 提供使用共享库的应用程序需要随程序一起提供 MFCxx.DLL 和其他库。 MFCxx.DLL 可以如同许多 DLL 一样随意再发行,但仍然必须在安装程序中安装该 DLL。 此外,还必须提供程序使用的其他可再发行库和 MFC DLL 本身。

如何编写 MFC 扩展 DLL

MFC 扩展 DLL 是包含用于扩展 MFC 类功能的类和函数的 DLL。 MFC 扩展 DLL 使用共享 MFC DLL 的方式与应用程序使用它们的方式相同,有一些其他注意事项:

  • 生成过程类似于生成使用共享 MFC 库的应用程序,具有几个额外编译器和链接器选项。

  • MFC 扩展 DLL 没有 CWinApp 派生类。

  • MFC 扩展 DLL 必须提供特殊的 DllMain。 AppWizard 提供可以修改的 DllMain 函数。

  • 如果 MFC 扩展 DLL 将 CRuntimeClass 类型或资源导出到应用程序,则 MFC 扩展 DLL 通常提供初始化例程来创建 CDynLinkLibrary。 如果每个应用程序数据必须由 MFC 扩展 DLL 维护,则可以使用 CDynLinkLibrary 的派生类。

这些注意事项在下面进行了更详细的介绍。 另请参阅 MFC 高级概念示例 DLLHUSK。 它演示如何:

  • 使用共享库生成应用程序。 (DLLHUSK.EXE 是 MFC 应用程序,会动态链接到 MFC 库和其他 DLL。)

  • 生成 MFC 扩展 DLL。 (它演示生成 MFC 扩展 DLL. 时如何使用特殊标志,如 _AFXEXT。)

  • 生成 MFC 扩展 DLL 的两个示例。 一个演示具有有限导出的 MFC 扩展 DLL 基本结构 (TESTDLL1),另一个演示导出整个类接口 (TESTDLL2)。

客户端应用程序和任何 MFC 扩展 DLL 都必须使用相同版本的 MFCxx.DLL。 遵循 MFC DLL 的约定,并提供 MFC 扩展 DLL 的调试和发布 (/release) 版本。 这种做法允许客户端程序生成其应用程序的调试和发布版本,并将其链接到所有 DLL 的相应调试或发布版本。

注意

由于 C++ 名称重整和导出问题,一个 MFC 扩展 DLL 中的导出列表在同一个 DLL 的调试版本和发布版本之间以及用于不同平台的 DLL 之间可能不同。 发布 MFCxx.DLL 具有大约 2000 个导出入口点;调试 MFCxxD.DLL 具有大约 3000 个导出入口点。

有关内存管理的快速说明

本技术说明接近末尾处标题为“内存管理”的部分介绍了具有 MFC 的共享版本的 MFCxx.DLL 实现。 此处介绍了仅实现 MFC 扩展 DLL 所需的信息。

MFCxx.DLL 以及加载到客户端应用程序地址空间中的所有 MFC 扩展 DLL 会使用相同的内存分配器、资源加载和其他 MFC“全局”状态,就如同它们位于同一个应用程序中一样。 这十分重要,因为非 MFC DLL 库和静态链接到 MFC 的规则 MFC DLL 恰好相反:每个 DLL 都从其自己的内存池中进行分配。

如果 MFC 扩展 DLL 分配内存,则该内存可以自由地与任何其他应用程序分配的对象混合。 此外,如果使用共享 MFC 库的应用程序崩溃,则操作系统会维护共享 DLL 的任何其他 MFC 应用程序的完整性。

同样,其他“全局”MFC 状态(如从其加载资源的当前可执行文件)也在客户端应用程序、所有 MFC 扩展 DLL 与 MFCxx.DLL 本身之间共享。

生成 MFC 扩展 DLL

可以使用 AppWizard 创建 MFC 扩展 DLL 项目,它会自动生成相应的编译器和链接器设置。 它还会生成可以修改的 DllMain 函数。

如果要将现有项目转换为 MFC 扩展 DLL,请从使用 MFC 的共享版本生成的标准设置开始。 然后,进行以下更改:

  • /D_AFXEXT 添加到编译器标志。 在“项目属性”对话框中,选择“C/C++”>“预处理器”类别。 将 _AFXEXT 添加到“定义宏”字段,用分号分隔每个项。

  • 移除 /Gy 编译器开关。 在“项目属性”对话框中,选择“C/C++”>“代码生成”类别。 确保未启用“启用函数级链接”属性。 通过此设置可更轻松地导出类,因为链接器不会移除未引用的函数。 如果原始项目生成静态链接到 MFC 的规则 MFC DLL,请将 /MT(或 /MTd)编译器选项更改为 /MD(或 /MDd)。

  • 使用设置为 LINK 的 /DLL 选项生成导出库。 创建新目标并将 Win32 动态链接库指定为目标类型时,会设置此选项。

更改头文件

MFC 扩展 DLL 的通常目标是将某个常用功能导出到可以使用该功能的一个或多个应用程序。 实质上,DLL 导出类和全局函数供客户端应用程序使用。

若要确保每个成员函数都根据情况标记为导入或导出,请使用特殊声明 __declspec(dllexport)__declspec(dllimport)。 当客户端应用程序使用你的类时,你希望它们声明为 __declspec(dllimport)。 生成 MFC 扩展 DLL 本身时,函数应声明为 __declspec(dllexport)。 生成的 DLL 还必须导出函数,以便客户端程序可以在加载时绑定到它们。

若要导出整个类,请在类定义中使用 AFX_EXT_CLASS。 框架在定义了 _AFXDLL_AFXEXT 时将此宏定义为 __declspec(dllexport),但是未定义 _AFXEXT 时,将它定义为 __declspec(dllimport)。 仅在生成 MFC 扩展 DLL 时才会定义 _AFXEXT。 例如:

class AFX_EXT_CLASS CExampleExport : public CObject
{ /* ... class definition ... */ };

不导出整个类

有时可能要仅导出类的单个必需成员。 例如,如果要导出 CDialog 派生类,则可能只需要导出构造函数和 DoModal 调用。 可以使用 DLL 的 DEF 文件导出这些成员,但也可以采用大致相同的方式对需要导出的单个成员使用 AFX_EXT_CLASS

例如:

class CExampleDialog : public CDialog
{
public:
    AFX_EXT_CLASS CExampleDialog();
    AFX_EXT_CLASS int DoModal();
    // rest of class definition
    // ...
};

执行此操作时,可能会遇到其他问题,因为不会导出类的所有成员。 问题在于 MFC 宏的工作方式。 MFC 的一些帮助程序宏实际上声明或定义数据成员。 DLL 也需要导出这些数据成员。

例如,在生成 MFC 扩展 DLL 时,DECLARE_DYNAMIC 宏会定义如下:

#define DECLARE_DYNAMIC(class_name) \
protected: \
    static CRuntimeClass* PASCAL _GetBaseClass(); \
    public: \
    static AFX_DATA CRuntimeClass class##class_name; \
    virtual CRuntimeClass* GetRuntimeClass() const; \

static AFX_DATA 开头的行会在类的内部声明静态对象。 若要正确导出此类并从客户端 EXE 访问运行时信息,需要导出此静态对象。 由于静态对象使用修饰符 AFX_DATA 进行声明,因此只需在生成 DLL 时将 AFX_DATA 定义为 __declspec(dllexport)。 在生成客户端可执行文件时将它定义为 __declspec(dllimport)

如上所述,AFX_EXT_CLASS 已采用这种方式进行了定义。 只需将 AFX_EXT_CLASS 重新定义为与类定义中的 AFX_DATA 相同。

例如:

#undef  AFX_DATA
#define AFX_DATA AFX_EXT_CLASS
class CExampleView : public CView
{
    DECLARE_DYNAMIC()
    // ... class definition ...
};
#undef  AFX_DATA
#define AFX_DATA

MFC 始终对它在其宏中定义的数据项使用 AFX_DATA 符号,所以此方法适用于所有此类方案。 例如,它会适用于DECLARE_MESSAGE_MAP。

注意

如果要导出整个类,而不是导出类的所选成员,则会自动导出静态数据成员。

可以使用相同方法为使用 DECLARE_SERIAL 和 IMPLEMENT_SERIAL 宏的类自动导出 CArchive 提取运算符。 通过使用以下代码将类声明(位于头文件中)括起来,来导出存档运算符:

#undef AFX_API
#define AFX_API AFX_EXT_CLASS

/* your class declarations here */

#undef AFX_API
#define AFX_API

_AFXEXT 的限制

只要没有多层 MFC 扩展 DLL,便可以对 MFC 扩展 DLL 使用 _AFXEXT 预处理器符号。 如果 MFC 扩展 DLL 调用或派生自你自己的 MFC 扩展 DLL 中的类,然后从 MFC 类派生,则必须使用你自己的预处理器符号以避免歧义。

问题在于,在 Win32 中,必须将要从 DLL 中导出的任何数据显式声明为 __declspec(dllexport),且必须将要从 DLL 中导入的任何数据显式声明为 __declspec(dllimport)。 定义 _AFXEXT 时,MFC 标头确保了正确定义 AFX_EXT_CLASS

如果有多个层,则一个符号(例如 AFX_EXT_CLASS)是不够的:MFC 扩展 DLL 可能会导出自己的类,也从其他 MFC 扩展 DLL 导入其他类。 若要处理此问题,请使用一个特殊的预处理器符号,指示你在生成 DLL 本身,而不是在使用 DLL。 例如,假设两个 MFC 扩展 DLL:A.DLLB.DLL。 它们各自分别导出 A.HB.H 中的一些类。 B.DLL 使用来自 A.DLL 的类。 头文件类似于以下形式:

/* A.H */
#ifdef A_IMPL
    #define CLASS_DECL_A   __declspec(dllexport)
#else
    #define CLASS_DECL_A   __declspec(dllimport)
#endif

class CLASS_DECL_A CExampleA : public CObject
{ /* ... class definition ... */ };

/* B.H */
#ifdef B_IMPL
    #define CLASS_DECL_B   __declspec(dllexport)
#else
    #define CLASS_DECL_B   __declspec(dllimport)
#endif

class CLASS_DECL_B CExampleB : public CExampleA
{ /* ... class definition ... */ };

生成 A.DLL 时,它是使用 /DA_IMPL 生成的,生成 B.DLL 时,它是使用 /DB_IMPL 生成的。 通过对每个 DLL 使用单独的符号,在生成 B.DLL 时导出 CExampleB 并导入 CExampleACExampleA 在生成 A.DLL 时导出,在被 B.DLL(或某个其他客户端)使用时导入。

使用内置 AFX_EXT_CLASS_AFXEXT 预处理器符号时,无法执行这种类型的分层。 上述方法采用与 MFC 相同的方式解决此问题。 MFC 在生成 OLE、数据库和网络 MFC 扩展 DLL 时使用此方法。

仍不导出整个类

同样,在不导出整个类时必须特别小心。 请确保正确导出由 MFC 宏创建的必需数据项。 可以通过对特定类的宏重新定义 AFX_DATA 来执行此操作。 每当不导出整个类时便重新定义它。

例如:

// A.H
#ifdef A_IMPL
    #define CLASS_DECL_A  _declspec(dllexport)
#else
    #define CLASS_DECL_A  _declspec(dllimport)
#endif

#undef  AFX_DATA
#define AFX_DATA CLASS_DECL_A

class CExampleA : public CObject
{
    DECLARE_DYNAMIC()
    CLASS_DECL_A int SomeFunction();
    // class definition
    // ...
};

#undef AFX_DATA
#define AFX_DATA

DllMain

下面是应在 MFC 扩展 DLL 的主源文件中放置的代码。 它应在标准包含之后。 使用 AppWizard 为 MFC 扩展 DLL 创建起始文件时,它会为你提供 DllMain

#include "afxdllx.h"

static AFX_EXTENSION_MODULE extensionDLL;

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID)
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      // MFC extension DLL one-time initialization
      if (!AfxInitExtensionModule(
             extensionDLL, hInstance))
         return 0;

      // TODO: perform other initialization tasks here
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      // MFC extension DLL per-process termination
      AfxTermExtensionModule(extensionDLL);

      // TODO: perform other cleanup tasks here
   }
   return 1;   // ok
}

调用 AfxInitExtensionModule 会捕获模块的运行时类(CRuntimeClass 结构),以及其对象工厂(COleObjectFactory 对象),以便稍后在创建 CDynLinkLibrary 对象时使用。 (可选)调用 AfxTermExtensionModule 允许 MFC 在每个进程从 MFC 扩展 DLL 分离时清理 MFC 扩展 DLL(进程退出时或 DLL 由 FreeLibrary 调用卸载时发生)。 由于大多数 MFC 扩展 DLL 不会动态加载(它们通常通过导入库链接),因此通常不需要调用 AfxTermExtensionModule

如果应用程序动态加载并释放 MFC 扩展 DLL,请务必按如上所示调用 AfxTermExtensionModule。 此外,如果应用程序使用多个线程,或者如果它动态地加载 MFC 扩展 DLL,请确保使用 AfxLoadLibraryAfxFreeLibrary(而不是 Win32 函数 LoadLibraryFreeLibrary)。 使用 AfxLoadLibraryAfxFreeLibrary 可以确保在加载和卸载 MFC 扩展 DLL 时执行的启动和关闭代码不会损坏全局 MFC 状态。

头文件 AFXDLLX.H 包含 MFC 扩展 DLL 中使用的结构的特殊定义,例如 AFX_EXTENSION_MODULECDynLinkLibrary 的定义。

全局 extensionDLL 必须按如下所示进行声明。 与 MFC 的 16 位版本不同,可以在此时间内分配内存并调用 MFC 函数,因为 MFCxx.DLL 在调用 DllMain 时已完全初始化。

共享资源和类

简单 MFC 扩展 DLL 只需要将一些低带宽函数导出到客户端应用程序,仅此而已。 更多用户界面密集型 DLL 可能需要将资源和 C++ 类导出到客户端应用程序。

导出资源通过资源列表来进行。 在每个应用程序中都是 CDynLinkLibrary 对象的单独链接列表。 查找资源时,加载资源的大多数标准 MFC 实现首先查看当前资源模块 (AfxGetResourceHandle),如果未找到,则会遍历尝试加载所请求资源的 CDynLinkLibrary 对象的列表。

给定 C++ 类型的 C++ 对象以类似方式动态创建。 MFC 对象反序列化机制需要注册所有 CRuntimeClass 对象,以便它可以通过基于以前存储的内容动态创建所需类型的 C++ 对象来重新构造。

如果希望客户端应用程序使用 MFC 扩展 DLL 中为 DECLARE_SERIAL 的类,则需要导出类,使其对客户端应用程序可见。 它也通过遍历 CDynLinkLibrary 列表来完成。

在 MFC 高级概念示例 DLLHUSK 中,列表如下所示:

head ->   DLLHUSK.EXE   - or - DLLHUSK.EXE
               |                    |
          TESTDLL2.DLL         TESTDLL2.DLL
               |                    |
          TESTDLL1.DLL         TESTDLL1.DLL
               |                    |
               |                    |
           MFC90D.DLL           MFC90.DLL

MFCxx.DLL 条目通常位于资源和类列表的最后。 MFCxx.DLL 包含所有标准 MFC 资源,包括所有标准命令 ID 的提示字符串。 将它放在列表末尾使 DLL 和客户端应用程序本身可以依赖于 MFCxx.DLL 中的共享资源,而不是具有自己的副本。

将所有 DLL 的资源和类名合并到客户端应用程序的命名空间的缺点在于,必须谨慎处理选择的 ID 或名称。 可以通过不将资源或 CDynLinkLibrary 对象导出到客户端应用程序来禁用此功能。 DLLHUSK 示例使用多个头文件管理共享资源命名空间。 有关使用共享资源文件的更多提示,请参阅技术说明 35

初始化 DLL

如上所述,通常需要创建 CDynLinkLibrary 对象以将资源和类导出到客户端应用程序。 需要提供已导出的入口点来初始化 DLL。 它至少是不采用任何参数并且不返回任何内容的 void 例程,不过可以是你所喜欢的任何内容。

如果使用此方法,则要使用 DLL 的每个客户端应用程序都必须调用此初始化例程。 也可以仅在调用 AfxInitExtensionModule 后在 DllMain 中分配此 CDynLinkLibrary 对象。

初始化例程必须在当前应用程序的堆中创建 CDynLinkLibrary 对象,连接到 MFC 扩展 DLL 信息。 可以通过定义如下所示的函数来执行此操作:

extern "C" extern void WINAPI InitXxxDLL()
{
    new CDynLinkLibrary(extensionDLL);
}

此示例中的例程名称 InitXxxDLL 可以是所需的任何内容。 它不需要是 extern "C",但它使导出列表更易于维护。

注意

如果从规则 MFC DLL 使用 MFC 扩展 DLL,则必须导出此初始化函数。 在使用任何 MFC 扩展 DLL 类或资源之前,都必须从规则 MFC DLL 调用此函数。

导出条目

导出类的简单方法是对要导出的每个类和全局函数使用 __declspec(dllimport)__declspec(dllexport)。 这要简单得多,但如下所述,这比在 DEF文件中命名每个入口点的效率更低。 这是因为你对导出哪些函数的控制较少。 而且无法按序号导出函数。 TESTDLL1 和 TESTDLL2 使用此方法导出其条目。

更高效的方法是通过在 DEF 文件中进行命名来导出每个条目。 MFCxx.DLL 使用此方法。 由于我们从 DLL 有选择地导出,因此必须确定要导出的特定接口。 这很困难,因为必须在 DEF 文件中以条目的形式指定链接器的重整名称。 请勿导出任何 C++ 类,除非确实需要为其创建符号链接。

如果以前尝试过使用 DEF 文件导出 C++ 类,则可能要开发工具以自动生成此列表。 可以使用两阶段链接过程完成此操作。 在没有导出的情况下链接 DLL 一次,并允许链接器生成映射文件。 映射文件包含应导出的函数列表。 通过进行一些重新排列,可以使用它为 DEF 文件生成导出条目。 MFCxx.DLL 以及 OLE 和数据库 MFC 扩展 DLL 的导出列表(数量为数千个)是使用此类过程生成的(尽管它不是完全自动的,有时需要进行一些手动优化)。

CWinApp 与 CDynLinkLibrary

MFC 扩展 DLL 没有其自己的 CWinApp 派生对象。 而是必须使用客户端应用程序的 CWinApp 派生对象。 这意味着客户端应用程序拥有主消息泵、空闲循环等。

如果 MFC 扩展 DLL 需要维护每个应用程序的额外数据,则可以从 CDynLinkLibrary 派生新类并在上面介绍的 InitXxxDLL 例程中创建它。 在运行时,DLL 可以检查当前应用程序的 CDynLinkLibrary 对象列表,以查找该特定 MFC 扩展 DLL 的对应对象。

在 DLL 实现中使用资源

如上所述,默认资源加载会遍历 CDynLinkLibrary 对象的列表,从而查找具有请求资源的第一个 EXE 或 DLL。 所有 MFC API 和所有内部代码都使用 AfxFindResourceHandle 遍历资源列表来查找任何资源,无论资源位于何处。

如果不想只从特定位置加载资源,请使用 API AfxGetResourceHandleAfxSetResourceHandle 保存旧句柄并设置新句柄。 在返回到客户端应用程序之前,请务必还原旧资源句柄。 示例 TESTDLL2 使用此方法显式加载菜单。

遍历该列表具有一些缺点:速度稍微较慢,需要管理资源 ID 范围。 其优点是链接到多个 MFC 扩展 DLL 的客户端应用程序可以使用任何 DLL 提供的资源,而无需指定 DLL 实例句柄。 AfxFindResourceHandle 是用于遍历资源列表以查找给定匹配项的 API。 它采用资源的名称和类型,并返回首次找到该资源的资源句柄(或 NULL)。

编写使用 DLL 版本的应用程序

应用程序要求

使用 MFC 的共享版本的应用程序必须遵循一些基本规则:

  • 它必须具有 CWinApp 对象,并遵循消息泵的标准规则。

  • 它必须使用一组所需的编译器标志进行编译(请参阅下文)。

  • 它必须与 MFCxx 导入库链接。 通过设置所需的编译器标志,MFC 标头可在链接时确定应用程序应与之链接的库。

  • 若要运行可执行文件,MFCxx.DLL 必须位于路径上或 Windows 系统目录中。

使用开发环境进行生成

如果使用包含大多数标准默认值的内部生成文件,则可以轻松更改项目以生成 DLL 版本。

以下步骤假定你具有与 NAFXCWD.LIB(用于调试)和 NAFXCW.LIB(用于发布)链接的正确运行的 MFC 应用程序,并且你要将它转换为使用 MFC 库的共享版本。 你在运行 Visual Studio 环境并具有内部项目文件。

  1. 在“项目”菜单上选择“属性”。 在“常规”页中的“项目默认值”下,将 Microsoft 基础类设置为“在共享 DLL 中使用 MFC”(MFCxx(d).dll)

使用 NMAKE 进行生成

如果使用编译器的外部生成文件功能,或者直接使用 NMAKE,则必须编辑生成文件以支持所需的编译器和链接器选项。

所需的编译器标志:

  • /D_AFXDLL /MD /D_AFXDLL

标准 MFC 标头需要定义 _AFXDLL 符号。

  • /MD 应用程序必须使用 C 运行时库的 DLL 版本。

所有其他编译器标志都遵循 MFC 默认值(例如用于调试的 _DEBUG)。

编辑库的链接器列表。 将 NAFXCWD.LIB 更改为 MFCxxD.LIB 并将 NAFXCW.LIB 更改为 MFCxx.LIB。 将 LIBC.LIB 替换为 MSVCRT.LIB。 与任何其他 MFC 库一样,请务必将 MFCxxD.LIB 放置在任何 C 运行时库之前

可以选择将 /D_AFXDLL 添加到发布和调试资源编译器选项(实际使用 /R 编译资源的选项)。 此选项通过共享 MFC DLL 中存在的资源来缩小最终可执行文件。

完成这些更改后,需要进行完整重新生成。

生成示例

大多数 MFC 示例程序可以从 Visual C++ 或是从与 NMAKE 兼容的共享生成文件(通过命令行)生成。

若要转换这些示例中的任何一个以使用 MFCxx.DLL,可以将 MAK 文件加载到 Visual C++ 中并设置项目选项(如上所述)。 如果使用 NMAKE 生成,则可以在 NMAKE 命令行上指定 AFXDLL=1,这会使用共享 MFC 库生成示例。

MFC 高级概念示例 DLLHUSK 是使用 MFC 的 DLL 版本生成的。 此示例不仅演示如何生成与 MFCxx.DLL 链接的应用程序,还演示 MFC DLL 打包选项的其他功能(如本技术说明后面部分介绍的 MFC 扩展 DLL)。

打包说明

DLL 的发布版本(MFCxx.DLLMFCxxU.DLL)可随意再发行。 DLL 的调试版本不可随意再发行,只应在应用程序开发期间使用。

调试 DLL 随调试信息一起提供。 通过使用 Visual C++ 调试器,可以跟踪应用程序和 DLL 的执行。 发布 DLL(MFCxx.DLLMFCxxU.DLL)不包含调试信息。

如果自定义或重新生成 DLL,则应将它们命名为“MFCxx”以外的名称。 MFC SRC 文件 MFCDLL.MAK 描述生成选项,并包含用于重命名 DLL 的逻辑。 重命名文件是必需操作,因为这些 DLL 可能由许多 MFC 应用程序共享。 让 MFC DLL 的自定义版本替换系统上安装的 MFC DLL 可能会中断使用共享 MFC DLL 的其他 MFC 应用程序。

不建议重新生成 MFC DLL。

如何实现 MFCxx.DLL

以下部分介绍如何实现 MFC DLL(MFCxx.DLLMFCxxD.DLL)。 如果只是想将 MFC DLL 用于应用程序,则了解此处的详细信息也并不重要。 此处的详细信息对于了解如何编写 MFC 扩展 DLL 并不是必需的,不过了解此实现可能有助于编写自己的 DLL。

实现概述

如上所述,MFC DLL 实际上是 MFC 扩展 DLL 的特殊情况。 对于大量类,它具有大量导出。 在 MFC DLL 中,我们进行了一些额外工作,使它比规则 MFC 扩展 DLL 更特殊。

Win32 执行大部分工作

MFC 的 16 位版本需要一些特殊技术,包括堆栈段上的每应用数据、由一些 80x86 程序集代码创建的特殊段、每进程异常上下文和其他技术。 Win32 直接支持 DLL 中的每进程数据,这是你在大多数时间需要的功能。 对于大多数部件,MFCxx.DLL 只是在 DLL 中打包的 NAFXCW.LIB。 如果你查看 MFC 源代码,则很少会发现 #ifdef _AFXDLL 用例,因为不需要很多特殊用例。 有一些在 Windows 3.1 上专门处理 Win32 的特殊用例(也称为 Win32s)。 Win32s 不直接支持每进程 DLL 数据。 MFC DLL 必须使用线程本地存储 (TLS) Win32 API 获取进程本地数据。

对库源、其他文件的影响

_AFXDLL 版本对普通 MFC 类库源和标头的影响相对较小。 主 AFXWIN.H 标头包含一个特殊的版本文件 (AFXV_DLL.H) 和一个附加头文件 (AFXDLL_.H)。 AFXDLL_.H 标头包含 _AFXDLL 应用程序和 MFC 扩展 DLL 的 CDynLinkLibrary 类和其他实现详细信息。 提供了 AFXDLLX.H 标头以用于生成 MFC 扩展 DLL(请参阅上文以了解详细信息)。

MFC SRC 中 MFC 库的规则源在 _AFXDLL ifdef 下具有一些额外的条件代码。 附加源文件 (DLLINIT.CPP) 包含用于 MFC 的共享版本的额外 DLL 初始化代码和其他粘附。

为了生成 MFC 的共享版本,会提供附加文件。 (请参阅下文,以了解有关如何生成 DLL 的详细信息。)

  • 两个 DEF 文件用于为 DLL 的调试 (MFCxxD.DEF) 和发布 (MFCxx.DEF) 版本导出 MFC DLL 入口点。

  • 一个 RC 文件 (MFCDLL.RC) 包含 DLL 的所有标准 MFC 资源和 VERSIONINFO 资源。

  • 提供了一个 CLW 文件 (MFCDLL.CLW),以便可以使用 ClassWizard 浏览 MFC 类。 此功能并不特定于 MFC 的 DLL 版本。

内存管理

使用 MFCxx.DLL 的应用程序会使用 MSVCRTxx.DLL(共享 C 运行时 DLL)提供的通用内存分配器。 应用程序、任何 MFC 扩展 DLL 以及 MFC DLL 都使用此共享内存分配器。 通过使用共享 DLL 进行内存分配,MFC DLL 可以分配稍后会由应用程序释放的内存,反之亦然。 由于应用程序和 DLL 都必须使用相同的分配器,因此不应替代 C++ 全局 operator newoperator delete。 相同的规则适用于 C 运行时内存分配例程的其余部分(例如 mallocreallocfree 等等)。

序号和 class __declspec(dllexport) 以及 DLL 命名

我们不使用 C++ 编译器的 class__declspec(dllexport) 功能。 相反,类库源(MFCxx.DEFMFCxxD.DEF)中包含导出列表。 仅导出一组选定的入口点(函数和数据)。 不会导出其他符号,例如 MFC 专用实现函数或类。 所有导出都按序号执行,在驻留或非驻留名称表中没有字符串名称。

使用 class__declspec(dllexport) 可能是生成较小 DLL 的可行替代方法,但在大型 DLL(如 MFC)中,默认导出机制具有效率和容量限制。

这一切意味着我们可以在只有大约 800 KB 的发布 MFCxx.DLL 中打包大量功能,而不会影响执行速度或加载速度。 如果未使用此方法,则 MFCxx.DLL 的大小会增加 100 KB。 通过此方法,可以在 DEF 文件末尾添加额外入口点。 它允许进行简单的版本控制,而不会影响按序号导出的速度和大小效率。 MFC 类库中的主版本修订会更改库名称。 也就是说,MFC30.DLL 是包含 MFC 类库版本 3.0 的可再发行 DLL。 在此 DLL 升级时,例如在假设的 MFC 3.1 中,此 DLL 会改为命名为 MFC31.DLL。 同样,如果修改 MFC 源代码以生成 MFC DLL 的自定义版本,请使用其他名称(最好在名称中没有“MFC”)。

另请参阅

按编号列出的技术说明
按类别列出的技术说明