MSDN DLL 综合




使用 DEF 文件从 DLL 导出

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

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

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

  • EXPORTS 语句列出名称,可能的话还会列出 DLL 导出函数的序号值。通过在函数名的后面加上 @ 符和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从 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 文件并将其添加到项目中。

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

如果生成扩展 DLL 并使用 .def 文件导出,则将下列代码放在包含导出类的头文件的开头和结尾:

#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <body of your header file>
#undef AFX_DATA
#define AFX_DATA

这些代码行确保内部使用的 MFC 变量或添加到类的变量是从扩展 DLL 导出(或导入)的。例如,当使用 DECLARE_DYNAMIC 派生类时,该宏扩展以将 CRuntimeClass 成员变量添加到类。省去这四行代码可能会导致不能正确编译或链接 DLL,或在客户端应用程序链接到 DLL 时导致错误。

当生成 DLL 时,链接器使用 .def 文件创建导出 (.exp) 文件和导入库 (.lib) 文件。然后,链接器使用导出文件生成 DLL 文件。隐式链接到 DLL 的可执行文件在生成时链接到导入库。

请注意,MFC 本身使用 .def 文件从 MFCx0.dll 导出函数和类。

按序号而不是按名称从 DLL 导出函数

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



使用 __declspec(dllexport) 从 DLL 导出

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 导出函数




导出 C++ 函数以用于 C 语言可执行文件

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

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

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

导出 C 函数以用于 C 或 C++ 语言可执行文件

如果在用 C 编写的 DLL 中有希望从 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 函数名:

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

使用 AFX_EXT_CLASS 导出和导入

扩展 DLL 使用 AFX_EXT_CLASS 宏导出类;链接到扩展 DLL 的可执行文件使用该宏导入类。用于生成扩展 DLL 的相同头文件可通过 AFX_EXT_CLASS 宏与链接到 DLL 的可执行文件一起使用。

在 DLL 的头文件中,将 AFX_EXT_CLASS 关键字添加到类的声明中,如下所示:


class AFX_EXT_CLASS CMyClass : public CDocument
{
// <body of class>
};

当定义了预处理器符号 _AFXDLL_AFXEXT 时,该宏被 MFC 定义为 __declspec(dllexport)。但当定义了 _AFXDLL 而未定义 _AFXEXT 时,该宏被定义为 __declspec(dllimport)。定义后,预处理器符号 _AFXDLL 指示共享 MFC 版本正在由目标可执行文件(DLL 或应用程序)使用。当 _AFXDLL_AFXEXT 都定义了时,这指示目标可执行文件是扩展 DLL。

由于从扩展 DLL 导出时,AFX_EXT_CLASS 被定义为 __declspec(dllexport),因此可以导出整个类,而不必将该类的所有符号的修饰名放到 .def 文件中。此方法由 MFC 示例 DLLHUSK 使用。

虽然使用此方法可以避免创建 .def 文件和类的所有修饰名,但由于名称可以按序号导出,因此创建 .def 文件的效率更高。若要使用 .def 文件导出方法,请将下列代码放在头文件的开头和结尾处:


#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <body of your header file>
#undef AFX_DATA
#define AFX_DATA
Caution note警告

导出内联函数时要小心,因为它们有可能导致版本冲突。内联函数扩展到应用程序代码中;因此,如果以后重写内联函数,除非重新编译应用程序本身,否则内联函数不会被更新。通常,不用重新生成使用 DLL 函数的应用程序就可以更新 DLL 函数。

导出类中的个别成员

有时,您可能希望导出类中的个别成员。例如,如果导出 CDialog 派生类,可能只需要导出构造函数和 DoModal 调用。可以对需要导出的个别成员使用 AFX_EXT_CLASS

例如:


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

您不再导出类的所有成员,但由于 MFC 宏的工作方式,您可能会遇到其他问题。几个 MFC 的 Helper 宏实际声明或定义数据成员。因此,还必须从 DLL 导出这些数据成员。

例如,当生成扩展 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 打头的行声明类的内部静态对象。若要正确导出该类并从客户端可执行文件访问运行时信息,必须导出此静态对象。由于静态对象是用 AFX_DATA 修饰符声明的,因此只需在生成 DLL 时将 AFX_DATA 定义为 __declspec(dllexport),并在生成客户端可执行文件时将 AFX_DATA 定义为 __declspec(dllimport)。由于已经以此方式定义了 AFX_EXT_CLASS,因此只需参考类定义,将 AFX_DATA 重定义为与 AFX_EXT_CLASS 相同。

例如:


#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

使用 DEF 文件导入

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

// project.def
LIBRARY project
EXPORTS
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) 的某对象文件进行重新编译。

使用 __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.def
LIBRARY project
EXPORTS
ulDataInDll CONSTANT

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

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

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

__declspec(dllexport) ULONG ulDataInDLL;



使用 __declspec(dllimport) 导入函数调用

下面的代码示例显示如何使用 _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_func1func1 的槽在 .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 定义的公共符号,就说该程序是在导入公共符号。为使用 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)
#endif

class CLASS_DECLSPEC CExampleA : public CObject
{ ... class definition ... };


posted @ 2009-03-27 15:15  dzqabc  阅读(846)  评论(0编辑  收藏  举报