113E博章

       登录 /注册
首页 一算子网 发布
分类 :c++/vc

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 动态链接库。


图6 建立一个非mfc dll

  在建立的工程中添加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所示的对话框。


图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 = 1;

  其结果是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 &centrepoint);
  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 &centrepoint)
{
 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

浏览模式 : 显示全部 | 评论 : 0 | 排序 | 浏览 : 154

我要发表文章
回复标题:  
    Email :
* 请输入验证码
 
统计数据
文章数量 : 300297
评论次数 : 460
访问次数 : 19290671
在线人数 : 2023

附近文章
  • C+/CLI中栈对象的设计问题...
  • 托管C+中GDI+和GDI混合编...
  • 用premiere制作多声道vcd...
  • vc中利用原始套接字实现网...
  • VC+动态链接库编程之基础...
  • VC+动态链接库编程之MFC规...
  • VC+动态链接库编程之读者...
  • 英国投票否决C+/CLI 微软...
  • C+内存对象大会战
  • 在VC+应用程序中实现颜色...
热门文章
  • 如...
  • Socket编程中select()的妙...
  • 如何在DLL中创建窗体(对话...
  • 重载赋值运算符 C+/VC
  • 展现TRACE之威力 C+/VC
  • ·用豪杰大眼睛制作照片v...
  • 一個簡單的錄音放音程序 ...
  • 用Socket接口实现网络异步...
  • MFC动态创建控件的消息处...
  • 实现服务器端的多线程SOC...
最新文章
  • 股票技术不准了
  • 2009年元旦放假安排
  • 三国 旬彧VS诸葛亮
  • ASR浮筹比例
  • 杨百万投资股票的三大秘诀...
最近评论
  • xueshanfh
  • 匿名
  • 匿名
  • 匿名
  • 匿名
  • 匿名Martha Jack
  • 匿名
  • Tom Moddy
  • 匿名Jimmy Adams
  • 匿名
 
 

Copyright (C) 一算子网 2006-2008, All Rights Reserved 京ICP备06020154号
服务QQ: 有问题请留言 留言邮箱pinhu530@sohu.com