windows中动态链接库的创建
创建动态链接库工程:我使用的vs2010
1. 创建win32项目,(我们命名为DllDemo)
2.在应用程序类型中选择DLL
动态链接库的入口点DllMain (在dllmain.cpp文件中):DllMain函数。库的入口函数仅供操作系统使用,Windows在库装载、卸载、进程中线程创建和结束时调用入口函数,以便动态链接库可以采取相应的动作。DllMain由工具自动生成,我们不用去修改它。
动态链接库中的函数:DLL能够定义两种函数,导出函数和内部函数。导出函数可以被其他模块调用,也可以被定义这个函数的模块调用,而内部函数只能被定义这个函数的模块调用。
动态链接库的主要功能就是向外导出函数,供进程中其他模块使用。
在DllMain.cpp中定义导出函数:我们创建一个函数 void ExportFunc(LPCTSTR pszContent)
注意:函数定义完后只能在本工程中使用,要想将函数导出供其他模块调用,最简单的方法是在DllDemo.h文件中进行声明。。
声明如下:
#ifdef DLLDEMO_EXPORTS
#define DLLDEMO_API __declspec(dllexport)
#else
#define DLLDEMO_API __declspec(dllimport)
#endif
//声明要导出测函数
DLLDEMO_API void ExportFunc(LPCTSTR pszContent);
也可以这样声明导出函数:直接在返回类型前面加上_declspec(dllexport)就可以将该函数声明为导出函数
_declspec(dllexport) int Add(int a, int b){
return b + a;
}
_declspec(dllexport) int Sub(int a, int b){
return a - b;
}
使用导出函数:
调用DLL中的导出函数有两种方法:
(1). 装载期间动态链接:模块可以像调用本地函数一样调用从其他模块导出的函数(API就是这样调用的)。装载期间链接必须使用DLL的导入库(.lib文件),它为系统提供了加载这个DLL和定位DLL中的导出函数所需的信息。
实现装载期间动态链接需要将DllDemo.h、DllDemo.lib、DllDemo.dll三个文件拷贝到另外一个新建的工程中。
(2).运行期间动态链接:模块可以使用LoadLibrary或者LoadLibraryEx函数在运行期间加载这个DLL。DLL被加载之后,加载模块调用GetProcAddress函数取得DLL导出函数的地址,然后通过函数地址调用DLL中的函数。
为了实现运行期间动态的导出函数,一般要在原先创建动态链接库的工程中建立一个DEF文件来指定要导出的函数。例如添加一个DllDemo.def文件。
例子: DllDemo工程:
dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include <stdio.h>
#include "DllDemo.h"
#include <stdexcept>
using namespace std;
HMODULE g_hModule;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HMODULE)hModule;
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
//导出函数
void ExportFunc(LPCTSTR pszContent)
{
char sz[MAX_PATH];
::GetModuleFileNameA(g_hModule, sz, MAX_PATH);
::MessageBox(NULL, pszContent, strrchr(sz, '\\')+1, MB_OK);
}
namespace MathFuncs
{
double MyMathFuncs::Add(double a, double b)
{
return a + b;
}
double MyMathFuncs::Sub(double a, double b)
{
return a - b;
}
}
DllDemo.h文件
#ifdef DLLDEMO_EXPORTS
#define DLLDEMO_API __declspec(dllexport)
#else
#define DLLDEMO_API __declspec(dllimport)
#endif
//声明要导出测函数
DLLDEMO_API void ExportFunc(LPCTSTR pszContent);
namespace MathFuncs
{
class MyMathFuncs
{
public:
//return a + b
static DLLDEMO_API double Add(double a, double b);
//return a - b
static DLLDEMO_API double Sub(double a, double b);
};
}
另一个工程: 测试前面的动态链接库
#include <windows.h>
#include "DllDemo.h"
#pragma comment(lib, "DllDemo.lib")
int main()
{
ExportFunc("大家好");
return 0;
}
如何使用动态链接库:
引入动态链接库:#pragma comment(lib, "Dll2013.lib")
声明外部函数:利用extern来声明,
extern int Add(int a, int b);
extern int Sub(int a, int b);
或者直接利用_declspec(dllimport)来直接声明函数是从动态链接库中引入的。
_declspec(dllimport) int Add(int a, int b);
_declspec(dllimport) int Sub(int a, int b);
_declspec(dllimport) 与 extern相比,使用_declspec(dllimport)标识符声明外部函数时,它将告诉编译器该函数是从动态链接库中引入的,编译器可以运行效率更高的代码。因此,如果调用的函数来自于动态链接库,应该采用这种方式声明外部函数。
dumpbin命令:
在VS2010中,通过“工具->visual studio 2010命令提示”就可以打开命令窗口。输入dumpbin可以查看该命令的使用方法。
输入dumpbin -exports [动态链接库的路径]就可以查看动态链接库的信息。
输入dumpbin -imports [应用程序的路径] 可以查看应用程序的导入信息。
从DLL中导出C++类:
不仅可以从动态链接库中导出函数,还可以导出一个C++类。为了导出一个C++类,我们可以在DLL的头文件中添加类似下面的代码
class DLL2013_API Point{
public:
void Output(int x, int y);
};
然后再DLL的cpp文件中添加Point类对应的函数实现
void Point::Output(int x, int y){
HWND hwnd = ::GetForegroundWindow();
HDC hdc = ::GetDC(hwnd);
char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x=%d, y=%d", x, y);
::TextOut(hdc, 0, 0, buf, strlen(buf));
::ReleaseDC(hwnd, hdc);
}
完成上面两步,就可以将Point类导出了。
我们也可以不导出整个类,而只是导出该类中的某些函数。为了只导出类中的部分函数,我们要将类前面使用的DLL2013_API注释掉,然后在导出函数前面放置DLL2013_API宏。这样就只有使用了DLL2013_API宏的函数才能被导出。
class /*DLL2013_API*/ Point{
public:
void DLL2013_API Output(int x, int y);
void Test();
};
上面的代码我们导出了函数Output,而函数Test没有导出。可以用工具dumpbin来查看。另外,在导出类的成员函数时需要注意,该函数必须具有public类型的访问权限,否则,该函数即使能够被导出,也不能被其它程序访问。
解决名字改编问题:
我们知道,C++编译器在生成DLL时,会对导出函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字是不一样的。这样,如果利用不同的编译器分别生成DLL和访问DLL的客户端程序的话,后者在访问该DLL的导出函数时就会出现问题。例如C++编译器和C编译器,C编译器不会对名字进行改编。
为了解决上面的问题,我们希望动态链接库文件在编译时,导出函数的名称不要发生改变。为了实现这一目的,在定义导出函数是,需要加上限定符:extern "C"。注意双引号中的字母C一定要大写。看下面的例子:
在Dll的头文件(我的头文件是Dll2013.h)中添加如下代码:
#ifdef DLL2013_API
#else
#define DLL2013_API extern "C" _declspec(dllimport)
#endif
DLL2013_API int Add(int a, int b);
DLL2013_API int Sub(int a, int b);
在Dll的源文件(我的源文件是dllmain.cpp)中添加如下代码:
#define DLL2013_API extern "C" _declspec(dllexport)
#include <Windows.h>
#include <stdio.h>
#include "stdafx.h"
#include "Dll2013.h"
// dllmain.cpp : 定义 DLL 应用程序的入口点。
BOOL APIENTRY DllMain( HMODULE 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:
break;
}
return TRUE;
}
int Add(int a, int b){
return b + a;
}
int Sub(int a, int b){
return a - b;
}
重新生成DLL2013.dll之后,用dumpbin命令查看导出函数信息,可以发现DLL导出的Add和Sun函数的名字没有发生改编。
需要注意的是,extern "C" 可以解决C++和C语言之间相互调用是函数命名的问题,但是这种方法有一个缺陷,就是不能用于导出一个类的成员函数,只能用于导出全局函数这种情况。另外,如果导出函数的调用约定发生了改变,那么即使使用了限定符:extern "C" ,该函数的名字仍会发生改编。
例如:上面的程序中,使导出函数Add和Sub使用标准调用约定(标准调用约定就是WINAPI约定,也就是pascal调用约定,这种约定方式与C调用约定不一样):即在声明这些函数时添加_stdcall 关键字。
在Dll2013.h中:
DLL2013_API int _stdcall Add(int a, int b);
DLL2013_API int _stdcall Sub(int a, int b);
在dllmain.cpp中:
int _stdcall Add(int a, int b){
return b + a;
}
int _stdcall Sub(int a, int b){
return a - b;
}
上面的代码中,如果没有添加_stdcall关键字,那么函数的调用就是C调用约定。
还有一种方法来解决名字改编问题:使用模块定义文件(DEF)
LIBRARY DLL2013
EXPORT
Add
Sub
其中LIBRARY语句用来指定动态链接库的内部名称,该名称与生成的动态链接库的名称一定要匹配,不过这句代码并不是必须的。
EXPORT语句的作用是表明DLL将要导出的函数,以及为这些函数指定的符号名。当连接器在链接时,会分析这个DEF文件,当发现在EXPORT语句下面有Add和Sub这两个符号名,并且它们与源文件中定义的Add和Sub函数的名字一致时,它就会以Add和Su这两个符号导出相应的函数,如果不一致,就按照下述语法指定导出函数: entryname = internalname 。
其中entername是导出的符号名,interalname是DLL中将要导出的函数的名字。
显式加载DLL
使用动态方式加载动态链接库时,需要用到LoadLibrary函数,该函数的作用是将指定的可执行模块映射到调用进程的地址空间。Loadlibrary函数不仅能够加载DLL(.dll),还可以加载可执行模块(.exe)。
当获取到动态链接库的句柄后,接下来就是要获取动态链接库中导出函数的地址,这可以通过GetProcAddress函数来实现。
使用动态加载方式要注意的几个地方:
1.创建DLL时,一定要解决名字改变问题,例如可以利用extern "C" ,是编译器不对名字进行改编。
2.动态加载DLL时,客户端程序不再需要包含导出函数声明的头文件(.h)和引入库文件(.lib),只需要.dll文件即可。
3.当采用动态方式加载DLL时,在客户端将不能看到调用该DLL的输入信息。可以用dumpbin -imports [.exe文件路径]来查看输入信息。
例子:
HINSTANCE hInst;
//动态加载DLL
hInst = ::LoadLibrary("F:\\windows\\000MFC\\Dll2013\\Debug\\Dll2013.dll");
if(!hInst){
MessageBox("加载失败");
return;
}
//定义函数指针类型
typedef int (*ADDPROC)(int a, int b);
//获取DLL的导出函数
ADDPROC add = (ADDPROC)GetProcAddress(hInst, "Add");
if(!add){
MessageBox("获取函数地址失败");
return;
}
CString str;
str.Format("5 + 3 = %d", add(5, 3));
MessageBox(str);
调用约定:
如果我们将DLL工程中的导出函数的调用约定改为标准约定(_stdcall),那么生成的DLL,其导出函数名字会被改编吗???自己可以试验一下,结果是不会发生名字改编。
还要注意的地方:当DLL中导出函数采用的是标准调用约定时,放完该DLL的客户端程序也应该采用该约定类型来访问相应的导出函数。
DLL中:
int _stdcall Add(int a, int b){
return b + a;
}
客户端程序中:
typedef int (_stdcall *ADDPROC)(int a, int b);
根据序号访问DLL中的导出函数
当我们用dumpbin工具查看DLL文件是时,可以看到每个导出函数都进行了编号,而我们就可以利用这些编号来调用导出函数。在这里我们要解决的问题是,如何把int类型的序号转换为LPCSTR类型的变量???使用宏 MAKEINTRESOURCE ,该宏可以把指定的函数序号转换为相应的函数名字字符串。
//获取DLL的导出函数
//ADDPROC add = (ADDPROC)GetProcAddress(hInst, "Add");
ADDPROC add = (ADDPROC)GetProcAddress(hInst, MAKEINTRESOURCE(1));
最后,当不在需要访问该DLL时,调用FreeLibrary函数释放对该DLL的引用。