转自:http://www.a3gs.com/BookViews.asp?InfoID=2839&classID=819
描述:
本文讲述了非MFC动态链接库相关原理及使用方法。
技术实现:
VC6中的动态链接库分非MFC动态链接库、MFC规则动态链接库、MFC扩展动态链接库;下面我们就以例子的形式分别介绍这些动态链接库的实现方法。
1 非MFC动态链接库
1.1 创建一个非MFC动态链接库
启动VC6选择FileàNew…;弹出新建对话框;选择Projects选项卡,在项目类型列表中选中Win32 Dynamic-Link Library,在Project name中输入您所要创建的项目名称(如:MyNoneMFCTestDll),在Location中输入您所创建的项目的保存路径(如C:/MyProjects/ MyNoneMFCTestDll)。
点击OK按钮弹出Win32 Dynamic-Link Library – Step 1 of 1,对于新手我们在What kind of Dll would you like to create?中选择A Dll that exprots some symbols,这样class wizard在为我们生成非MDC动态链接库框架的同时也为我们生成了一些动态链接库的例子;点击Finish按钮弹出New project information对话框,在此对话框中我们可以看到所创建工程的一些概要信息。
然后点击OK按扭,现在VC6的class wizard就为我们创建了一个非MFC动态链接库框架与一些例子;接下来就是我们为项目添加自己的导出函数了。
在我们的工程中添加Mylib.h及Mylib.cpp文件,源代码如下:
/*文件名:MyLib.h*/
#ifndef MY_LIB_H
#define MY_LIB_H
extern "C" int __declspec(dllexport)add(int x, int y);
#endif
/*文件名:MyLib.cpp*/
#include "StdAfx.h"
#include "MyLib.h"
int add(int x, int y)
{
return x + y;
}
在上面的MyLib.cpp文件中我们注意到一行#include "StdAfx.h",从MyLib.cpp文件中我们并没有发现任何需要StdAfx.h文件中的内容,可是为们为什么需要这一行呢?这是因为我们这个例子工程是利用classwizard生成的框架,这样如果没有这一行则会提示:fatal error C1010: unexpected end of file while looking for precompiled header directive。
1.2 调用动态链接库
在VC中动态链接库的调用分静态加载与动态调用;对与静态加载需要相应的lib文件,但此lib文件仅仅是关于其对应DLL文件中函数的重定位信息;在这里我们以例子的方式说明这两种调用方式的使用。
在上面所建的工程中选择FileàNew…;在弹出的新建对话框中选择Projects选项卡,这回在项目类型列表中选中Win32 Console Application,在Project name中输入Test,选中Add to current workspace,别的东西均保持默认值;点击OK,弹出Win32 Console Application – Step 1 of 1对话框。在What kind of console application do you want to create?中选择An empty project。点击Finish按钮,弹出New project information对话框,点击OK。
在所建的Test工程中加入Test.cpp文件。
Ø 如果以动态方式调用则Test.cpp文件内容如下:
/*文件名:Test.cpp*/
#include <stdio.h>
#include <windows.h>
typedef int(*lpAddFun)(int, int); //宏定义函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpAddFun addFun; //函数指针
hDll = LoadLibrary("..//Debug//MyNoneMFCTestDll.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, "add");
if (addFun != NULL)
{
int result = addFun(2, 3);
printf("%d/r/n", result);
}
FreeLibrary(hDll);
}
return 0;
}
Ø 如果以静态方式调用则我们的Test.cpp文件内容如下:
/*文件名:Test.cpp*/
#include <stdio.h>
#pragma comment(lib,"..//Debug//MyNoneMFCTestDll.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) add(int x,int y);
int main(int argc, char* argv[])
{
int result = add(2,3);
printf("%d/r/n",result);
return 0;
}
这个例子非常简单,在这里就不多解释了,如果您对个别Windows API有什么疑问的话请查看MSDN文档。
1.3 声明导出函数
DLL中导出函数的声明有两种方式:一种为1.1节例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在MyNoneMFCTestDll工程中添加Mylib.def文件),文件内容如下:
; lib.def : 导出DLL函数
LIBRARY MyNoneMFCTestDll
EXPORTS
add @ 1
.def文件的规则为:
(1) LIBRARY语句说明.def文件相应的DLL;
(2) EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3) .def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“MyNoneMFCTestDll”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
1.4 DllMain函数
Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。虽然工程中可以没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数,特别是在工程有全局资源的申请与释放时DllMain就显得特别有用。
根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
我们来看一个DllMain函数的例子。
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("/nprocess attach of dll");
break;
case DLL_THREAD_ATTACH:
printf("/nthread attach of dll");
break;
case DLL_THREAD_DETACH:
printf("/nthread detach of dll");
break;
case DLL_PROCESS_DETACH:
printf("/nprocess detach of dll");
break;
}
return TRUE;
}
DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和THREAD_DETACH,以switch语句列出。
来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。
APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
执行下列代码:
hDll = LoadLibrary("..//Debug// MyNoneMFCTestDll.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));
//MAKEINTRESOURCE直接使用导出文件中的序号
if (addFun != NULL)
{
int result = addFun(2, 3);
printf("/ncall add in dll:%d", result);
}
FreeLibrary(hDll);
}
我们看到输出顺序为:
process attach of dll
call add in dll:5
process detach of dll
,
这一输出顺序验证了DllMain被调用的时机。
代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
#define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
#define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
#ifdef UNICODE
#define MAKEINTRESOURCE MAKEINTRESOURCEW
#else
#define MAKEINTRESOURCE MAKEINTRESOURCEA
1.5 调用约定
谈到动态链接库我们就免不要和调用约定与名字修饰约定,前不久在做一VC动态库给Delphi调用时就时为调用约定与名字修饰约定问题带来了不少地麻烦;在这一节里我们就来谈谈调用约定,在下一节中再谈谈名字修饰约定为后继的动态库调用发生异常分析原因做些基础铺垫。
在VC中的调用约定有__cdecl、__fastcall与__stdcall三种调用约定(Calling convention);调用约定决定以下内容:1)函数参数的压栈顺序,2)由调用者还是被调用者把参数弹出栈,3)以及产生函数修饰名的方法。
1. __stdcall调用约定:
函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈。
2. _cdecl调用约定:
cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。函数采用从右到左的压栈方式。注意:对于可变参数的成员函数,始终使用__cdecl的转换方式。
3. __fastcall调用约定:
__fastcall调用约定:它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。
4. thiscall调用约定:
thiscall仅仅应用于"C++"成员函数。this指针存放于CX寄存器,参数从右到左压。thiscall不是关键词,因此不能被程序员指定。
5. naked call调用约定:
naked call采用1-4的调用约定时,如果必要的话,进入函数时编译器会产生代码来保存ESI,EDI,EBX,EBP寄存器,退出函数时则产生代码恢复这些寄存器的内容。naked call不产生这样的代码。naked call不是类型修饰符,故必须和_declspec共同使用。
调用约定可以通过工程设置:Setting.../C/C++ /Code Generation项进行选择,缺省状态为__cdecl。
1.6 名字修饰约定
1. 修饰名(Decoration name):"C"或者"C++"函数在内部(编译和链接)通过修饰名识别。
2. C编译时函数名修饰约定规则:
Ø __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个"@"符号和其参数的字节数,格式为_functionname@number,例如 :function(int a, int b),其修饰名为:_function@8 。
Ø __cdecl调用约定仅在输出函数名前加上一个下划线前缀,格式为_functionname。
Ø __fastcall调用约定在输出函数名前加上一个"@"符号,后面也是一个"@"符号和其参数的字节数,格式为@functionname@number。
3. C++编译时函数名修饰约定规则:
Ø __stdcall调用约定:
1) 以"?"标识函数名的开始,后跟函数名;
2) 函数名后面以"@@YG"标识参数表的开始,后跟参数表;
3) 参数表以代号表示:
X--void ,
D--char,
E--unsigned char,
F--short,
H--int,
I--unsigned int,
J--long,
K--unsigned long,
M--float,
N--double,
_N--bool,
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以"0"代替,一个"0"代表一次重复;
4) 参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5) 参数表后以"@Z"标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。其格式为"?functionname@@YG*****@Z"或"?functionname@@YG*XZ",例如:
int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z”
void Test2()----------------------------------“?Test2@@YGXXZ”
6)
Ø __cdecl调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YA"。
Ø __fastcall调用约定:
规则同上面的_stdcall调用约定,只是参数表的开始标识由上面的"@@YG"变为"@@YI"。
VC++对函数的省缺声明是"__cedcl",将只能被C/C++调用。
注意:
1. _beginthread需要__cdecl的线程函数地址,_beginthreadex和CreateThread需要__stdcall的线程函数地址。
2. 一般WIN32的函数都是__stdcall。而且在Windef.h中有如下的定义:
#define CALLBACK __stdcall
#define WINAPI __stdcall
3. extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
typedef int (__cdecl*FunPointer)(int a, int b);
修饰符的书写顺序如上。
4. extern "C"的作用:如果Add(int a, int b)是在c语言编译器编译,而在c++文件使用,则需要在c++文件中声明:extern "C" Add(int a, int b),因为c编译器和c++编译器对函数名的解释不一样(c++编译器解释函数名的时候要考虑函数参数,这样是了方便函数重载,而在c语言中不存在函数重载的问题),使用extern "C",实质就是告诉c++编译器,该函数是c库里面的函数。如果不使用extern "C"则会出现链接错误。
一般象如下使用:
#ifdef _cplusplus
#define EXTERN_C extern "C"
#else
#define EXTERN_C extern
#endif
#ifdef _cplusplus
extern "C"{
#endif
EXTERN_C int func(int a, int b);
#ifdef _cplusplus
}
#endif
5. MFC提供了一些宏,例如可以使用AFX_EXT_CLASS来代替__declspec(dllexport),并修饰类名,从而导出类,AFX_API_EXPORT来修饰函数,AFX_DATA_EXPORT来修饰变量。一些常用的宏如下:
AFX_CLASS_EXPORT:__declspec(dllexport)
AFX_CLASS_IMPORT:__declspec(dllimport)
AFX_API_EXPORT __declspec(dllexport)
AFX_API_IMPORT:__declspec(dllimport)
AFX_DATA_EXPORT __declspec(dllexport)
AFX_DATA_IMPORT __declspec(dllimport)
// used when building extension DLLs
#ifndef AFX_EXT_DATA
#define AFX_EXT_DATA
#define AFX_EXT_DATADEF
#define AFX_EXT_CLASS
#define AFX_EXT_API
#endif
6. DLLMain负责初始化(Initialization)和结束(Termination)工作,每当一个新的进程或者该进程的新的线程访问DLL时,或者访问DLL的每一个进程或者线程不再使用DLL或者结束时,都会调用DLLMain。但是,使用TerminateProcess或TerminateThread结束进程或者线程,不会调用DLLMain。
7. 一个DLL在内存中只有一个实例。
DLL程序和调用其输出函数的程序的关系:
1) DLL与进程、线程之间的关系
DLL模块被映射到调用它的进程的虚拟地址空间。DLL使用的内存从调用进程的虚拟地址空间分配,只能被该进程的线程所访问。DLL的句柄可以被调用进程使用;调用进程的句柄可以被DLL使用。DLL可以有自己的数据段,但没有自己的堆栈,使用调用进程的栈,与调用它的应用程序相同的堆栈模式。
2) 关于共享数据段
DLL定义的全局变量可以被调用进程访问;DLL可以访问调用进程的全局数据。使用同一DLL的每一个进程都有自己的DLL全局变量实例。如果多个线程并发访问同一变量,则需要使用同步机制;对一个DLL的变量,如果希望每个使用DLL的线程都有自己的值,则应该使用线程局部存储(TLS,Thread Local Strorage)。
1.7 调用约定举例
在上面的MyLib.h文件中假设我们把extern "C" int __declspec(dllexport)add(int x, int y);改成extern "C" int __declspec(dllexport) __stdcall add(int x, int y);那么我们在其调有时的申明应该相应的改成typedef int(__stdcall *lpAddFun)(int, int);此时如果我们再用上面例子中的typedef int(*lpAddFun)(<, S, style="COLOR: blue" PAN>int, int);则在程序运行时会提示“This is usually a result of…”异常。
对于为什么会提示此异常请参见上面的调用约定说明。
1.8 动态库导出变量
动态链接库定义的全局变量可以被调用进程访问;同时动态链接库也可以访问调用进程的全局数据,我们来看看在应用工程中引用动态链接库中变量的例子(为了方便起见我们还是以上面的例子工程--- MyNoneMFCTestDll为例子来说明)。
这回我们在MyNoneMFCTestDll工程中的MyLib.h中加入一个全局变是,MyLib.h及MyLib.cpp内容如下:
/*文件名:MyLib.h*/
#ifndef MY_LIB_H
#define MY_LIB_H
extern "C" int __declspec(dllexport) add(int x, int y);
extern "C" int __declspec(dllexport) g_nVal;
#endif
/*文件名:MyLib.cpp*/
#include "StdAfx.h"
#include "MyLib.h"
int add(int x, int y)
{
return x + y;
}
int g_nVal = 100;
为了测试变量的导出这回我们把上面测试工程中的Test.cpp文件中写入如下内容:
/*文件名:Test.cpp*/
#include <stdio.h>
#pragma comment(lib,"..//Debug//MyNoneMFCTestDll.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) add(int x,int y);
extern "C" __declspec(dllimport) g_nVal;
int main(int argc, char* argv[])
{
int result = add(2,3);
printf("%d/r/n",result);
printf("%d/r/n",g_nVal);
g_nVal = 999;
printf("%d/r/n",g_nVal);
return 0;
}
1.9 动态库导出类
动态库中定义的类可以在应用工程中使用。下面的例子里,我们在动态库中定义了一个CMyTest类,并在测试工程中引用了它。
这回我们在上面的测试工程(MyNoneMFCTestDll)的MyLib.h中添加一个CMyTest类的声明,同时在MyLib.cpp中实现它;MyLib.h与MyLib.cpp内容如下:
/*文件名:MyLib.h*/
#ifndef MY_LIB_H
#define MY_LIB_H
class __declspec(dllexport) CMyTest
{
public:
void TestFun();
};
extern "C" int __declspec(dllexport) add(int x, int y);
extern "C" int __declspec(dllexport) g_nVal;
#endif
/*文件名:MyLib.cpp*/
#include "StdAfx.h"
#include "Stdio.h"
#include "MyLib.h"
void CMyTest::TestFun()
{
printf("This is My test export class.../r/n");
}
int add(int x, int y)
{
return x + y;
}
int g_nVal = 100;
为了测试变量的导出这回我们把上面测试工程中的Test.cpp文件中写入如下内容:
/*文件名:Test.cpp*/
#include <stdio.h>
#pragma comment(lib,"..//Debug//MyNoneMFCTestDll.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息
extern "C" __declspec(dllimport) add(int x,int y);
extern "C" __declspec(dllimport) g_nVal;
class __declspec(dllimport) CMyTest
{
public:
void TestFun();
};
int main(int argc, char* argv[])
{
int result = add(2,300);
printf("%d/r/n",result);
printf("%d/r/n",g_nVal);
g_nVal = 999;
printf("%d/r/n",g_nVal);
CMyTest rTest;
rTest.TestFun();
return 0;
}
1.10 小结
到此我们讲完了VC中非MFC动态链接库的开发及使用方法,同时也介绍了VC动态链接库中应该注意的问题。
在上面的例子中我们对与导出与导入我们均直接用了__declspec(dllexport)与__declspec(dllimport),在上面的例子工程中因为项目小所以无所谓;但如果需要导出与导入量比较大的时候这样就显的很是麻烦,因为我们根本就没有用到开发动态库时的头文件,而是在每个测试工程中直接做相应的声明。
为了在应用工程中可以直接利用到开发动态库时的头文件,我们一般采用预编译的方法,例如我们把上面的例子工程MyNoneMFCTestDll中的MyLib.h改写成如下形式:
/*文件名:MyLib.h*/
#ifndef MY_LIB_H
#define MY_LIB_H
#ifdef MYNONEMFCTESTDLL_EXPORTS
#define MYNONEMFCTESTDLL_API __declspec(dllexport)
#else
#define MYNONEMFCTESTDLL_API __declspec(dllimport)
#endif
class MYNONEMFCTESTDLL_API CMyTest
{
public:
void TestFun();
};
extern "C" int MYNONEMFCTESTDLL_API add(int x, int y);
extern "C" int MYNONEMFCTESTDLL_API g_nVal;
#endif
这样在开发动态库时我们定义一个MYNONEMFCTESTDLL_EXPORTS宏而在使用时则不取消对MYNONEMFCTESTDLL_EXPORTS宏的定义既可。
这样我们的测试工程中的Test.cpp就可以写成如下形式:
#include <stdio.h>
#include "MyLib.h"
#pragma comment(lib,"..//Debug//MyNoneMFCTestDll.lib")
int main(int argc, char* argv[])
{
int result = add(2,300);
printf("%d/r/n",result);
printf("%d/r/n",g_nVal);
g_nVal = 999;
printf("%d/r/n",g_nVal);
CMyTest rTest;
rTest.TestFun();
return 0;
}