+++++++++++++++++++++++++++++++++++++++++++++
++++++++++++ 动态库的创建及使用 +++++++++++
+++++++++++++++++++++++++++++++++++++++++++++
1. 创建一个简单的非MFC的DLL:
用动态链接库实现一个同样功能的add函数
l 创建一个DLL的项目:
文件—》新建—》项目—》win32控制台—》输入名字“dllTest”—》Dll—》空项目—》完成。
l 在建立的工程中添加lib.h及lib.cpp文件,源代码如下:
lib.h
/* 文件名:lib.h */
#ifndef LIB_H
#define LIB_H
//对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数
extern "C" int __declspec(dllexport)add(int x, int y);
#endif
lib.cpp
/* 文件名:lib.cpp */
#include "lib.h"
#include <iostream>
using namespace std;
int add(int x, int y)
{
std::cout<<"动态库中......两个数之积:是吗???"<<(x*y)<<std::endl;
return x * y;
}
l 编译成功后
你在..//Debue里面能找到下面的文件,则证明动态库生成成功。
l 动态调用方式————创建同一个解决方案下的应用项目dllCall 加上下面的代码,编译运行。 Main.cpp #include <stdio.h> #include <tchar.h> //和_T有关,且是Unicode #include <windows.h> typedef int(*lpAddFun)(int, int); //宏定义函数指针类型,定义了一个与add函数接受参数类型和返回值均相同的函数指针类型 int main(int argc, char *argv[]) { HINSTANCE hDll; //DLL句柄 lpAddFun addFun; //函数指针,定义了lpAddFun的实例addFun //通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll //动态库所在的路径,如果有误则会报错。上一级目录下面的Debug文件夹下面的,一个dllTest.dll文件 hDll = LoadLibraryW(_T("..//Debug//dllTest.dll")); if (hDll != NULL) { //Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun addFun = (lpAddFun)GetProcAddress(hDll, "add"); if (addFun != NULL) { //应用程序使用动态库中的函数 int result = addFun(2, 3); printf("%d", result); } //应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。 FreeLibrary(hDll); } return 0; } 能得到你要的结果。你可以在上面的 int result = addFun(2, 3);中设置断点,然后F11单步调试,你可以进入到原动态库里面的语句中。 通过这个简单的例子,我们获知DLL定义和调用的一般概念: 2.声明导出函数 在函数声明中加上__declspec(dllexport),上面已经举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。 l 在dllTest中添加lib.def的文件
(1)DLL中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。
l 在lib.def中加下面的内容: LIBRARY dllTest l .def文件的规则: (1)LIBRARY语句说明.def文件相应的DLL; (3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。 3.Dll的调用方式: 在1中已经介绍了动态调用方式,现在我们来认识静态调用方式: l 创建一个新的控制台项目StaticCallDll,并把下面代码拷贝到 StaticMain.cpp中 #pragma comment(lib,"dllTest.lib") #include <iostream> using namespace std; //.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息 extern "C" __declspec(dllimport) int add(int x,int y); using namespace std; void main(int argc, char* argv[]) { int result = add(2,3); std::cout<<"应用程序中两个数的之和:"<<result<<std::endl; } l 将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径。 当然你也可以在 StaticCallDll的属性—》链接—》常用—》增加Lib路径中设置新的路径。并在链接—》输入—》dllTest.lib这个名字的Lib l 总结: (1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。 程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。 (2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。 静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。 1. 导出变量: DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子。 l 在原来的动态库项目中修改如下: lib.h /* 文件名:lib.h */ #ifndef LIB_H #define LIB_H //对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数 extern "C" int __declspec(dllexport)add(int x, int y); //导出一个全局变量 extern int dllGlobalVar; #endif lib.cpp #include "lib.h" #include "windows.h" #include "stdio.h" #include <iostream> using namespace std; // lib.cpp : DLLMain函数的例子 //如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的 //缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所 //表明的那样。 int dllGlobalVar; //全局变量声明 BOOL APIENTRY DllMain( HANDLE hModule, /* 进程中的每个DLL模块被全局唯一的字节的HINSTANCE句柄标识进程自己还有一个HINSTANCE句柄。 所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始 地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用。进程模块句柄几乎 总是等于x400000,而DLL模块的加载地址的缺省句柄是x10000000。如果程序同时使用了几个DLL模 块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为 加载程序对DLL代码进行了重定位。 */ DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: printf("/nprocess attach of dll/n"); dllGlobalVar = 100; //在dll被加载时,赋全局变量为 break; case DLL_THREAD_ATTACH: printf("/nthread attach of dll/n"); break; case DLL_THREAD_DETACH: printf("/nthread detach of dll/n"); break; case DLL_PROCESS_DETACH: printf("/nprocess detach of dll/n"); break; } return TRUE; } int add(int x, int y) { std::cout<<"动态库中......两个数之积:是吗???"<<(x*y)<<std::endl; return x * y; } lib.def LIBRARY "dllTest" EXPORTS add @ 1 dllGlobalVar CONSTANT ;或dllGlobalVar DATA 从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加: 变量名 CONSTANT //过时的方法 或 变量名 DATA //VC++提示的新方法 在StaticCallDll项目中的StaticMain.cpp里面修改如下 #pragma comment(lib,"dllTest.lib") #include <iostream> using namespace std; //.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息 extern "C" __declspec(dllimport) int add(int x,int y); extern int dllGlobalVar; //声明所导入的并不是DLL中全局变量本身,而是其地址 //extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入 //通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了 using namespace std; void main(int argc, char* argv[]) { int result = add(2,3); std::cout<<"应用程序中两个数的之和:"<<result<<std::endl; ////应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出 printf("%d ", *(int*)dllGlobalVar); *(int*)dllGlobalVar = 1; printf("%d ", *(int*)dllGlobalVar); } l 应用工程中引用DLL中全局变量的一个更好方法: #include <stdio.h> #pragma comment(lib,"dllTest.lib") extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入 int main(int argc, char *argv[]) { printf("%d ", dllGlobalVar); dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换 printf("%d ", dllGlobalVar); return 0; } 通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。 2. 导出类: DLL中定义的类可以在应用工程中使用。 下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们。 l 添加代码: point.h #ifndef _POINT_H #define _POINT_H #ifdef DLL_FILE class _declspec(dllexport) point //导出类point #else class _declspec(dllimport) point //导入类point #endif { public: float y; float x; point(); point(float x_coordinate, float y_coordinate); }; #endif point.cpp #ifndef DLL_FILE #define DLL_FILE #endif #include "point.h" //类point的缺省构造函数 point::point() { x = 0.0; y = 0.0; } //类point的构造函数 point::point(float x_coordinate, float y_coordinate) { x = x_coordinate; y = y_coordinate; } circle.h #ifndef CIRCLE_H #define CIRCLE_H #include "point.h" #ifdef DLL_FILE class _declspec(dllexport)circle //导出类circle #else class _declspec(dllimport)circle //导入类circle #endif { public: void SetCentre(const point ); void SetRadius(float r); float GetGirth(); float GetArea(); circle(); private: float radius; point centre; }; #endif circle.cpp #ifndef DLL_FILE #define DLL_FILE #endif #include "circle.h" #define PI 3.1415926 //circle类的构造函数 circle::circle() { centre = point(0, 0); radius = 0; } //得到圆的面积 float circle::GetArea() { return PI *radius * radius; } //得到圆的周长 float circle::GetGirth() { return 2 *PI * radius; } //设置圆心坐标 void circle::SetCentre(const point centrePoint) { centre = centrePoint; } //设置圆的半径 void circle::SetRadius(float r) { radius = r; } l 编译生成后,在应用程序中用下面的代码,编译再运行。 #pragma comment(lib,"dllTest.lib"); //#include "windows.h" #include "stdio.h" #include "circle.h" //包含类声明头文件 int main(int argc, char *argv[]) { circle c; point p(2.0, 2.0); c.SetCentre(p); c.SetRadius(1.0); printf("area:%f girth:%f", c.GetArea(), c.GetGirth()); return 0; } 从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为 class _declspec(dllexport) point //导出类point { … } 和 class _declspec(dllexport) circle //导出类circle { …. } 而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为: class _declspec(dllimport) point //导入类point { … } 和 class _declspec(dllimport) circle //导入类circle { … }
EXPORTS
add @ 1
(2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。