【转自:http://blog.csdn.net/zgb881020/article/details/4039682】
链接库分为静态链接库和动态链接库,而动态链接库在使用时,又进一步分为装载时链接和运行时链接。装载时链接是指该动态链接库是在程序装入时进行加载链接的,而运行时链接是指该动态链接库是在程序运行时执行LoadLibrary(或LoadLibraryEx,下同)函数动态加载的。因此,由于动态链接库有这两种链接方式,所以在编写使用DLL的程序时,就有了两种可选方案。
|
|
接着,再创建一个Win32 Console Application工程DLL_Test,同样将该工程加入先前的DLLTest工作区中,并直接保存在该工作区目录下。然后向工程DLL_Test加入下面的文件:
|
此时,工作差不多做完了,但还需进行一下设置。在Project|Settings里,把两个工程里的General标签里的Intermediate files和Output files都设置为Debug。这样确保两个工程的输出文件在一个目录中,以便后面动态库链接时的查找。另外,设置DLL_Test为活动工程(Project|Set Active Project),设置DLL_Test依赖于DLL_Lib(Project|Dependencies)。此时,就可以编译运行了。运行结果为:
> process attach of dll
max(2, 3) = 3
> process detach of dll
Press any key to continue
下面对上面的代码和结果进行分析。
在dll_lib.h中,EXPORT宏实质上就是一个导出函数所需要的关键字。__declspec (dllexport)是Windows扩展关键字的组合,表示DLL里的对象的存储类型关键字。extern "C"用于C++程序使用该函数时的函数声明的链接属性。WINAPI是宏定义,等价于__stdcall。下面列出Windows编程中常见的几种有关调用约定的宏,它们都是与__stdcall和__cdecl有关的(from windef.h):
#define CALLBACK __stdcall // 用于回调函数
#define WINAPI __stdcall // 用于API函数
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
另外,关于__stdcall:如果通过VC++编写的DLL欲被其他语言编写的程序调用,应将函数的调用约定声明为__stdcall方式,WINAPI、CALLBACK都采用这种方式,而C/C++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionName@number ,而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionName。(小技巧:如何查看这些符号?写一个程序,只提供函数的声明而不给定义,就可以看到链接器给出的符号了)
因此,在前面例子中,该DLL声明了一个导出函数GetMax,其连接属性采用CALLBACK(即__stdcall)。另外,请注意,例子中的宏EXPORT会根据是在C程序还是在C++程序中被调用选择相应的连接方式。在定义导出函数时,不需要EXPORT宏,只需要在函数声明时使用即可。
DllMain函数在DLL载入和卸载时被调用。它的第一个参数是DLL句柄,第三个参数保留。第二个参数用来区分该DLLMain函数是在什么情况下被调用的,如程序所示。如果初始化成功,则DllMain应该返回一个非零值。如果返回零值将导致程序停止运行(你可以修改上面例子中的DllMain的返回值为0,将看到相应的出错结果)。如果在你的DLL程序中没有编写DllMain函数,那么在执行该DLL时,系统将引入一个不做任何操作的缺省DllMain函数版本。
|
此时,不再需要动态的.h文件和.lib文件,只需要提供.dll文件即可。在具体使用时,先用LoadLibrary加载Dll文件,然后用GetProcAddress寻找函数的地址,此时必须提供该函数的在Dll中的名字(不一定与函数名相同)。
然后编译链接、运行,结果与前面的运行结果相同。
下面将解释,为什么前面要去掉WINAPI调用约定(即采用默认的__cdecl方式)。我们可以先看看DLL_Lib.dll里面的链接符号。在cmd中运行命令:
dumpbin /exports DLL_Lib.dll
得到如下结果:
Dump of file f:/code/DLLTest/Debug/Dll_lib.dll File Type: DLL Section contains the following exports for DLL_Lib.dll 0 characteristics ordinal hint RVA name 1 0 0000100A GetMax Summary 4000 .data |
可以看到GetMax函数在编译后在Dll中的名字仍为GetMax,所以在前面的程序中使用的是:
pGetMax =(PGetMax)GetProcAddress(hDll,"GetMax");
然后,我们把WINAPI添加回去,重新编译DLL_Lib工程。运行刚才的DLL_Test程序,运行出错,结果如下:
> process attach of dll
Can't find function "GetMax"
> process detach of dll
Press any key to continue
显然,运行失败原因是因为没有找到GetMax函数。再次运行命令:dumpbin /exports DLL_Lib.dll,结果如下(部分结果):
1 ordinal base ordinal hint RVA name 1 0 0000100A _GetMax@8 |
从上面dumpbin的输出看,GetMax函数在WINAPI调用约定方式下在DLL里的名字与源码中的函数定义时的名字不再相同,其导出名是"_GetMax@8"。此时,你把testMain.c中的函数指针类型声明和函数查找语句作如下修改:
typedef int (WINAPI* PGetMax)(int, int);
pGetMax = (PGetMax)GetProcAddress(hDll, "_GetMax@8");
再次编译链接,然后运行,发现结果又正确了。
现在找到了问题所在。很显然,这种修改方式并不适用,而默认生成的名字又不是我们所想要的。那么该怎么解决这个问题呢?这就需要用到.def文件来解决。
模块定义文件(.def)
模块定义文件(.def文件)是一个描述DLL的各种属性的文件,可以包含一个或多个模块定义语句。如果你不使用关键字__declspec(dllexport)关键字导出DLL中的函数,那么DLL就需要一个.def文件。
一个最小的.def文件必须包含下面的模块定义语句:
(1)文件中第一个语句必须是LIBRARY语句。该语句标记该.def文件属于哪个DLL。语法形式为:LIBRARY <DLL名>。
(2)EXPORTS语句列表。第一个导出语句的形式为:entryname[=internalname] [@ordinal],列出DLL中要导出的函数的名字和可选的序号(ordinal value)。要导出的函数名可以是程序源码中的函数名,也可以定义新的函数别名(但后面必须紧跟[=<原函数名>]);序号必须在范围1到N之间且不能重复,其中N是DLL中导出的函数个数。因此,EXPORTS语句语法形式为:
EXPORTS
<funcName1>[=<InternalName1] [@<num1>]
<funcName2>[=<InternalName2] [@<num2>]
;...
(3)虽然不是必须的,一个.def文件也常常包含DESCRIPTION语句,用来描述该DLL的用途之类,语法形式为:
DESCRIPTION "<Description about the purpose of this DLL.>"
(4)在任意位置,可以包含注释语句,以分号(;)开始。
例如,在本文中后面将用到的.def文件为:
; DLL_Lib.def LIBRARY DLL_Lib ; the dll name EXPORTS ; Ok, over |
现在,让我们回到DLL_Lib工程,修改GetMax函数的声明,把EXPORT去掉,重新编译该工程。然后,运行dumpbin命令,我们发现此时没有导出函数。再将上面的DLL_Lib.def文件添加进DLL_Lib工程,再次编译,并运行dumpbin命令,得到如下结果(引用部分结果):
1 ordinal base ordinal hint RVA name 1 0 0000100A GetMax |
正如我们所预期的,有两个导出函数GetMax和Max。注意,此时源码中的GetMax函数的导出名不再是默认的“_GetMax@8”。另外,需要注意的是,两个导出函数有相同的相对虚拟地址(RVA),也说明了两个导出名实质是同一个函数的不同名字而已,都是源码中GetMax函数的导出名。
现在,回到DLL_Test工程,修改testMain.c文件内容如下:
|
编译链接、运行,结果如下:
> process attach of dll
GetMax(2, 3) = 3
Max(2, 3) = 3
> process detach of dll
Press any key to continue
运行结果正如前面分析的那样,GetMax和Max都得到了相同的结果。
到这里,我们解决了DLL导出函数名在各种调用约定下的默认名可能不同于源码中函数名的问题。此时,你就可以制作跟Windows的自带API函数库相同的库了:使用__stdcall调用约定以满足Windows下的任何语言都可以调用DLL库,同时使用函数名作为导出名,以方便用户使用DLL里的函数。
#pragma data_seg ("shared")
int g_oneNumber = 0;
#pragma data_seg ()
若要创建纯资源 DLL,请创建一个新的 Win32 DLL(非 MFC)项目,并将资源添加到此项目。
在“新建项目”对话框中选择“Win32 项目”,并在“Win32 项目向导”中指定 DLL 项目类型。
为 DLL 创建一个包含资源(如字符串或菜单)的新资源脚本,并保存该 .rc 文件。如果该.rc文件包含位图用记事本打开它,可以看到下面这样的一段
DIB_BKGD_HF BITMAP /"bkgd_**.bmp/"
DIB_BKGD_GT BITMAP /"bkgd_***.bmp/"
DIB_BKGD_BF BITMAP /"bkgd_*****.bmp/"
这些就是针对位图的申明,将相应的位图添加到这个里面就行了。
在“项目”菜单上单击“添加现有项”,然后在项目中插入这个新的 .rc 文件。
指定 /NOENTRY 链接器选项。/NOENTRY 防止链接器将 _main 引用链接到 DLL 中;此选项是创建纯资源 DLL 所必需的。
生成 DLL。
使用纯资源 DLL 的应用程序应调用 LoadLibrary 来显式链接到 DLL。若要访问资源,请调用一般函数 FindResource 和 LoadResource,这两个函数对任何类型的资源都有效,或调用下列资源特定的函数之一:
FormatMessage
LoadAccelerators
LoadBitmap
LoadCursor
LoadIcon
LoadMenu
LoadString
使用完资源后,应用程序应调用 FreeLibrary。
下面部份是说明一下资源中的BMP如何动态加载并显示
hmodule = LoadLibrary(/"ExtendDLL.dll/");
HDC m_hdcMem;
m_hdcMem = CreateCompatibleDC(hdc);
BITMAP bm;
RECT rect;
HBITMAP hBitmap = LoadBitmap(hmodule,/"DIB_BKGD_HF/"); //MAKEINTRESOURCE(IDB_BITMAP1)
//HBITMAP hBitmap = ( HBITMAP )LoadImage( hmodule, /"DIB_BKGD_HF/",IMAGE_BITMAP,0,0,LR_DEFAULTSIZE);
GetObject( hBitmap, sizeof BITMAP, &bm);
SelectObject( m_hdcMem, hBitmap);
GetClientRect( hWnd, &rect);
//::SetStretchBltMode(hdc,COLORONCOLOR); [Page]
//::StretchBlt(hdc, rect.left, rect.top, rect.right, rect.bottom, m_hdcMem, 0, 0, bm.bmWidth, bm.bmHeight,SRCCOPY);
BitBlt(hdc,0,0,bm.bmWidth,bm.bmHeight,m_hdcMem,0,0,SRCCOPY);
ReleaseDC( hWnd, m_hdcMem );
FreeLibrary(hmodule);