VC+动态链接库编程之非MFC DLL
[
2007-6-10 23:56:00
| 发表者 :
zihe
]
4.1一个简单的dll
第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
如图6,在vc++中new一个win32 dynamic-link library工程dlltest(
单击此处下载本工程)。注意不要选择mfc appwizard(dll),因为用mfc appwizard(dll)建立的将是第5、6节要讲述的mfc 动态链接库。
在建立的工程中添加lib.h及lib.cpp文件,源代码如下:
/* 文件名:lib.h */
#ifndef lib_h #define lib_h extern "c" int __declspec(dllexport)add(int x, int y); #endif
/* 文件名:lib.cpp */
#include "lib.h" int add(int x, int y) { return x + y; } |
与第2节对静态链接库的调用相似,我们也建立一个与dll工程处于同一工作区的应用工程dllcall,它调用dll中的函数add,其源代码如下:
#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\\dlltest.dll"); if (hdll != null) { addfun = (lpaddfun)getprocaddress(hdll, "add"); if (addfun != null) { int result = addfun(2, 3); printf("%d", result); } freelibrary(hdll); } return 0; }
|
分析上述代码,dlltest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为dll的导出函数。dll内的函数分为两种:
(1)dll导出函数,可供应用程序调用;
(2) dll内部函数,只能在dll程序使用,应用程序无法调用它们。
而应用程序对本dll的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。
首先,语句typedef int ( * lpaddfun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpaddfun的实例addfun;
其次,在函数main中定义了一个dll hinstance句柄实例hdll,通过win32 api函数loadlibrary动态加载了dll模块并将dll模块句柄赋给了hdll;
再次,在函数main中通过win32 api函数getprocaddress得到了所加载dll模块中函数add的地址并赋给了addfun。经由函数指针addfun进行了对dll中add函数的调用;
最后,应用工程使用完dll后,在函数main中通过win32 api函数freelibrary释放了已经加载的dll模块。
通过这个简单的例子,我们获知dll定义和调用的一般概念:
(1)dll中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用dll的导出函数(或变量、类)。
下面我们来对“特定的方式进行”阐述。
4.2 声明导出函数
dll中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
下面的代码演示了怎样同.def文件将函数add声明为dll导出函数(需在dlltest工程中添加lib.def文件):
; lib.def : 导出dll函数
library dlltest
exports
add @ 1 |
.def文件的规则为:
(1)library语句说明.def文件相应的dll;
(2)exports语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
(3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“dlltest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
4.3 dll的调用方式
在4.1节的例子中我们看到了由“loadlibrary-getprocaddress-freelibrary”系统api提供的三位一体“dll加载-dll函数地址获取-dll释放”方式,这种调用方式称为dll的动态调用。
动态调用方式的特点是完全由编程者用 api 函数加载和卸载 dll,程序员可以决定 dll 文件何时加载或不加载,显式链接在运行时决定加载哪个 dll 文件。
与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态ip与dhcp、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库dll,而想不到,深入到dll内部,其调用方式也分为静态与动态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。
静态调用方式的特点是由编译系统完成对dll的加载和应用程序结束时 dll 的卸载。当调用某dll的应用程序结束时,若系统中还有其它程序使用该 dll,则windows对dll的应用记录减1,直到所有使用该dll的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。
下面我们来看看静态调用的例子(
单击此处下载本工程),将编译dlltest工程所生成的.lib和.dll文件拷入dllcall工程所在的路径,dllcall执行下列代码:
#pragma comment(lib,"dlltest.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",result); return 0; } |
由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:
(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的输出函数,就象调用程序内部的其他函数一样。
4.4 dllmain函数
windows在加载dll的时候,需要一个入口函数,就如同控制台或dos程序需要main函数、win32程序需要winmain函数一样。在前面的例子中,dll并没有提供dllmain函数,应用工程也能成功引用dll,这是因为windows在找不到dllmain的时候,系统会从其它运行库中引入一个不做任何操作的缺省dllmain函数版本,并不意味着dll可以放弃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\\dlltest.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 |
4.5 __stdcall约定
如果通过vc++编写的dll欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,winapi都采用这种方式,而c/c++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用c编译方式(在c++中需将函数声明为extern "c"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):
#define callback __stdcall //这就是传说中的回调函数 #define winapi __stdcall //这就是传说中的winapi #define winapiv __cdecl #define apientry winapi //dllmain的入口就在这里 #define apiprivate __stdcall #define pascal __stdcall |
在lib.h中,应这样声明add函数:
| int __stdcall add(int x, int y); |
在应用工程中函数指针类型应定义为:
| typedef int(__stdcall *lpaddfun)(int, int); |
若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpaddfun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。
图8中的那段话实际上已经给出了错误的原因,即“this is usually a result of …”。
单击此处下载__stdcall调用例子工程源代码。
4.6 dll导出变量
dll定义的全局变量可以被调用进程访问;dll也可以访问调用进程的全局数据,我们来看看在应用工程中引用dll中变量的例子(单击此处下载本工程)。
/* 文件名:lib.h */
#ifndef lib_h #define lib_h extern int dllglobalvar; #endif
/* 文件名:lib.cpp */
#include "lib.h" #include <windows.h>
int dllglobalvar;
bool apientry dllmain(handle hmodule, dword ul_reason_for_call, lpvoid lpreserved) { switch (ul_reason_for_call) { case dll_process_attach: dllglobalvar = 100; //在dll被加载时,赋全局变量为100 break; case dll_thread_attach: case dll_thread_detach: case dll_process_detach: break; } return true; }
|
;文件名:lib.def
;在dll中导出变量
library "dlltest"
exports
dllglobalvar constant
;或dllglobalvar data
getglobalvar |
从lib.h和lib.cpp中可以看出,全局变量在dll中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的exports后添加:
变量名 constant //过时的方法
或
变量名 data //vc++提示的新方法
在主函数中引用dll中定义的全局变量:
#include <stdio.h> #pragma comment(lib,"dlltest.lib")
extern int dllglobalvar;
int main(int argc, char *argv[]) { printf("%d ", *(int*)dllglobalvar); *(int*)dllglobalvar = 1; printf("%d ", *(int*)dllglobalvar); return 0; } |
特别要注意的是用extern int dllglobalvar声明所导入的并不是dll中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用dll中的全局变量。这一点,从*(int*)dllglobalvar可以看出。因此在采用这种方式引用dll全局变量时,千万不要进行这样的赋值操作:
其结果是dllglobalvar指针的内容发生变化,程序中以后再也引用不到dll中的全局变量了。
在应用工程中引用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中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
4.7 dll导出类
dll中定义的类可以在应用工程中使用。
下面的例子里,我们在dll中定义了point和circle两个类,并在应用工程中引用了它们(
单击此处下载本工程)。
//文件名:point.h,point类的声明
#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,point类的实现
#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,circle类的声明
#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 ¢repoint); void setradius(float r); float getgirth(); float getarea(); circle(); private: float radius; point centre; };
#endif
//文件名:circle.cpp,circle类的实现
#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 ¢repoint) { centre = centrepoint; }
//设置圆的半径
void circle::setradius(float r) { radius = r; } |
类的引用:
#include "..\circle.h" //包含类声明头文件
#pragma comment(lib,"dlltest.lib");
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 { … } |
不错,正是通过dll中的
class _declspec(dllexport) class_name //导出类circle { … } |
与应用程序中的
class _declspec(dllimport) class_name //导入类 { … } |
匹对来完成类的导出和导入的!
我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:
#ifdef dll_file class _declspec(dllexport) class_name //导出类 #else class _declspec(dllimport) class_name //导入类 #endif |
实际上,在mfc dll的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。
由此可见,应用工程中几乎可以看到dll中的一切,包括函数、变量以及类,这就是dll所要提供的强大能力。只要dll释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!
本章虽以vc++为平台讲解非mfc dll,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。 接下来,我们将要研究mfc规则dll。
上一篇:
VC+动态链接库编程之基础慨念 下一篇:
VC+动态链接库编程之MFC规则DLL