原文地址:www.tenouk.com/ModuleBB.ht…
原文作者:www.tenouk.com/
发布时间:
在这个模块中我们有什么?
- 动态链接库和C语言运行时故事
- 应用程序和DLL的区别
- 使用DLL的优势
- DLL的类型
- 将一个可执行文件链接到一个DLL
- 隐性使用链接
- 明确使用链接
- 确定使用哪种链接方法
- 隐性链接
- 明确链接
- 创建一个仅有资源的DLL
- 进口和出口
- 使用.DEF文件
- 使用
__declspec - 使用
__declspec(dllimport)导入应用程序 - 从DLL中导出
- 使用
__declspec(dllexport)从DLL中导出。 - 初始化一个DLL
- 初始化扩展DLL(对于MFC程序)
- 初始化非MFC DLLs
- 运行时的图书馆行为
我的训练时间:xx小时。在你开始之前,请阅读这里的一些说明。DLL的Windows MFC编程(GUI编程)可以在MFC GUI编程步骤教程中找到。
应该获得的能力。
- 能够理解、构建和运行动态链接库程序。
- 能够体会到使用DLL与普通应用相比的好处。
- 能够从MSDN文档中收集信息,以了解和使用DLL。
动态链接库和C语言运行时的故事
注:本模块是一个通用的MSDN文档,涉及Visual C++上的C/C++运行时库和MFC。让我们先来了解一下全貌吧!
动态链接库(DLL)是一个可执行文件,作为一个共享的函数库。动态链接为一个进程提供了一种方法来调用一个不属于其可执行代码的函数。函数的可执行代码位于DLL中,它包含一个或多个函数,这些函数被编译、链接,并与使用它们的进程分开存储。DLL还有利于数据和资源的共享。多个应用程序可以同时访问内存中一个DLL副本的内容。动态链接与静态链接的不同之处在于,它允许可执行模块(无论是.dll还是.exe文件)在运行时只包含定位DLL函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库中获取所有被引用的函数,并将其与你的代码一起放入可执行文件中。使用动态链接代替静态链接有几个优点。DLL可以节省内存,减少交换,节省磁盘空间,升级更容易,提供后市场支持,提供扩展MFC库类的机制,支持多语言程序,方便创建国际版本。
应用程序和DLL的区别
尽管DLLs和应用程序都是可执行的程序模块,但它们在几个方面是不同的。对于终端用户来说,最明显的区别是DLL不是可以直接执行的程序。从系统的角度来看,应用程序和DLL有两个根本的区别。
-
一个应用程序可以有多个实例同时在系统中运行,而一个DLL只能有一个实例。
-
应用程序可以拥有诸如堆栈、全局内存、文件句柄和消息队列等东西,但DLL不能。
使用DLL的优势
动态链接具有以下优点。
- 节省内存,减少交换。许多进程可以同时使用一个DLL,在内存中共享一个DLL的副本。相比之下,Windows必须为每个使用静态链接库构建的应用程序在内存中加载一份库代码。
- 节省了磁盘空间。许多应用程序可以在磁盘上共享DLL的单一副本。相比之下,每个使用静态链接库构建的应用程序都有库代码作为单独的副本链接到其可执行映像中。
- DLL的升级更容易。当DLL中的函数发生变化时,只要函数的参数和返回值不发生变化,使用它们的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- 提供后市场支持。例如,可以修改显示驱动DLL,以支持应用程序出厂时没有的显示器。
- 支持多语言程序。用不同编程语言编写的程序可以调用相同的DLL函数,只要程序遵循函数的调用约定即可。程序和DLL函数必须在以下方面兼容:函数期望其参数被推到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及是否有任何参数在寄存器中传递。
- 提供了一个扩展MFC库类的机制。您可以从现有的MFC类中派生出类,并将它们放在MFC扩展DLL中,供MFC应用程序使用。
- 简化国际版本的创建。通过在DLL中放置资源,可以更容易地创建应用程序的国际版本。您可以将您的应用程序的每个语言版本的字符串放置在一个单独的资源DLL中,并让不同语言版本加载适当的资源。
使用DLL的一个潜在的缺点是,应用程序并不是自成一体的;它依赖于一个单独的DLL模块的存在。
DLL的类型
使用Visual C++,你可以用C或C++构建。
- 用C或C++构建不使用微软基础类库(MFC)的Win32 DLL。
- 你可以用Win32应用向导创建一个非MFC DLL项目。
- 通过MFC DLL向导可以获得MFC库本身,可以是静态链接库,也可以是一些DLL。如果你的DLL使用的是MFC,Visual C++支持三种不同的DLL开发方案。
- 构建一个静态链接MFC的常规DLL。
- 构建一个动态链接MFC的常规DLL。
- 构建一个MFC扩展DLL。这些总是动态链接MFC。
将一个可执行文件链接到一个DLL
可执行文件以两种方式之一链接到(或加载)DLL。
- 隐式链接
- 显式链接
隐式链接有时被称为静态加载或加载时动态链接。显式链接有时被称为动态加载或运行时动态链接。在隐式链接中,使用DLL的可执行文件链接到由DLL制作者提供的导入库(.LIB文件)。当使用DLL的可执行程序被加载时,操作系统会加载该DLL。客户端可执行文件调用DLL的导出函数,就像这些函数包含在可执行文件中一样。
使用显式链接时,使用DLL的可执行程序必须进行函数调用,以显式加载和卸载DLL,并访问DLL的导出函数。客户端可执行文件必须通过函数指针来调用导出的函数。一个可执行文件可以用这两种链接方法使用同一个DLL。此外,这些机制并不相互排斥,因为一个可执行文件可以隐式地链接到DLL,而另一个可执行文件可以显式地附加到它。
使用隐式链接
要隐式链接到DLL,可执行文件必须从DLL的提供者那里获得以下内容。
- 一个头文件(.H文件),包含导出的函数和/或C++类的声明。
- 要链接的导入库(.LIB文件)。当DLL被构建时,链接器会创建导入库。
- 实际的DLL(.DLL文件)。
使用DLL的可执行文件必须在每个包含对导出函数调用的源文件中包含包含导出函数(或C++类)的头文件。从编码的角度来看,对导出函数的函数调用就像其他函数调用一样。要建立调用的可执行文件,必须与导入库链接。如果你使用的是外部的makefile,请指定导入库的文件名,在这里列出你要链接的其他对象(.OBJ)文件或库。操作系统在加载调用的可执行文件时,必须能够找到.DLL文件。
使用显式链接
通过显式链接,应用程序必须在运行时调用一个函数来显式加载DLL。要显式链接到一个DLL,应用程序必须。
- 调用LoadLibrary()(或类似的函数)来加载DLL并获得一个模块句柄。
- 调用GetProcAddress()获得应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针来调用DLL的函数,编译器不会产生外部引用,所以不需要与导入库链接。
- 当处理完DLL后,调用FreeLibrary()。
例如
typedef UINT (CALLBACK* LPFNDLLFUNC1)(DWORD, UINT);
...
HINSTANCE hDLL; // Handle to DLL
LPFNDLLFUNC1 lpfnDllFunc1; // Function pointer
DWORD dwParam1;
UINT uParam2, uReturnVal;
hDLL = LoadLibrary("MyDLL");
if (hDLL != NULL)
{
lpfnDllFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "DLLFunc1");
if (!lpfnDllFunc1)
{
// handle the error
FreeLibrary(hDLL);
return SOME_ERROR_CODE;
}
else
{
// call the function
uReturnVal = lpfnDllFunc1(dwParam1, uParam2);
}
}
确定使用哪种链接方法
隐性链接
当应用程序的代码调用导出的DLL函数时,会发生隐式链接。当调用可执行文件的源代码被编译或组装时,DLL函数调用会在对象代码中产生一个外部函数引用。为了解决这个外部引用,应用程序必须与DLL制作者提供的导入库(.LIB文件)链接。导入库只包含加载DLL和实现对DLL中函数调用的代码。在导入库中找到一个外部函数会通知链接器该函数的代码在DLL中。为了解决对DLL的外部引用,链接器只需在可执行文件中添加信息,告诉系统在进程启动时在哪里找到DLL代码。
当系统启动包含动态链接引用的程序时,它会使用程序可执行文件中的信息来定位所需的DLL。如果无法定位DLL,系统就会终止进程,并显示一个报告错误的对话框,如果正在构建应用程序,则可能输出以下错误信息。
testdll.obj : error LNK2019: unresolved external symbol "int __cdecl mydll(char *)" (?mydll@@YAHPAD@Z) referenced in function _main
Debug/mydlltest.exe : fatal error LNK1120: 1 unresolved externals
否则,系统会将DLL模块映射到进程的地址空间。如果任何一个DLL有一个入口点函数(用于初始化和终止代码),操作系统就会调用该函数。传递给切入点函数的参数之一指定了一个代码,该代码表示DLL附加到进程中。如果切入点函数没有返回TRUE,系统就会终止进程并报告错误。
最后,系统修改进程的可执行代码,为DLL函数提供起始地址。与程序的其他代码一样,DLL代码在进程启动时就被映射到进程的地址空间中,只有在需要时才加载到内存中。因此,在以前的Windows版本中,.DEF文件用来控制加载的PRELOAD和LOADONCALL代码属性不再有意义。
显式链接
大多数应用程序使用隐式链接,因为它是最简单的链接方法。然而,有时显式链接是必要的。下面是一些使用显式链接的常见原因。
- 应用程序在运行前不知道必须加载的DLL的名称。例如,应用程序可能需要从配置文件中获取 DLL 的名称和导出的函数。
- 如果在进程启动时没有找到DLL,使用隐式链接的进程就会被操作系统终止。使用显式链接的进程在这种情况下不会被终止,可以尝试从错误中恢复。例如,进程可以通知用户这个错误,并让用户指定另一个DLL的路径。
- 如果任何一个被链接到的DLLs的DllMain()函数失败,使用隐式链接的进程也会被终止。在这种情况下,使用显式链接的进程不会被终止。
- 隐式链接到许多DLL的应用程序可能会启动缓慢,因为Windows在应用程序加载时加载所有的DLL。为了提高启动性能,应用程序可以在加载后立即隐式链接到那些需要的DLL,并等待在需要时显式链接到其他DLL。
- 显式链接消除了用导入库链接应用程序的需要。如果DLL中的变化导致导出序数发生变化,使用显式链接的应用程序不必重新链接(假设他们调用GetProcAddress()时使用的是函数名而不是序数值),而使用隐式链接的应用程序必须重新链接到新的导入库。
这里有两个显式链接的危害需要注意。
-
如果DLL有一个DllMain()入口点函数,操作系统会在调用LoadLibrary()的线程的上下文中调用该函数。如果因为之前调用LoadLibrary()而没有相应调用FreeLibrary()函数,DLL已经被连接到进程中,那么这个入口点函数就不会被调用。如果DLL使用DllMain()函数为进程的每个线程执行初始化,那么显式链接可能会引起问题,因为当LoadLibrary()(或AfxLoadLibrary())被调用时存在的线程将不会被初始化。
-
如果一个DLL将静态扩展数据声明为
__declspec(thread),如果显式链接,就会引起保护故障。在用LoadLibrary()加载DLL后,只要代码引用这些数据,就会引起保护故障。(Static-extent数据包括全局和本地静态项。)因此,当你创建一个DLL时,你应该避免使用线程本地存储,或者告知DLL用户潜在的陷阱(以防他们尝试动态加载)。
创建一个仅有资源的DLL
纯资源DLL是一个只包含资源的DLL,如图标、位图、字符串和对话框。使用只包含资源的 DLL 是在多个程序中共享同一资源集的好方法。它也是为应用程序提供多语言本地化资源的好方法。要创建一个资源专用DLL,您需要创建一个新的Win32 DLL(非MFC)项目,并将您的资源添加到该项目中。
- 在 "新建项目 "对话框中选择 "Win32项目",并在 "Win32项目向导 "中指定DLL项目类型。
- 为DLL创建一个包含资源(如字符串或菜单)的新资源脚本,并保存.rc文件。
- 在 "项目 "菜单上,单击 "添加现有项目",并将新的.rc文件插入到项目中。
- 指定/NOENTRY链接器选项。/NOENTRY可以防止链接器将对
_main的引用链接到DLL中;创建一个仅有资源的DLL时需要这个选项。 - 构建DLL。
使用资源专用DLL的应用程序应该调用LoadLibrary()来显式链接到DLL。要访问资源,可以调用通用函数FindResource()和LoadResource(),这两个函数适用于任何类型的资源,或者调用以下资源专用函数之一。
- FormatMessage()
- LoadAccelerators()
- LoadBitmap()
- LoadCursor()
- LoadIcon()
- LoadMenu()
- LoadString()
当应用程序使用完资源后,应该调用FreeLibrary()。
导入和导出
您可以使用两种方法将公共符号导入应用程序或从 DLL 中导出函数。
- 在构建 DLL 时使用模块定义 (.DEF) 文件。
- 在主应用程序的函数定义中使用关键字
__declspec(dllimport)或__declspec(dllexport)。
使用.DEF文件
模块定义 (.DEF) 文件是一个文本文件,它包含了一个或多个模块声明,这些声明描述了 DLL 的各种属性。如果你没有使用__declspec(dllimport)或__declspec(dllexport)来导出 DLL 的函数,那么 DLL 需要一个 .DEF 文件。你可以使用 .DEF 文件导入到应用程序中或者从 DLL 中导出。
使用 __declspec
32位版本的Visual C++使用__declspec(dllimport)和__declspec(dllexport)来代替以前在16位版本的Visual C++中使用的__export关键字。你不需要使用__declspec(dllimport)来让你的代码正确编译,但是这样做可以让编译器生成更好的代码。编译器能够生成更好的代码,因为它知道一个函数是否存在于DLL中,所以编译器可以生成跳过通常会出现在一个跨越DLL边界的函数调用中的间接层次的代码。然而,你必须使用__declspec(dllimport)来导入DLL中使用的变量。如果使用适当的.DEF 文件 EXPORTS 部分,__declspec(dllexport) 是不需要的。增加了 __declspec(dllexport) 来提供一种简单的方法来从 .EXE 或 .DLL 中导出函数,而无需使用 .DEF 文件。Win32 Portable Executable (PE) 格式被设计为最小化为修复导入而必须触及的页面数量。为了做到这一点,它将任何程序的所有导入地址放在一个叫做导入地址表的地方。这使得加载器在访问这些导入时,只需修改一两个页面。
使用__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)是可选的,但是如果你使用这个关键字,编译器会产生更有效的代码。然而,你必须使用__declspec(dllimport)才能让导入的可执行文件访问 DLL 的公共数据符号和对象。请注意,您的 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 ... };
从DLL中导出
.DLL文件的布局与.EXE文件非常相似,但有一个重要的区别:DLL文件包含一个导出表。导出表包含DLL向其他可执行文件导出的每个函数的名称。这些函数是进入DLL的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL中的任何其他函数都是DLL的私有函数。DLL的导出表可以通过DUMPBIN工具(Visual Studio自带的,或者你可以尝试更强大的工具,PEBrowser ( www.smidgeonsoft.prohosting.com/ ),使用/EXPORTS选项来查看。你可以使用两种方法从DLL中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建DLL时使用该.DEF文件。如果你想从 DLL 中按序号而不是按 byname 导出函数,请使用这种方法。
- 在函数的定义中使用关键字
__declspec(dllexport)。
当用这两种方法导出函数时,一定要使用 __stdcall 调用约定。模块定义 (.DEF) 文件是一个文本文件,它包含了一个或多个描述 DLL 各种属性的模块语句。如果你没有使用__declspec(dllexport)关键字来导出DLL的函数,DLL需要一个.DEF文件。一个最小的.DEF文件必须包含以下模块定义语句。
- 文件中的第一条语句必须是 LIBRARY 语句。该语句标识了.DEF文件属于一个DLL。LIBRARY 语句后面是 DLL 的名称。链接器将这个名字放在DLL的导入库中。
- EXPORTS 语句列出了 DLL 导出的函数的名称和可选的序数值。你可以通过在函数名称后面用at符号(@)和一个数字给函数分配一个序数值。当您指定序数值时,它们必须在1到N的范围内,其中N是DLL导出的函数的数量。
例如,一个包含实现二进制搜索树的代码的DLL可能看起来像下面这样。
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
如果您使用 MFC DLL 向导来创建 MFC DLL,向导会为您创建一个骨架 .DEF 文件,并自动将其添加到您的项目中。添加要导出到该文件的函数名称。对于非 MFC DLL,您必须自己创建 .DEF 文件并将其添加到您的项目中。如果您要导出C++文件中的函数,您必须将装饰的名称放在.DEF文件中,或者使用extern "C "用标准的C语言链接定义您导出的函数。如果您需要将装饰名放在.DEF文件中,您可以使用DUMPBIN工具或使用链接器/MAP选项来获得它们。注意,编译器产生的装饰名是编译器特有的。如果您将Visual C++编译器产生的装饰名放入.DEF文件中,那么链接到您的DLL的应用程序也必须使用相同版本的Visual C++来构建,以便调用应用程序中的装饰名与DLL的.DEF文件中导出的名称相匹配。如果你正在构建一个扩展DLL(MFC),并使用一个.DEF文件导出,请在包含导出类的头文件的开头和结尾放置以下代码。
#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
// <body of your header file>
#undef AFX_DATA
#define AFX_DATA
这些行确保内部使用的MFC变量或添加到你的类中的MFC变量能从你的扩展DLL中导出(或导入)。例如,当使用DECLARE_DYNAMIC派生一个类时,该宏会展开将一个CRuntimeClass成员变量添加到你的类中。漏掉这四行可能会导致你的DLL编译或链接不正确,或者在客户端应用程序链接到DLL时导致错误。
当构建DLL时,链接器使用.DEF文件创建一个导出(.EXP)文件和一个导入库(.LIB)文件。然后链接器使用导出文件来构建.DLL文件。隐式链接到DLL的可执行文件会在构建时链接到导入库。请注意,MFC本身使用.DEF文件从MFCx0.DLL导出函数和类。
使用__declspec(dllexport)从DLL中导出。
微软在Visual C++的16位编译器版本中引入了__export,允许编译器自动生成导出名,并将它们放在一个.LIB文件中。然后,这个.LIB文件就可以像静态的.LIB一样,用来与DLL链接。在32位编译器版本中,你可以使用__declspec(dllexport)关键字从DLL中导出数据、函数、类或类成员函数。__declspec(dllexport) 将导出指令添加到对象文件中,因此你不需要使用 .DEF 文件。这种便利性在尝试导出装饰的 C++ 函数名时最为明显。由于没有标准的名称装饰规范,所以在不同的编译器版本之间,导出的函数名称可能会发生变化。如果你使用__declspec(dllexport),重新编译DLL和依赖的.EXE文件是必要的,只是为了说明任何命名约定的变化。许多导出指令,如ordinals、NONAME和PRIVATE,只能在a.DEF文件中进行,没有a.DEF文件就无法指定这些属性。然而,除了使用 .DEF 文件之外,使用 __declspec(dllexport) 不会导致构建错误。要导出函数,__declspec(dllexport)关键字必须出现在调用约定关键字的左边,如果有指定关键字的话。例如:
__declspec(dllexport) void __stdcall WilBeExportedFunctionName(void);
而真正的例子可能是这样的。
__declspec(dllexport) int mydll(LPTSTR lpszMsg)
要导出一个类中所有的公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示。
class __declspec(dllexport) CExampleExport : public CObject
{ ... class definition ... };
当构建你的 DLL 时,你通常会创建一个头文件,其中包含你要导出的函数原型和/或类,并在头文件的声明中添加 __declspec(dllexport) 。为了使你的代码更易读,为 __declspec(dllexport) 定义一个宏,并对你要导出的每个符号使用这个宏。
#define DllExport __declspec(dllexport)
__declspec(dllexport) 在 DLL 的导出表中存储函数名。
初始化一个 DLL
通常,你的DLL有初始化代码(如分配内存),当你的DLL加载时必须执行。当使用Visual C++时,你在哪里添加代码来初始化你的DLL取决于你正在构建的DLL的种类。如果你不需要添加初始化或终止代码,那么在构建你的DLL时就没有什么特别的事情要做。如果你需要初始化你的DLL,下面的表格描述了添加代码的位置。
| DLL的类型 | 在哪里添加初始化和终止代码 |
|---|---|
| 常规DLL | 在DLL的CWinApp()对象的InitInstance()和ExitInstance()中 |
| 扩展DLL | 在MFC DLL向导生成的DllMain()函数中 |
| 非MFC DLL | 在你提供的一个名为DllMain()的函数中。 |
在Win32中,所有的DLL都可能包含一个可选的入口点函数(通常称为DllMain()),该函数在初始化和终止时都会被调用。这使你有机会根据需要分配或释放额外的资源。Windows在四种情况下调用入口点函数:进程附加、进程分离、线程附加和线程分离。C运行时库提供了一个名为_DllMainCRTStartup()的入口点函数,它调用DllMain()。根据DLL的种类,要么在源代码中应该有一个叫做DllMain()的函数,要么使用MFC库中提供的DllMain()。
初始化扩展DLL(对于MFC程序)
由于扩展DLL没有CWinApp派生的对象(和普通DLL一样),所以你应该将你的初始化和终止代码添加到MFC DLL向导生成的DllMain()函数中。向导提供了以下扩展DLL的代码。在下面的代码部分,PROJNAME是你的项目名称的占位符。
#include "stdafx.h"
#include <afxdllx.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static AFX_EXTENSION_MODULE PROJNAMEDLL = { NULL, NULL };
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("PROJNAME.DLL Initializing!\n");
// Extension DLL one-time initialization
AfxInitExtensionModule(PROJNAMEDLL, hInstance);
// Insert this DLL into the resource chain
new CDynLinkLibrary(Dll3DLL);
}
else if (dwReason == DLL_PROCESS_DETACH)
{
TRACE0("PROJNAME.DLL Terminating!\n");
}
return 1; // ok
}
在初始化过程中创建一个新的CDynLinkLibrary对象,允许扩展DLL导出CRuntimeClass对象或资源到客户端应用程序。如果你要从一个或多个常规DLL中使用你的扩展DLL,你必须导出一个创建CDynLinkLibrary对象的初始化函数。该函数必须从每个使用扩展DLL的常规DLL中调用。在使用任何扩展DLL的导出类或函数之前,调用这个初始化函数的合适位置是在常规DLL的CWinApp派生对象的InitInstance()成员函数中。
在MFC DLL向导生成的DllMain()中,对AfxInitExtensionModule的调用捕获了模块的运行时类(CRuntimeClass结构)以及它的对象工厂(COleObjectFactory对象),以便在创建CDynLinkLibrary对象时使用。你应该检查AfxInitExtensionModule的返回值;如果从AfxInitExtensionModule返回的值为零,则从你的DllMain()函数中返回零。如果你的扩展DLL将被显式链接到一个可执行文件(意味着可执行文件调用AfxLoadLibrary来链接到DLL),你应该在DLL_PROCESS_DETACH上添加对AfxTermExtensionModule的调用。这个函数允许MFC在每个进程脱离扩展DLL时清理扩展DLL(这发生在进程退出时,或者当DLL因AfxFreeLibrary调用而被卸载时)。如果您的扩展DLL将隐式链接到应用程序,那么对AfxTermExtensionModule的调用是不必要的。显式链接到扩展DLL的应用程序必须在释放DLL时调用AfxTermExtensionModule。如果应用程序使用多个线程,它们还应该使用AfxLoadLibrary和AfxFreeLibrary(而不是Win32函数LoadLibrary()和FreeLibrary())。使用AfxLoadLibrary和AfxFreeLibrary可以确保在加载和卸载扩展DLL时执行的启动和关闭代码不会破坏全局MFC状态.因为MFCx0. DLL在调用DllMain的时候已经完全初始化了,你可以在DllMain中分配内存和调用MFC函数(与16位版本的MFC不同).扩展DLL可以通过处理DllMain()函数中的DLL_THREAD_ATTACH和DLL_THREAD_DETACH情况来处理多线程。当线程附加和脱离DLL时,这些情况会传递给DllMain()。当DLL附加时,调用TlsAlloc()可以让DLL为每一个附加到DLL的线程维护线程本地存储(TLS)索引。
请注意,头文件AFXDLLX.H中包含了扩展DLL中使用的结构的特殊定义,例如AFX_EXTENSION_MODULE和CDynLinkLibrary的定义。你应该在你的扩展DLL中包含这个头文件。请注意,重要的是您既不要定义也不要取消定义 stdafx.h 中的任何_AFX_NO_XXX 宏。请注意,示例中包含了一个名为LibMain()的入口点函数,但您应该将这个函数命名为 DllMain(),以便它能与 MFC 和 C 运行时库一起工作。
初始化非MFC DLLs
要初始化非MFC DLL,你的DLL源代码必须包含一个叫做DllMain()的函数。下面的代码提供了一个基本的骨架,展示了DllMain()的定义。
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
...
case DLL_THREAD_ATTACH:
...
case DLL_THREAD_DETACH:
...
case DLL_PROCESS_DETACH:
...
}
return TRUE;
}
运行时的图书馆行为
C/C++运行时库代码执行DLL启动序列,无需像Windows 3.x中那样需要与单独的模块链接。C/C++运行时库代码中包含了名为DllMainCRTStartup()DLL入口点函数。DllMainCRTStartup()数做了几件事,包括调用_CRT_INIT,它初始化C/C++运行时库,并调用静态、非本地变量上的C++构造函数。如果没有这个函数,运行时库将处于未初始化状态。CRT_INIT可以用于静态链接的CRT,也可以用于从用户DLL链接到CRT DLLmsvcrt.dll。
虽然可以使用/ENTRY: linker选项指定另一个入口点函数,但不建议这样做,因为你的入口点函数将不得不重复DllMainCRTStartup()做的一切。当用Visual C++构建DLL时,DllMainCRTStartup()自动链接进来,你不需要使用/ENTRY: linker选项指定一个入口点函数。
除了初始化C运行时库之外,DllMainCRTStartup()调用一个叫做DllMain()的函数。根据你正在构建的DLL的种类,Visual C++为你提供了DllMain(),并且它被链接进来,这样DllMainCRTStartup()是有东西可以调用。这样一来,如果你不需要初始化你的DLL,那么在构建你的DLL时就没有什么特别的事情要做。如果你需要初始化你的DLL,你在哪里添加你的代码取决于你编写的DLL的种类。
C/C++运行时库代码在静态、非本地变量上调用构造函数和析构函数。例如,在下面的DLL源代码中,Equus和Sugar是类CHorse的两个静态的、非局部的对象,定义在HORSES.H中。在源代码中没有包含对CHorse的构造函数的调用,也没有对destructor函数的调用,因为这些对象是在任何函数之外定义的。因此,对这些构造函数和反构造函数的调用必须由运行时代码来执行。应用程序的运行时库代码也执行这个功能。
#include "horses.h"
CHorse Equus(ARABIAN, MALE);
CHorse Sugar(THOROUGHBRED, FEMALE);
BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)
...
每当一个新进程试图使用DLL时,操作系统都会为DLL的数据创建一个单独的副本:这称为 "进程附加"。DLL的运行时库代码会调用所有全局对象的构造函数(如果有的话),然后在选择进程附加的情况下调用DllMain()函数。与此相反的情况是进程detach:运行时库代码在进程detach被选中的情况下调用DllMain(),然后调用一系列终止函数,包括atexit()函数、全局对象的析构函数和静态对象的析构函数。需要注意的是,进程 attach 中的事件顺序与进程 detach 中的顺序是相反的。
在线程附加和线程分离过程中,运行时库代码也会被调用,但运行时代码本身并不进行初始化或终止。
------------------------------------End C运行-时间与动态链接库第1部分------------------------------------。
进一步的阅读和挖掘
-
Microsoft Visual C++,在线MSDN。
-
结构、枚举、联合和typedef故事可以参考C/C++结构、枚举、联合和typedef。
-
多字节、Unicode字符和本地化请参考Locale、宽字符和Unicode(Story)和Windows用户与组编程教程(Implementation)。
-
Windows数据类型是Windows数据类型。
通过www.DeepL.com/Translator(免费版)翻译