应用程序(exe)要引用目标代码(.obj)外部的函数时,有两种实现途径——静态链接和动态链接。
链接程序搜索对应的库文件(.lib),然后将这个对象模块拷贝到应用程序(.exe)中来。Windows之所不使用静态链接库,是因为很多基础库被很多应用程序使用。如果每个应用程序一份拷贝,将带来内存的极大浪费。
链接程序搜索到对应的库文件(.lib),然后根据函数名得到对应的函数入口地址,即可进行编译链接。直到真正运行的时候,应用程序才会从lib文件中记录的DLL名字去搜索同名的DLL,然后将DLL的执行代码内存映射到exe中来。动态链接库的好处是多个应用程序可以共用一份DLL的代码段内存。但是数据段则是每个调用进程一份拷贝。
静态链接库的使用比较简单,一般使用如下方式创建。
然后就像普通工程一样,添加头文件的声明以及源文件的实现。
编译该工程就可以得到StaticLib.lib文件了。
调用者调用.lib库也非常简单,只需要包含头文件声明以及指明.lib库路径即可。如:
#include "..\StaticLib\StaticLib.h"
#pragma comment (lib, "..\\Lib\\staticlib.lib")
或者在Configuration Properties\Liker\Input\Additional Dependencies中指明.lib库路径。
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类库所编写的应用程序所调用。
创建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_APIint 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(5));
FreeLibrary(hModule);
如果要保证导出的函数名是不带修饰的,一定要将指定函数为C编译器编译。否则函数名需要以被修饰过的以“?”开始的函数名来获取函数的入口指针。上图为Dependency Walker查看到的。
除了直接以函数名获取入口地址外,还可以用索引获取函数入口地址。GetProcAddress获取的是入口地址,所以除了可以获取函数的入口地址,同样可以获取变量的地址。
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)的导出效果一样。
导入函数的方法和前面使用的一样。下面只说一下导入变量的方法。
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 具有下列功能和要求:
扩展 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
一个纯资源 DLL 是一个 DLL,它包含资源如图标、 位图、 字符串和对话框。 使用一个纯资源 DLL 是共享一组相同的多个程序之间的资源的好办法。 它也是一个好的方法,以提供资源被针对多种语言进行本地化的应用程序。
要创建纯资源 DLL,请创建一个新的 Win32 DLL (非 MFC) 项目,并将资源添加到项目中。
使用纯资源 DLL 的应用程序应调用 LoadLibrary 到显式链接到 DLL。 若要访问的资源,调用泛型函数 FindResource 和 LoadResource,其中从事任何种类的资源,或调用下面的特定资源的函数之一:
应用程序应调用句完成时使用的资源。
可以调用与资源切换相同的方式完成资源的切换。
HINSTANCE oldHInst = AfxGetResourceHandle();
HINSTANCE hInst = LoadLibrary("ExDll.dll");
AfxSetResourceHandle(hInst);
CMyDlg dlg;
dlg.DoModal();
AfxSetResourceHandle(oldHInst);
也可以调用指定资源函数获取指定资源的句柄。
注意项
上面是EXE默认的搜索DLL路径。但是有有时我们希望更改DLL存放的目录,那么就需要在搜索路径上做一些修改了。
如果去改上的模块目录、当前目录,会导致程序中使用目录上的不便,所以不建议修改上面这些目录。Windows其实提供了修改DLL搜索路径的API。
void SetDllDirectory( LPCTSTR lpPathName);
调用这个函数之后,DLL的搜索路径改变为:
HMODULE LoadLibraryEx( LPCTSRlpFileName,HANDLEhFile, DWORD dwFlags);
以参数dwFlags为 _WITH_ALTERED_SEARCH_PATH调用上面的函数时,DLL搜索路径如下:
Windows Me/98/95: This directory does not exist.
通过上面两个修改DLL搜索路径的API,我们可以发现,LoadLibraryEx会将一个新添的搜索路径放在其他所有搜索路径之前。而SetDllDirectory则将搜索路径放在第2位。通过实验也发现,LoadLibraryEx的DLL搜索时间明显少于SetDllDirectory设置之后的DLL搜索时间。所以建议使用LoadLibraryEx修改DLL搜索路径。
如果是在一个进程中,几个模块共同调用同一个DLL,那么DLL中的静态变量是全局共用的。
静态库,相当于obj文件的集合。使用者直接将静态库代码链接至自己的模块中。如果一个进程中的两个模块(EXE/DLL)有包含同一个静态库,那么两个模块的静态库是不同的。并不是共用的。
Windows允许一个进程有多个Head。我们知道每个DLL会有自己的数据区,也就是说每个DLL都会有自己的Head。Windows有一个规则,即谁的Head谁负责,也就是说每个DLL必须得自己负责Head上的内存申请以及释放。
简单的内存申请释放很容易发现,但是有一些vector、CString、CStringArray等,它们的内部实现其实都是有动态申请内存的,所以如果导出函数的参数涉及到这些类型时,也会导致内存释放的问题。
以上列举了一些容易出现的错误。总之如果在Client端链接出错时,就应该考虑Client与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的,必须使用资源切换。
它的作用是放置一个lib搜索路径到对应文件的OBJ文件中,链接的时候,就会根据相应的路径来查找lib。
是用来设置编译器的,与编译器有关的。这个功能是编译器来完成的。
所以后面附带的路径与编译器设置文件路径有关,也即以“.dsp"、”.vcproj“为相对路径。
针对导入库,通过#pragma comment(lib。。。来设置的路径,与通过Configration Properties->Liker->Input->Additional Dependencies里添加的Lib路径作用是一样的。
针对静态库,#pragma方式和Configration Properties方式有较大差异。#pragma方式只是记录一个搜索路径,在直接链接的时候才合成进对应模块中。而Configration Properties则直接将静态库合并起来。
Additinal Dependencies和#pragma comment(lib,"*.lib")都是针对.lib文件的。
而.lib文件其实有两种。
一种是与DLL相关的,描述DLL中的函数入口地址等的,在这里两种方法作用一样。
一种是静态链接库,就是将静态库工程当前所有目标文件编译成一个二进制文件(通常是以.lib为扩展名)。Additinal Dependencies和#pragma comment(lib,"*.lib")在这静态库这里会体现出重大的不同。
假如我们有4个工程,静态库工程A,B,C和EXE工程D,B依赖A,C依赖B,D依赖C,D依赖于C。
#pragma comment(lib,"*.lib")
其作用是放置1个库的链接搜索记录到目标文件中,这个搜索记录排在默认搜索记录之后。可以在一个目标文件中放置多个搜索记录,其顺序依据于源码中添加的顺序。因为这行代码的作用只是指明链接时去相应的lib文件中查找相应函数的实现代码,只是一个记录指明的作用,并没有链接的作用。
lib工程并没有链接过程,只有编译过程。只有在生成DLL/EXE时,才会有链接过程的产生,将会将所有目标链接在一起。
因为没有链接过程,所以在编译B,C, D工程时并不需要指明其依赖的静态库(lib),只需要#include相应的头文件即可以编译生成lib文件。
那么在链接生成DLL/EXE时,则需要指明链接搜索记录。
可以在工程D中指明所有需要链接搜索的lib文件。
#pragma comment(lib,"A.lib")
#pragma comment(lib,"B.lib")
#pragma comment(lib,"C.lib")
这个时候才会真正完成链接过程,会检查函数是否定义了,是否重复定义等链接错误。
因为D表面看只依赖C,所以指明B.lib, C.lib感觉不太直观,更直观的可能是用到哪个lib添加哪个:
工程B中添加#pragma comment(lib,"A.lib")
工程C中添加#pragma comment(lib,"B.lib")
工程D中添加#pragma comment(lib,"C.lib")
Project Property->Additinal Dependencies->*.lib
这种方式附加lib文件,看起来效果与上面的方式一样,其实工作原理是不一样的,认识不清,容易产生一些问题。
AdditinalDependencies->*.lib分两种情况:
1、静态库工程中,Configuration Properties->Librarian->General->Additinal Dependencies
其作用是附加指定的lib文件至当前工程,并编译生成至当前的lib文件中。这种行为有点类似链接,但不是链接,只是将将指定lib文件合并到新的lib文件中来。
如果单独编译A, B, C, 它们均生成50K大小的lib文件。
工程B中Additinal Dependencies->A.lib
工程C中Additinal Dependencies->B.lib
那么B.lib将会是100K,C.lib将会是150K。可以看到是直接合并进来的。
但如果是如下设置:
工程B中Additinal Dependencies->A.lib
工程C中Additinal Dependencies->A.lib;B.lib
那么B.lib将会是100K,C.lib将会是200K。可以看到C.lib直接将A.lib,B.lib直接合并进来了,相当于合并了两份A.lib。这样会导致重复合并,VS编译器会提出warning LNK4006,already defined in *.obj;second definition ignored。虽然报出来的是一个链接警告,但其实并不是链接过程的。因为这样的情况很多,所以会出现非常多这类警告。MSDN对这类警告的有个相差描述:You can get this warning if you try to merge twoimport libs into one.就是指我们上面合并两个lib文件。
解决方法也分两种:
A、不合并多个lib文件成1个lib文件,则我们不需要在静态库工程中设置Additinal Dependencie。可以在DLL/EXE中通过
#pragma comment(lib,"*.lib")一次全部指明lib搜索记录,也可以在DLL/EXE工程中通过Additinal Dependencie一次附加所有涉及到的lib。如果想更直观点,用到哪个lib指明哪个,即分别在相应的lib工程里用#pragma comment(lib,"*.lib")指明相应的lib文件即可。
B、合并多个lib文件,工程B Additinal Dependencies->A.lib, 工程C Additinal Dependencies->B.lib即可。
2、DLL/EXE工程中,Configuration Properties->Linker->Input->AdditinalDependencies
可以看出,这里是在链接器下面的一个设置,也说明这里的附加是为链接完成的,即指明链接lib搜索记录,作用与#pragma comment(lib,"*.lib")一样,只不过在DLL/EXE中会产生链接过程。静态库中出现的重复重复定义、定义未实现均不会报错,但是链接过程中会检查这类错误,所以很多静态库中的错误会在DLL/EXE工程的链接结果中报出来。
当MFC库和CRT库冲突时,会出现一个LNK2005的错误。具体的错误如下:
nafxcwd.lib(dllmodul.obj): error LNK2005: _DllMain@12 already defined in LIBCMTD.lib(dllmain.obj)
nafxcwd.lib(afxmem.obj): error LNK2005: "void * __cdecl operator new(unsigned int)"(??2@YAPAXI@Z) already defined in LIBCMTD.lib(new.obj)
nafxcwd.lib(afxmem.obj): error LNK2005: "void __cdecl operator delete(void *)"(??3@YAXPAX@Z) already defined in LIBCMTD.lib(dbgdel.obj)
nafxcwd.lib(afxmem.obj): error LNK2005: "void __cdecl operator delete[](void *)"(??_V@YAXPAX@Z) already defined in LIBCMTD.lib(delete2.obj)
首先我们来认识下这些错误中提示的信息:
nafxcwd.lib,MFC静态链接库,调试版。(发布版是nafxcw.lib)
LIBCMTD.lib,多线程版的CRT库。
DLLMain函数,在dllmodul.cpp和dllmain.cpp中都有定义。
new、delete、delete[],CRT库中有一套标准实现,但是MFC中均重新实现了一遍。
那么上面的错误的意思即是,已经链接过LIBCMTD.lib,再链接nafxcwd.lib即会报重复定义的错误。
对于MFC工程中的DllMain,new, delete函数要求先链接MFC库。而通过上面的错误信息显示,工程是先链接的LIBCMTD.lib,才会出错。
•VC的链接顺序是按文件名来的,而文件名以字母排序的,所以可以建立一个Aa.cpp的文件,然后写个如下函数:int test(){new int; return 1;}这样就可以保证优先链接nafxcwd.lib。
•另外可以把两个库Nafxcwd.lib;Libcmtd.lib都忽略掉,然后重新附加这两个库Nafxcwd.lib;Libcmtd.lib。然后就能正确链接了。
因为涉及到切换资源线程状态。不能传CWnd,可以传句柄HWND。
DLL中自有CXXXApp类似,但是它与主窗口的CMainApp是并不关联的,即主窗口接受到的消息并没有传递到DLL中。模态对话框完全接管了主窗口的消息循环并没有问题,而非模态对话框则需要主窗口将消息循环传进来。
源码下载