1. DLL的基本概念
应用程序(exe)要引用目标代码(.obj)外部的函数时,有两种实现途径——静态链接和动态链接。
1. 静态链接
链接程序搜索对应的库文件(.lib),然后将这个对象模块拷贝到应用程序(.exe)中来。Windows之所不使用静态链接库,是因为很多基础库被很多应用程序使用。如果每个应用程序一份拷贝,将带来内存的极大浪费。
2. 动态链接
链接程序搜索到对应的库文件(.lib),然后根据函数名得到对应的函数入口地址,即可进行编译链接。直到真正运行的时候,应用程序才会从lib文件中记录的DLL名字去搜索同名的DLL,然后将DLL的执行代码内存映射到exe中来。动态链接库的好处是多个应用程序可以共用一份DLL的代码段内存。但是数据段则是每个调用进程一份拷贝。
2. 静态链接库
静态链接库的使用比较简单,一般使用如下方式创建。
然后就像普通工程一样,添加头文件的声明以及源文件的实现。
编译该工程就可以得到StaticLib.lib文件了。
调用者调用.lib库也非常简单,只需要包含头文件声明以及指明.lib库路径即可。如:
#include "..\StaticLib\StaticLib.h"
#pragma comment (lib, "..\\Lib\\staticlib.lib")
或者在Configuration Properties\Liker\Input\Additional Dependencies中指明.lib库路径。
3. 动态链接库
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。他们之间的区别简单概括如下:
非MFC动态库:即Win32DLL,不采用MFC库函数,其导出函数为标准的C接口,能被非MFC和MFC编写的应用程序所调用。
MFC规则DLL:包含一个继承自CWinApp的类,但其无消息循环,可以使用MFC,但是接口不能为MFC。
MFC扩展DLL:采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
1. 非MFC动态库
创建Win32DLL
DLL生成向导提供一些简单的示例,使得建立Win32DLL变得更简单。
// The following ifdef block is the standard way of creating macros which make exporting
// from a DLL simpler. All files within this DLL are compiled with the WIN32DLL_EXPORTS
// symbol defined on the command line. this symbol should not be defined on any project
// that uses this DLL. This way any other project whose source files include this file see
// WIN32DLL_API functions as being imported from a DLL, whereas this DLL sees symbols
// defined with this macro as being exported.
#ifdef WIN32DLL_EXPORTS
#define WIN32DLL_API __declspec(dllexport)
#else
#define WIN32DLL_API __declspec(dllimport)
#endif // This class is exported from the Win32DLL.dll
class WIN32DLL_API CWin32DLL {
public:
CWin32DLL(void);
// TODO: add your methods here.
int Add(int x, int y);
}; extern WIN32DLL_API int nWin32DLL; extern “C” WIN32DLL_API int fnWin32DLL(void);
调用程序有两种方式来调用DLL。
1. 隐式链接到DLL
需要完成3步,头文件、.lib文件和DLL。具体实现如下:
#include "..\StaticLib\StaticLib.h" #pragma comment(lib, "..\\Lib\\staticlib.lib")
或者在Configuration Properties\Liker\Input\Additional Dependencies中指明.lib库路径。
DLL的搜索路径见文末.
2. 显式链接到DLL
首先LoadLibary指定的DLL,然后GetProcAddress得到指定函数的入口指针,并且通过函数入口指针来访问DLL的函数,最后通过FreeLibrary制裁DLL。
typedef int (*PADDFUN)(void); HINSTANCE hModule = LoadLibrary("Win32DLL.dll"); PADDFUN pAddFun = (PADDFUN)GetProcAddress(hModule, "GetValue"); pAddFun = (PADDFUN)GetProcAddress(hModule, MAKEINTRESOURCE()); FreeLibrary(hModule);
如果要保证导出的函数名是不带修饰的,一定要将指定函数为C编译器编译。否则函数名需要以被修饰过的以“?”开始的函数名来获取函数的入口指针。上图为Dependency Walker查看到的。
除了直接以函数名获取入口地址外,还可以用索引获取函数入口地址。GetProcAddress获取的是入口地址,所以除了可以获取函数的入口地址,同样可以获取变量的地址。
2. MFC规则DLL
MFC规则的DLL有两种,一种链接MFC动态库的,一种是链接MFC静态库的。选择MFC动态库还是静态库与调用者有关系。因为调用者必须与DLL链接MFC库一致,否则会导致库调用的冲突。如果不是追求追求生成的exe和DLL占较小的空间,推荐使用MFC静态库。
MFC规则DLL的导出基本上同Win32DLL一样,同样不允许导出继承自MFC库的类。不同点主要体现在MFC规则DLL中可以使用MFC库,其实WIN32DLL如果包含了MFC头文件以及链接库,也是可以使用MFC库的。
1. 链接MFC动态库
链接MFC动态库资源的切换。这一点需要注意,并且VC默认生成的代码中也用大篇幅的注释提示了,并且也给出了如下基本的解释。
//TODO: If this DLL is dynamically linked against the MFC DLLs,
// any functions exported from this DLL which call into
// MFC must have the AFX_MANAGE_STATE macro added at the
// very beginning of the function.
//
// For example:
//
// extern "C" BOOL PASCAL EXPORT ExportedFunction()
// {
// AFX_MANAGE_STATE(AfxGetStaticModuleState());
// // normal function body here
// }
//
// It is very important that this macro appear in each
// function, prior to any calls into MFC. This means that
// it must appear as the first statement within the
// function, even before any object variable declarations
// as their constructors may generate calls into the MFC
// DLL.
//
// Please see MFC Technical Notes 33 and 58 for additional
// details.
2. 链接MFC静态库
链接MFC动态库基本上和链接MFC静态库除了上面介绍的不同,导出和添加文件之类的完全一样。所以下面重点讲解链接MFC静态库。
DLL导出变量、函数以及类有两种方法,前面使用的都是通过关键字来导出。另外还有一种方法,即通过模块定义(.def)文件来导出。
我们来看一下模块定义(.def)文件的基本格式:
; MFCDLL.def : Declares the module parameters for the DLL.
LIBRARY "MFCDLL"
EXPORTS
; Explicit exports can go here
ShowDlg @2
nDllValue DATA
注释是通过;来完成的。
关键字LIBRARY,描述DLL的名字,并将此信息写入记录DLL信息的.lib文件中。所以如果在Linker\Output File中修改了生成的DLL的名字,注意也一定要与LIBRARY中描述的一致。当然也可以直接注释掉LIBRARY,这样生成的DLL信息就直接与Linker\Output File中指定的名字相同。另外LIBARAY后面描述的名字,可以加引号,也可以不加引号。ShowDlg,这个是需要导出的函数名。@2,这里的2是描述方法的地址索引,可以修改,也可以不使用,系统会生成默认的。其实不仅仅函数有,变量也有。
如果同时使用了.def和__declspec(dllexport)导出,编译器会优先使用.def文件的导出。.def文件的导出默认是C编译的,即和extern “c” __declspec(dllexport)的导出效果一样。
导入函数的方法和前面使用的一样。下面只说一下导入变量的方法。
- 直接通过关键字__declspec(dllimport) int nDLLValue;
- int* pnDLLValue = (int*)GetProcAddress(hModule, "nDllValue");
pnDLLValue = (int*)GetProcAddress(hModule, MAKEINTRESOURCE(3));
提供按序号导入的原因是这样导入的速度更快,不用去按名字比对查找。
这里只介绍了.def文件导出导入的常用的一些方法,但是基本上够用。如果想更深入的了解.def文件还涉及到很多知识点,可以参考:
http://blog.csdn.net/henry000/article/details/6852521
http://msdn.microsoft.com/zh-cn/library/28d6s79h.aspx
http://msdn.microsoft.com/zh-cn/library/54xsd65y.aspx
http://msdn.microsoft.com/zh-cn/library/d91k01sh.aspx
MFC 扩展 DLL 是通常实现从现有 Microsoft 基础类库类派生的可重用类的 DLL。
MFC 扩展 DLL 具有下列功能和要求:
- 客户端可执行文件必须是用定义的 _AFXDLL 编译的 MFC 应用程序。
- 扩展 DLL 也可由动态链接到 MFC 的规则 DLL 使用。
- 扩展 DLL 应该用定义的 _AFXEXT 编译。 这将强制同时定义 _AFXDLL,并确保从 MFC 头文件中拉入正确的声明。 它也确保了在生成 DLL 时将 AFX_EXT_CLASS 定义为__declspec(dllexport),这在使用此宏声明扩展 DLL 中的类时是必要的。
- 扩展 DLL 不应实例化从 CWinApp 派生的类,而应依赖客户端应用程序(或 DLL)提供此对象。
- 但扩展 DLL 应提供 DllMain 函数,并在那里执行任何必需的初始化。
扩展 DLL 是使用 MFC 动态链接库版本(也称作共享 MFC 版本)生成的。 只有用共享 MFC 版本生成的 MFC 可执行文件(应用程序或规则 DLL)才能使用扩展 DLL。 客户端应用程序和扩展 DLL 必须使用相同版本的 MFCx0.dll。 使用扩展 DLL,可以从 MFC 派生新的自定义类,然后将此“扩展”版本的 MFC 提供给调用 DLL 的应用程序。
扩展 DLL 也可用于在应用程序和 DLL 之间传递 MFC 派生的对象。 与已传递的对象关联的成员函数存在于创建对象所在的模块中。 由于在使用 MFC 的共享 DLL 版本时正确导出了这些函数,因此可以在应用程序和它加载的扩展 DLL 之间随意传递 MFC 或 MFC 派生的对象指针。
客户端必须定义_AFXDLL 编译,其实就是说客户端必须使用MFC动态库,即共享MFC库。另外,扩展DLL中显示对话框,和动态链接MFC的DLL一样需要进行资源的切换,只是两个DLL的DllMain函数不同,导致切换资源的方法不同。扩展DLL的切换方法。MFC扩展DLL的导入导出,基本上和静态链接MFC的DLL一样。
HINSTANCE oldHInst = AfxGetResourceHandle();
HINSTANCE hInst = LoadLibrary("ExDll.dll");
AfxSetResourceHandle(hInst);
CMyDlg dlg;
dlg.DoModal();
AfxSetResourceHandle(oldHInst);
另外,还可以使用构造函数、析构函数来自动完成资源的切换,详见示例代码。
MFC扩展DLL的使用比较复杂,尤其是涉及资源导出之类的,所以如果不是必需,尽量少用FMC扩展DLL,能够用MFC常规DLL代表的尽量代替。
想详细了解扩展DLL的请参考:
http://msdn.microsoft.com/zh-cn/library/1btd5ea3.aspx
http://msdn.microsoft.com/zh-cn/library/h5f7ck28(VS.80).aspx
4. 纯资源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
应用程序应调用句完成时使用的资源。
可以调用与资源切换相同的方式完成资源的切换。
HINSTANCE oldHInst = AfxGetResourceHandle(); HINSTANCE hInst = LoadLibrary("ExDll.dll"); AfxSetResourceHandle(hInst); CMyDlg dlg; dlg.DoModal(); AfxSetResourceHandle(oldHInst);
也可以调用指定资源函数获取指定资源的句柄。
注意项
1. DLL搜索路径
- 当前进程的可执行模块所在的目录。
- 当前目录。
- Windows 系统目录。 GetSystemDirectory 函数检索此目录的路径。
- Windows 目录。 GetWindowsDirectory 函数检索此目录的路径。
- PATH 环境变量中列出的目录。
上面是EXE默认的搜索DLL路径。但是有有时我们希望更改DLL存放的目录,那么就需要在搜索路径上做一些修改了。
如果去改上的模块目录、当前目录,会导致程序中使用目录上的不便,所以不建议修改上面这些目录。Windows其实提供了修改DLL搜索路径的API。
void SetDllDirectory( LPCTSTR lpPathName);
调用这个函数之后,DLL的搜索路径改变为:
- The directory from which the application loaded.
- The directory specified by the lpPathName parameter.
- The system directory. Use the GetSystemDirectory function to get the path of this directory. The name of this directory is System32.
- The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched. The name of this directory is System.
- The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
- The directories that are listed in the PATH environment variable.
HMODULE LoadLibraryEx( LPCTSRlpFileName,HANDLEhFile, DWORD dwFlags);
以参数dwFlags为 _WITH_ALTERED_SEARCH_PATH调用上面的函数时,DLL搜索路径如下:
- The directory specified by the lpFileName path. In other words, the directory that the specified executable module is in.
- The current directory.
- The system directory. Use the GetSystemDirectory function to get the path of this directory.
- The 16-bit system directory. There is no function that obtains the path of this directory, but it is searched.
Windows Me/98/95: This directory does not exist.
- The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
- The directories that are listed in the PATH environment variable.
通过上面两个修改DLL搜索路径的API,我们可以发现,LoadLibraryEx会将一个新添的搜索路径放在其他所有搜索路径之前。而SetDllDirectory则将搜索路径放在第2位。通过实验也发现,LoadLibraryEx的DLL搜索时间明显少于SetDllDirectory设置之后的DLL搜索时间。所以建议使用LoadLibraryEx修改DLL搜索路径。
2. DLL中的静态变量
如果是在一个进程中,几个模块共同调用同一个DLL,那么DLL中的静态变量是全局共用的。
3. 关于释放DLL内存的问题
Windows允许一个进程有多个Heap。我们知道每个DLL会有自己的数据区,也就是说每个DLL都会有自己的Heap。Windows有一个规则,即谁的Heap谁负责,也就是说每个DLL必须得自己负责Head上的内存申请以及释放。
简单的内存申请释放很容易发现,但是有一些vector、CString、CStringArray等,它们的内部实现其实都是有动态申请内存的,所以如果导出函数的参数涉及到这些类型时,也会导致内存释放的问题。
4. Client与DLL在设置上必须一致
- 是否动态链接到MFC库。
- 运行时库也必须一致。MD/MDd;MT/MTd。
- 字符集是否一致,是否Unicode之类的必须一致。
- 导入导出的声明必须一致。
- 须相同的修饰名extern “C”;
- 必须相同的调用方式,_cdecl,_stdcall,_fastcall必须导入导出时一致。
以上只列举了一些容易出现的错误。总之如果在Client端链接出错时,就应该考虑Client与DLL的一致性问题了。
5. 动态链接到MFC共享DLL
If this DLL is dynamically linked against the MFC DLLs,any functions exported from this DLL which call into MFC must have the AFX_MANAGE_STATE macro added at the very beginning of the function.
即只要是动态链接到MFC共享DLL的,必须使用资源切换。