使用 DEF 文件从 DLL 导出
模块定义或 DEF 文件 (*.def) 文件是文本文件,其中包含一个或多个描述 DLL 的各种特性的模块语句。 如果没有使用 __declspec(dllexport) 关键字来导出 DLL 的函数,则 DLL 需要 DEF 文件。
最小的 DEF 文件必须包含以下模块定义语句:
文件中的第一个语句必须是 LIBRARY 语句。 此语句将 DEF 文件标识为属于 DLL。 LIBRARY 语句后跟 DLL 的名称。 链接器将此名称放置在 DLL 的导入库中。
EXPORTS 语句列出 DLL 导出的函数的名称和(可选)序号值。 可以通过在函数名称后加一个 at 符号 (@) 和一个数字,为函数分配序号值。 指定序号值时,它们必须在 1 到 N 的范围内,其中 N 是 DLL 导出的函数的数量。 如果要按序号导出函数,请参阅按序号而不是按名称从 DLL 导出函数以及本主题。
例如,包含用于实现二进制搜索树的代码的 DLL 可能如下所示:
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
如果使用 MFC DLL 向导创建 MFC DLL,则向导将为你创建主干 DEF 文件,并自动将它添加到项目中。 添加要导出到此文件的函数的名称。 对于非 MFC DLL,请自己创建 DEF 文件并将它添加到项目。 然后转到“项目” >“属性” >“连接器” >“输入” >“模块定义文件” ,并输入 DEF 文件的名称。 为每个配置和平台重复此步骤,或者通过选择“配置 = 所有配置” 和“平台 = 所有平台” ,对所有配置和平台同时执行此步骤。
如果要导出 C++ 文件中的函数,则必须将修饰名放置在 DEF 文件中,或使用 extern “C” 通过标准 C 链接定义导出函数。 如果需要将修饰名放入 DEF 文件,则可以使用 DUMPBIN 工具或链接器 /MAP 选项来获取这些修饰名。 请注意,编译器生成的修饰名特定于编译器。 如果将 Microsoft C++ 编译器 (MSVC) 生成的修饰名放入 DEF 文件,则链接到 DLL 的应用程序也必须使用相同版本的 MSVC 生成,以便让调用应用程序中的修饰名与 DLL 的 DEF 文件中的导出名称相匹配。
注意
使用 Visual Studio 2015 生成的 DLL 可由使用 Visual Studio 2017 或 Visual Studio 2019 生成的应用程序使用。
如果要生成扩展 DLL 并使用 DEF 文件导出,请将以下代码放置在包含导出类的头文件的开头和结尾:
#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
//
#undef AFX_DATA
#define AFX_DATA
这些行可确保从 MFC 扩展 DLL 导出(或导入)在内部使用或添加到类的 MFC 变量。 例如,使用 DECLARE_DYNAMIC 派生类时,宏会进行扩展以向类添加 CRuntimeClass 成员变量。 省略这四行代码可能会导致 DLL 无法正确编译或链接,或是在客户端应用程序链接到 DLL 时导致错误。
生成 DLL 时,链接器使用 DEF 文件创建导出 (.exp) 文件和导入库 (.lib) 文件。 然后,链接器使用导出文件生成 DLL 文件。 隐式链接到 DLL 的可执行文件会在生成时链接到导入库。
请注意,MFC 本身使用 DEF 文件从 MFCx0.dll 导出函数和类。
使用 __declspec(dllexport) 从 DLL 导出
可以使用 __declspec(dllexport) 关键字从 DLL 中导出数据、函数、类或类成员函数。 __declspec(dllexport) 将导出指令添加到对象文件中,因此你不需要使用 .def 文件。
尝试导出已修饰的 C++ 函数名称时,这种便利性最为明显。 由于名称修饰没有标准规范,因此,导出函数的名称可能会因编译器版本而异。 如果你使用 __declspec(dllexport),则只有在考虑到任何命名约定更改时,才需要重新编译 DLL 和依赖 .exe 文件。
许多导出指令只能在 .def 文件中创建,例如 ordinals、NONAME 和 PRIVATE,并且没有 .def 文件就无法指定这些属性。 不过,除了使用 .def 文件外还使用 __declspec(dllexport) 不会导致生成错误出现。
若要导出函数,__declspec(dllexport) 关键字必须出现在调用约定关键字的左侧(如果指定了关键字的话)。 例如:
__declspec(dllexport) void __cdecl Function1(void);
若要导出类中的所有公共数据成员和成员函数,该关键字必须出现在类名的左侧,如下所示:
class __declspec(dllexport) CExampleExport : public CObject
{ ... class definition ... };
注意
__declspec(dllexport) 不能应用于采用 __clrcall 调用约定的函数。
生成 DLL 时,通常会创建一个包含要导出的函数原型和/或类的头文件,并将 __declspec(dllexport) 添加到头文件中的声明内。 为了提高代码的可读性,请为 __declspec(dllexport) 定义宏,并对要导出的每个符号使用此宏:
#define DllExport __declspec( dllexport )
__declspec(dllexport) 将函数名称存储在 DLL 的导出表中。 如果要优化表的大小,请参阅按序号而不是按名称从 DLL 导出函数。
导出 C++ 函数以用于 C 语言可执行文件
如果要从 C 语言模块访问用 C++ 编写的 DLL 中的函数,则应使用 C 链接(而不是 C++ 链接)声明这些函数。 除非另外指定,否则 C++ 编译器会使用 C++ 类型安全命名(也称为名称修饰)和 C++ 调用约定(可能难以从 C 中进行调用)。
若要指定 C 链接,请为函数声明指定 extern “C”。 例如:
extern "C" __declspec( dllexport ) int MyFunc(long parm1);
导出 C 函数以用于 C 或 C++ 语言可执行文件
如果在 DLL 中有用 C 编写的函数,则可以使用预处理器宏从 C 语言和 C++ 语言代码轻松访问这些函数。 __cplusplus 预处理器宏指示正在编译的语言。 当从 C++ 语言代码调用时,可以使用它来声明具有 C 链接的函数。 如果使用此方法并为 DLL 提供头文件,则 C 和 C++ 用户可以在不进行任何更改的情况下使用这些函数。
下面的代码演示 C 和 C++ 客户端应用程序可以使用的头文件:
// MyCFuncs.h
#ifdef __cplusplus
extern "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++ 源文件中,包装 #include 指令以防止编译器修饰 C 函数名称:
extern "C" {
#include "MyCHeader.h"
}