2015年8月21日 星期五

静态链接库与动态链接库导出函数详解

静态链接库与动态链接库导出函数详解

一、分别编译与链接(Linking
大多数高级语言都支持分别编译,程序员可以显式地把程序划分为独立的模块或文件,然后每个独立部分分别编译。在编译之后,由链接器把这些独立的片段(称为编译单元)“粘接到一起”。(想想这样做有什么好处?)
C/C++中,这些独立的编译单元包括obj文件(一般的源程序编译而成)、lib文件(静态链接的函数库)、dll文件(动态链接的函数库)等。
静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。
动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。
二、静态链接库与动态链接库
先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终的EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。
采用动态链接库的优点:(1)更加节省内存;(2DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。
三、静态链接库的制作
对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。

建立一个静态链接库
如图1,在VC++6.0中new一个名称为libTest的static library工程,并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:
//文件:lib.h
#ifndef LIB_H
#define LIB_H
extern "C" int add(int x,int y);   //声明为C编译、连接方式的外部函数
#endif

//文件:lib.cpp
#include "lib.h"
int add(int x,int y)
{
return x + y;
}

编译这个工程就得到了一个libTest.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件lib.h和libTest.lib文件提交给用户后,用户就可以直接使用其中的add函数了。常用的标准C库函数(scanf、printf、memcpy、strcpy等)就来自这种静态库。
四、静态链接库的调用
下面来看看怎么使用这个库。在VC中new一个名为libCall的Win32 Console Application工程,并将上面生成的文件lib.h和libTest.lib文件拷贝到libCall的工程子目录下。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:
#include <stdio.h>
#include "lib.h"
#pragma comment( lib, "libTest.lib" )   //指定与静态库一起连接
int main()
{
printf( "2 + 3 = %d", add( 2, 3 ) );
}
静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "libTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。
如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择toolsoptionsdirectorieslibrary files菜单或选项,填入库文件路径。图2中加圈的部分为我们添加的libTest.lib文件的路径。

VC中设置库文件路径
这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:
(1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。
以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。

五、认识动态链接库
动态链接是相对于静态链接而言的。所谓静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的exe文件中,该文件包含了运行时所需的全部代码。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,Win32系统保证内存中只有DLL的一份复制品
动态链接库的两种链接方法:
(1) 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中,其主要目的是便于代码共享。
(2) 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。
六、制作一个简单的DLL
第4节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
如图 3,在VC new一个Win32 Dynamic-Link Library工程dllTest。注意不要选择MFC AppWizard(dll),因为这两只方式创建出来的动态库格式不一样。

在建立的工程中添加lib.hlib.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;
}
最后点击Build按钮(或菜单项Build> Build dllTest.dll)。创建成功之后在工程所在子目录的Debug子目录下就可以找到dllTest.dll文件。另外我们还可以看到一个dllTest.lib文件,后面我们解释该文件的作用。
分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数addDLL的导出函数。DLL内的函数分为两种:
(1) DLL导出函数,可供应用程序调用;
(2) DLL内部函数,只能在DLL程序使用,其他应用程序无法调用它们。
DLL中导出函数的声明有两种方式:一种为上面例子中给出的在函数声明中加上__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

七、动态地加载和调用动态库中的函数
VCnew一个名为dllCallWin32 Console Application工程,并将上面生成的dllTest.dll文件拷贝到dllCall的工程子目录下。dllCall工程仅包含一个main.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("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;
}
下面我们来逐一分析上面的程序。
首先,语句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进行了对DLLadd函数的调用;
最后,应用工程使用完DLL后,在函数main中通过Win32 API函数FreeLibrary释放了已经加载的DLL模块。
上面例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统API提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用方式。动态调用方式的特点是完全由编程者用Windows API 函数加载和卸载 DLL,程序员可以在运行时决定 DLL 文件何时加载或不加载,决定加载哪个 DLL 文件。
八、静态地加载和和调用动态库中的函数
与动态调用方式相对应的就是静态调用方式。静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则WindowsDLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。
下面我们来看看静态调用的例子,将编译dllTest工程所生成的dllTest.libdllTest.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:
// 导入库dllTest.lib文件中仅仅是关于其对应的DLL文件中函数的重定位信息
#pragma comment(lib,"dllTest.lib")
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的输出函数,就象调用程序内部的其他函数一样。

九、动态链接库的应用举例
1、所有的Windows系统调用(Windows API函数)都是以动态链接库的形式提供的。我们在Windows目录下的system32文件夹中会看到kernel32.dlluser32.dllgdi32.dllwindows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。与这些动态库相对应的导入库分别为kernel32.libuser32.libgdi32.lib
2、软件的自动更新。Windows应用的开发者常常利用动态链接库来分发软件更新。他们生成一个动态库的新版本,然后用户可以下载,并用它替代当前的版本。当然,新、旧版本动态库的输出接口(即导出函数)必须一致。下一次用户运行应用程序时,应用将自动链接和加载新的动态库。
3、软件插件技术。许多Windows应用软件都支持插件扩展方式,如IE浏览器、PhotoshopOffice等等。插件在本质上都是动态库。
4、可扩展的Web服务器。
5、每个Windows驱动程序在本质上都是动态链接库。

[C++] .lib静态库的使用

关于链接库的讲解在这个网址上有不错的讲解:http://www.360doc.com/content/11/0805/17/6675409_138289992.shtml
下面写一下自己的实践:
首先可以创建一个空工程,然后再工程属性---configuration properties---General---configuration type改为.lib,创建可执行工程默认情况下是.exe
然后,可以在这个工程中创建头文件和源文件,如:
chang.h:
int fun(int a, int b)
chang.cpp:
#include <iostream>
#include "chang01.h"
using namespace std;
int fun(int a, int b)
{
 return a + b;
}
之后编译就会在工程文件夹下的Debug文件夹下生成以当前工程名命名的lib文件,这个会生成chang.lib。
到这里静态链接库文件制作完毕。
下面来说明使用:
创建一个要使用这个库的工程如libtest
第一种方法:
把这个库拷贝到工程目录下,(最好拷贝到vs编译器可以找到的目录下,这个路径在tools---options---projects and soulutions---show libraray的目录下,至于这些目录如何查看,在以前的博客中有相关的文章,这样可以方便编译器编译时候查找到这个库文件)
之后把库头文件拷贝到当前目录下
在libtest的实现文件中,首先包含这个头文件,然后使用pragma指令指明链接库chang.lib,
然后再工程中使用这个函数中的库就可以了,源文件如下:
#include <iostream>
#include "chang.h"
using namespace std;
#pragma comment(lib,"chang.lib")
void main()
{
 cout << fun(2,3) << endl;
}
之后运行就可以了
第二种方法:
这个库文件可以在任意路径下,首先在tools---options---projects and soulutions---show libraray目录下添加这个库文件所在的目录
然后再在工程属性---configuration properties---Linker---Addtional library directories下添加这个库文件所在的目录
在工程属性---configuration properties---Linker---Input---Addtional dependencies添加要使用的库的名字,现在这个就是chang.lib
之后把库文件的头文件拷贝到libtest工程的源文件文件夹下
最后在源文件中就可以不使用pragma指令了,源文件如下:
#include <iostream>
#include "chang.h"
using namespace std;
void main()
{
 cout << fun(2,3) << endl;
}
最后编译运行即可
一些小发现:
当不把头文件拷贝到libtest的时候,直接包含chang.h头文件是不可以的,报找不到头文件
当不包含头文件的时候,直接使用这个库是不可以的,报错是找不到fun函数,
然后当不拷贝头文件到libtest下,而只是在libtest的源文件中进行int fun声明,则可以正常运行;如下:
#include <iostream>
#include "chang.h"
using namespace std;
#pragma comment(lib,"chang.lib")
int fun(int a, int b);
void main()
{
 cout << fun(2,3) << endl;
}
当在libtest下重新实现该函数的时候,会运行重新实现的函数,如下:
#include <iostream>
#include "chang.h"
using namespace std;
#pragma comment(lib,"chang.lib")
int fun(int a, int b);

int fun(int a, int b)
{
 return a - b;
}
void main()
{
 cout << fun(2,3) << endl;
}
有这些现象推测:lib库文件只有函数的二进制实现,而不包含函数的声明,头文件等,
使用的时候也只需要把库链接进来,然后对库中的文件进行完全相同的声明,就可以直接调用库中的文件了,函数名只是索引
但是重新实现后就会运行这个版本,说明执行时候有一定的链接顺序
所以,还是按照一般的做法,把库文件的头文件拷贝过来就好了,只是个人一些推测,还请看到这篇文章的高人指点,呵呵~~

2015年8月20日 星期四

[C++] DLL编写教程

DLL编写教程
半年不能上网,最近网络终于通了,终于可以更新博客了,写点什么呢?决定最近写一个编程技术系列,其内容是一些通用的编程技术。例如DLLCOMSocket,多线程等等。这些技术的特点就是使用广泛,但是误解很多;网上教程很多,但是几乎没有什么优质良品。我以近几个月来的编程经验发现,很有必要好好的总结一下这些编程技术了。一来对自己是总结提高,二来可以方便光顾我博客的朋友。
好了,废话少说,言归正传。第一篇就是《DLL编写教程》,为什么起这么土的名字呢?为什么不叫《轻轻松松写DLL》或者《DLL一日通》呢?或者更nb的《深入简出DLL》呢?呵呵,常常上网搜索资料的弟兄自然知道。
本文对通用的DLL技术做了一个总结,并提供了源代码打包下载,下载地址为:

DLL的优点

简单的说,dll有以下几个优点:
1)      节省内存。同一个软件模块,若是以源代码的形式重用,则会被编译到不同的可执行程序中,同时运行这些exe时这些模块的二进制码会被重复加载到内存中。如果使用dll,则只在内存中加载一次,所有使用该dll的进程会共享此块内存(当然,像dll中的全局变量这种东西是会被每个进程复制一份的)。
2)      不需编译的软件系统升级,若一个软件系统使用了dll,则该dll被改变(函数名不变)时,系统升级只需要更换此dll即可,不需要重新编译整个系统。事实上,很多软件都是以这种方式升级的。例如我们经常玩的星际、魔兽等游戏也是这样进行版本升级的。
3)      Dll库可以供多种编程语言使用,例如用c编写的dll可以在vb中调用。这一点上DLL还做得很不够,因此在dll的基础上发明了COM技术,更好的解决了一系列问题。

最简单的dll

开始写dll之前,你需要一个c/c++编译器和链接器,并关闭你的IDE。是的,把你的VCC++ BUILDER之类的东东都关掉,并打开你以往只用来记电话的记事本程序。不这样做的话,你可能一辈子也不明白dll的真谛。我使用了VC自带的cl编译器和link链接器,它们一般都在vcbin目录下。(若你没有在安装vc的时候选择注册环境变量,那么就立刻将它们的路径加入path吧)如果你还是因为离开了IDE而害怕到哭泣的话,你可以关闭这个页面并继续去看《VC++技术内幕》之类无聊的书了。
最简单的dll并不比chelloworld难,只要一个DllMain函数即可,包含objbase.h头文件(支持COM技术的一个头文件)。若你觉得这个头文件名字难记,那么用windows.H也可以。源代码如下:dll_nolib.cpp
#include <objbase.h>
#include <iostream.h>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       cout<<"Dll is attached!"<<endl;
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
       cout<<"Dll is detached!"<<endl;
       g_hModule=NULL;
       break;
    }
    return true;
}
其中DllMain是每个dll的入口函数,如同cmain函数一样。DllMain带有三个参数,hModule表示本dll的实例句柄(听不懂就不理它,写过windows程序的自然懂),dwReason表示dll当前所处的状态,例如DLL_PROCESS_ATTACH表示dll刚刚被加载到一个进程中,DLL_PROCESS_DETACH表示dll刚刚从一个进程中卸载。当然还有表示加载到线程中和从线程中卸载的状态,这里省略。最后一个参数是一个保留参数(目前和dll的一些状态相关,但是很少使用)。
从上面的程序可以看出,当dll被加载到一个进程中时,dll打印"Dll is attached!"语句;当dll从进程中卸载时,打印"Dll is detached!"语句。
编译dll需要以下两条命令:
cl /c dll_nolib.cpp
这条命令会将cpp编译为obj文件,若不使用/c参数则cl还会试图继续将obj链接为exe,但是这里是一个dll,没有main函数,因此会报错。不要紧,继续使用链接命令。
Link /dll dll_nolib.obj
这条命令会生成dll_nolib.dll
注意,因为编译命令比较简单,所以本文不讨论nmake,有兴趣的可以使用nmake,或者写个bat批处理来编译链接dll

加载DLL(显式调用)

使用dll大体上有两种方式,显式调用和隐式调用。这里首先介绍显式调用。编写一个客户端程序:dll_nolib_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
    //加载我们的dll
    HINSTANCE hinst=::LoadLibrary("dll_nolib.dll"); 
    if (NULL != hinst)
    {
       cout<<"dll loaded!"<<endl;
    }
    return 0;
}
注意,调用dll使用LoadLibrary函数,它的参数就是dll的路径和名称,返回值是dll的句柄。 使用如下命令编译链接客户端:
Cl dll_nolib_client.cpp
并执行dll_nolib_client.exe,得到如下结果:
Dll is attached!
dll loaded!
Dll is detached!
以上结果表明dll已经被客户端加载过。但是这样仅仅能够将dll加载到内存,不能找到dll中的函数。

使用dumpbin命令查看DLL中的函数

Dumpbin命令可以查看一个dll中的输出函数符号名,键入如下命令:
Dumpbin –exports dll_nolib.dll
通过查看,发现dll_nolib.dll并没有输出任何函数。

如何在dll中定义输出函数

总体来说有两种方法,一种是添加一个def定义文件,在此文件中定义dll中要输出的函数;第二种是在源代码中待输出的函数前加上__declspec(dllexport)关键字。

Def文件

首先写一个带有输出函数的dll,源代码如下:dll_def.cpp
#include <objbase.h>
#include <iostream.h>
void FuncInDll (void)
{
    cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}
这个dlldef文件如下:dll_def.def
;
; dll_def module-definition file
;
LIBRARY         dll_def.dll
DESCRIPTION     '(c)2007-2009 Wang Xuebin'
EXPORTS
                FuncInDll @1 PRIVATE
你会发现def的语法很简单,首先是LIBRARY关键字,指定dll的名字;然后一个可选的关键字DESCRIPTION,后面写上版权等信息(不写也可以);最后是EXPORTS关键字,后面写上dll中所有要输出的函数名或变量名,然后接上@以及依次编号的数字(从1N),最后接上修饰符。
用如下命令编译链接带有def文件的dll
Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def
再调用dumpbin查看生成的dll_def.dll
Dumpbin –exports dll_def.dll
得到如下结果:
Dump of file dll_def.dll
File Type: DLL
 Section contains the following exports for dll_def.dll
           0 characteristics
    46E4EE98 time date stamp Mon Sep 10 15:13:28 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names
    ordinal hint RVA      name
          1    0 00001000 FuncInDll
 Summary
        2000 .data
        1000 .rdata
        1000 .reloc
        6000 .text
观察这一行
          1    0 00001000 FuncInDll
会发现该dll输出了函数FuncInDll

显式调用DLL中的函数

写一个dll_def.dll的客户端程序:dll_def_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
    //定义一个函数指针
    typedef void (* DLLWITHLIB )(void); 
    //定义一个函数指针变量
    DLLWITHLIB pfFuncInDll = NULL; 
    //加载我们的dll
    HINSTANCE hinst=::LoadLibrary("dll_def.dll"); 
    if (NULL != hinst)
    {
       cout<<"dll loaded!"<<endl;
    }
    //找到dllFuncInDll函数
    pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll"); 
    //调用dll里的函数
    if (NULL != pfFuncInDll)
    {
       (*pfFuncInDll)();  
    }
    return 0;
}
有两个地方值得注意,第一是函数指针的定义和使用,不懂的随便找本c++书看看;第二是GetProcAddress的使用,这个API是用来查找dll中的函数地址的,第一个参数是DLL的句柄,即LoadLibrary返回的句柄,第二个参数是dll中的函数名称,即dumpbin中输出的函数名(注意,这里的函数名称指的是编译后的函数名,不一定等于dll源代码中的函数名)。
编译链接这个客户端程序,并执行会得到:
dll loaded!
FuncInDll is called!
这表明客户端成功调用了dll中的函数FuncInDll

__declspec(dllexport)

为每个dlldef显得很繁杂,目前def使用已经比较少了,更多的是使用__declspec(dllexport)在源代码中定义dll的输出函数。
Dll写法同上,去掉def文件,并在每个要输出的函数前面加上声明__declspec(dllexport),例如:
__declspec(dllexport) void FuncInDll (void)
这里提供一个dll源程序dll_withlib.cpp,然后编译链接。链接时不需要指定/DEF:参数,直接加/DLL参数即可,
Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj
然后使用dumpbin命令查看,得到:
1    0 00001000 ?FuncInDll@@YAXXZ
可知编译后的函数名为?FuncInDll@@YAXXZ,而并不是FuncInDll,这是因为c++编译器基于函数重载的考虑,会更改函数名,这样使用显式调用的时候,也必须使用这个更改后的函数名,这显然给客户带来麻烦。为了避免这种现象,可以使用extern “C”指令来命令c++编译器以c编译器的方式来命名该函数。修改后的函数声明为:
extern "C" __declspec(dllexport) void FuncInDll (void)
dumpbin命令结果:
1    0 00001000 FuncInDll
这样,显式调用时只需查找函数名为FuncInDll的函数即可成功。

extern “C”

使用extern “C”关键字实际上相当于一个编译器的开关,它可以将c++语言的函数编译为c语言的函数名称。即保持编译后的函数符号名等于源代码中的函数名称。

隐式调用DLL

显式调用显得非常复杂,每次都要LoadLibrary,并且每个函数都必须使用GetProcAddress来得到函数指针,这对于大量使用dll函数的客户是一种困扰。而隐式调用能够像使用c函数库一样使用dll中的函数,非常方便快捷。
下面是一个隐式调用的例子:dll包含两个文件dll_withlibAndH.cppdll_withlibAndH.h
代码如下:dll_withlibAndH.h
extern "C" __declspec(dllexport) void FuncInDll (void);
dll_withlibAndH.cpp
#include <objbase.h>
#include <iostream.h>
#include "dll_withLibAndH.h"//看到没有,这就是我们增加的头文件
extern "C" __declspec(dllexport) void FuncInDll (void)
{
    cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}
编译链接命令:
Cl /c dll_withlibAndH.cpp
Link /dll dll_withlibAndH.obj
在进行隐式调用的时候需要在客户端引入头文件,并在链接时指明dll对应的lib文件(dll只要有函数输出,则链接的时候会产生一个与dll同名的lib文件)位置和名称。然后如同调用api函数库中的函数一样调用dll中的函数,不需要显式的LoadLibraryGetProcAddress。使用最为方便。客户端代码如下:dll_withlibAndH_client.cpp
#include "dll_withLibAndH.h"
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll_withLibAndH.lib")
int main(void)
{
    FuncInDll();//只要这样我们就可以调用dll里的函数了
    return 0;
}

__declspec(dllexport)__declspec(dllimport)配对使用

上面一种隐式调用的方法很不错,但是在调用DLL中的对象和重载函数时会出现问题。因为使用extern “C”修饰了输出函数,因此重载函数肯定是会出问题的,因为它们都将被编译为同一个输出符号串(c语言是不支持重载的)。
事实上不使用extern “C”是可行的,这时函数会被编译为c++符号串,例如(?FuncInDll@@YAXH@Z ?FuncInDll@@YAXXZ),当客户端也是c++时,也能正确的隐式调用。
这时要考虑一个情况:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函数,但同时DLL2也是一个DLL,也要输出一些函数供Client.CPP使用。那么在DLL2中如何声明所有的函数,其中包含了从DLL1中引入的函数,还包括自己要输出的函数。这个时候就需要同时使用__declspec(dllexport)__declspec(dllimport)了。前者用来修饰本dll中的输出函数,后者用来修饰从其它dll中引入的函数。
所有的源代码包括DLL1.HDLL1.CPPDLL2.HDLL2.CPPClient.cpp。源代码可以在下载的包中找到。你可以编译链接并运行试试。
值得关注的是DLL1DLL2中都使用的一个编码方法,见DLL2.H
#ifdef DLL_DLL2_EXPORTS
#define DLL_DLL2_API __declspec(dllexport)
#else
#define DLL_DLL2_API __declspec(dllimport)
#endif
DLL_DLL2_API void FuncInDll2(void);
DLL_DLL2_API void FuncInDll2(int);
在头文件中以这种方式定义宏DLL_DLL2_EXPORTSDLL_DLL2_API,可以确保DLL端的函数用__declspec(dllexport)修饰,而客户端的函数用__declspec(dllimport)修饰。当然,记得在编译dll时加上参数/D “DLL_DLL2_EXPORTS”,或者干脆就在dllcpp文件第一行加上#define DLL_DLL2_EXPORTS
VC生成的代码也是这样的!事实证明,我是抄袭它的,hoho

DLL中的全局变量和对象

解决了重载函数的问题,那么dll中的全局变量和对象都不是问题了,只是有一点语法需要注意。如源代码所示:dll_object.h
#ifdef DLL_OBJECT_EXPORTS
#define DLL_OBJECT_API __declspec(dllexport)
#else
#define DLL_OBJECT_API __declspec(dllimport)
#endif
DLL_OBJECT_API void FuncInDll(void);
extern DLL_OBJECT_API int g_nDll;
class DLL_OBJECT_API CDll_Object {
public:
    CDll_Object(void);
    show(void);
    // TODO: add your methods here.
};
Cpp文件dll_object.cpp如下:
#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream.h>
#include "dll_object.h"
DLL_OBJECT_API void FuncInDll(void)
{
    cout<<"FuncInDll is called!"<<endl;
}
DLL_OBJECT_API int g_nDll = 9;
CDll_Object::CDll_Object()
{
    cout<<"ctor of CDll_Object"<<endl;
}
CDll_Object::show()
{
    cout<<"function show in class CDll_Object"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
    HANDLE g_hModule;
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
       g_hModule = (HINSTANCE)hModule;
       break;
    case DLL_PROCESS_DETACH:
        g_hModule=NULL;
        break;
    }
    return TRUE;
}
编译链接完后Dumpbin一下,可以看到输出了5个符号:
1    0 00001040 ??0CDll_Object@@QAE@XZ
 2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z
 3    2 00001020 ?FuncInDll@@YAXXZ
 4    3 00008040 ?g_nDll@@3HA
 5    4 00001069 ?show@CDll_Object@@QAEHXZ
它们分别代表类CDll_Object,类的构造函数,FuncInDll函数,全局变量g_nDll和类的成员函数show。下面是客户端代码:dll_object_client.cpp
#include "dll_object.h"
#include <iostream.h>
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll_object.lib")
int main(void)
{
    cout<<"call dll"<<endl;
    cout<<"call function in dll"<<endl;
    FuncInDll();//只要这样我们就可以调用dll里的函数了
    cout<<"global var in dll g_nDll ="<<g_nDll<<endl;
    cout<<"call member function of class CDll_Object in dll"<<endl;
    CDll_Object obj;
    obj.show();
    return 0;
}
运行这个客户端可以看到:
call dll
call function in dll
FuncInDll is called!
global var in dll g_nDll =9
call member function of class CDll_Object in dll
ctor of CDll_Object
function show in class CDll_Object
可知,在客户端成功的访问了dll中的全局变量,并创建了dll中定义的C++对象,还调用了该对象的成员函数。

中间的小结

牢记一点,说到底,DLL是对应C语言的动态链接技术,在输出C函数和变量时显得方便快捷;而在输出C++类、函数时需要通过各种手段,而且也并没有完美的解决方案,除非客户端也是c++
记住,只有COM是对应C++语言的技术。
下面开始对各各问题一一小结。

显式调用和隐式调用

何时使用显式调用?何时使用隐式调用?我认为,只有一个时候使用显式调用是合理的,就是当客户端不是C/C++的时候。这时是无法隐式调用的。例如用VB调用C++写的dll。(VB我不会,所以没有例子)

Def__declspec(dllexport)

其实def的功能相当于extern “C” __declspec(dllexport),所以它也仅能处理C函数,而不能处理重载函数。而__declspec(dllexport)__declspec(dllimport)配合使用能够适应任何情况,因此__declspec(dllexport)是更为先进的方法。所以,目前普遍的看法是不使用def文件,我也同意这个看法。

从其它语言调用DLL

从其它编程语言中调用DLL,有两个最大的问题,第一个就是函数符号的问题,前面已经多次提过了。这里有个两难选择,若使用extern “C”,则函数名称保持不变,调用较方便,但是不支持函数重载等一系列c++功能;若不使用extern “C”,则调用前要查看编译后的符号,非常不方便。
第二个问题就是函数调用压栈顺序的问题,即__cdecl__stdcall的问题。__cdecl是常规的C/C++调用约定,这种调用约定下,函数调用后栈的清理工作是由调用者完成的。__stdcall是标准的调用约定,即这些函数将在返回到调用者之前将参数从栈中删除。
这两个问题DLL都不能很好的解决,只能说凑合着用。但是在COM中,都得到了完美的解决。所以,要在Windows平台实现语言无关性,还是只有使用COM中间件。
总而言之,除非客户端也使用C++,否则dll是不便于支持函数重载、类等c++特性的。DLLc函数的支持很好,我想这也是为什么windows的函数库使用Cdll实现的理由之一。

VC中编写DLL

VC中创建、编译、链接dll是非常方便的,点击fileàNewàProjectàWin32 Dynamic-Link Library,输入dll名称dll_InVC然后点击确定。然后选择A DLL that export some symbols,点击Finish。即可得到一个完整的DLL
仔细观察其源代码,是不是有很多地方似曾相识啊,哈哈!

資料來源:http://www.blogjava.net/wxb_nudt/archive/2007/09/11/144371.html