编写一个dll文件
使用记事本创建一个cpp文件,名为dll_nolib.cpp:
代码如下:
#include <objbase.h>
#include <iostream>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void *lpReserved)
{
HANDLE g_hModule;
switch (dwReason)
{
case DLL_PROCESS_ATTACH: //表示dll刚刚被加载到一个进程中
std::cout << "Dll is attached!" << std::endl;
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH: //表示dll刚刚从一个进程中卸载
std::cout << "Dll is detached!" << std::endl;
g_hModule = NULL;
break;
}
return true;
}
2.将该obj文件链接生成dll文件:
加载dll(显示调用)
dll的使用分两种方式,显式调用和隐式调用。显式调用代码如下,创建一个名为dll_nolib_client.cpp的文件:
#include <windows.h>
#include <iostream>
int main(void)
{
//加载我们的dll
HINSTANCE hinst = ::LoadLibrary("dll_nolib.dll");
if (NULL != hinst)
{
std::cout << "dll loaded!" << std::endl;
}
return 0;
}
编译执行:
以上是仅仅将dll加载到内存中,但是无法找到dll中的函数。
使用dumpbin命令可查看dll中的函数,可看到dll_nolib.dll中并没有函数:
在dll中定义输出函数有两种方法:
一是添加一个def定义文件,在此文件中定义dll中要输出的函数;
二是在源代码中待输出的函数前加上__declspec(dllexport)关键字;
在def文件中添加的情况:
写一个带有输出函数的dll,创建名为dll_def.cpp的文件,如下:
#include <objbase.h>
#include <iostream>
extern "C" __declspec(dllexport) void FuncInDll(void)
{
std::cout << "FuncInDll is called!" << std::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;
}
再写一个dll的def文件,dll_def.def:
1. 关键字LIBRARY,指定dll名字;
2.一个可选的关键字DESCRIPTION,写上版权等信息;
3. EXPORT关键字,写上dll中所有要输出的函数名或变量名,接上@以及依次编号的数字(从1到N),若有第2个输出函数FuncInDll2,则在下一行写上FuncInDll2 @2 PRIVATE即可,则最后接上修饰符。如下:
;
;dll_def module-definition file
;
LIBRARY dll_def.dll
DESCRIPTION '(c)2011-2017 stanway'
EXPORTS
FuncInDll @1 PRIVATE
编译dll_def.cpp,生成dll_def.obj文件:
链接dll_def.obj与dll_def.def文件生成dll_def.dll:
使用dumpbin查看生成的dll_def.dll文件,发现该dll输出了函数FuncInDll:
再来显式调用一下上面dll中的函数,创建文件名为dll_def_client.cpp的文件;
1.函数指针的声明
2.GetProcAddress的用来查找dll中的函数地址,第一个参数是dll的句柄,即LoadLibrary返回的句柄;第二个参数是dll中函数的名字,即dumpbin中输出的函数名(该函数名指的是编译后的函数名,不一定等于dll源代码中的函数名)
如下代码:
#include <windows.h>
#include <iostream>
int main(void)
{
//定义一个函数指针
typedef void (* DLLWITHLIB)(void);
//定义一个函数指针变量
DLLWITHLIB pfFuncInDll = NULL;
//加载我们的dll
HINSTANCE hinst = ::LoadLibrary("dll_def.dll");
if (NULL != hinst)
{
std::cout << "dll loaded!" << std::endl;
}
//找到dll的FuncInDll函数
pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll");
//调用dll里的函数
if (NULL != pfFuncInDll)
{
(*pfFuncInDll)();
}
return 0;
}
编译执行后:
使用__declspec(dllexport)在源代码中定义dll的输出函数:
写法同上,去掉def文件,在每个要输出的函数前面加上声明__declspec(dllexport),如下:
编译链接dumpbin查看如下即可:
编译后的函数名为 aaa@qq.com@YAXXZ,而非FuncInDll。这是由于C++编译器基于函数重载的考虑,会更改函数名,若是这样,在使用显式调用的时候,也必须使用这个更改后的函数名,这会带来很大的麻烦。为了避免这种现象,使用extern "C"指令来命令C++编译器以C编译器的方式来命名该函数,因此修改后的函数为:
再次编译链接查看dll输出函数,看看到显示的名字正常了,这样之后,显式调用时只需要用FuncInDll函数名就可以了,实际上extern 'c'相当于一个编译开关,它可将C++函数编译为C函数的名字,保持编译后的函数符号等于源代码中的函数名:
隐式调用dll
显式调用每次都要调用LoadLibrary,且每个函数都必须使用GetProcAddrss来得到函数指针,对大量使用dll函数会造成困扰。然而,使用隐式调用能够像使用C函数库一样使用dll中的函数,非常方便。
创建两个文件dll_withlibAndH.cpp和dll_withlibAndH.h:
dll_withlibAndH.h:
extern "C" __declspec(dllexport) void FuncInDll(void);
dll_withlibAndH.cpp:
#include <objbase.h>
#include <iostream>
#include "dll_withLibAndH.h"
extern "C" __declspec(dllexport) void FuncInDll(void)
{
std::cout << "FuncInDll is called!" << std::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;
}
再写一个客户端程序,dll_withlibAndH_client.cpp,里面只要包含dll_withlibAndH.h文件即可引用函数:
#include "dll_withlibAndH.h"
#pragma comment(lib, "dll_withLibAndH.lib")
int main(void)
{
FuncInDll();
return 0;
}
编译后得到:
上面的这种隐式调用方法很不错,但是在调用dll中的对象和重载函数时会出现问题,因为使用extern 'C'修饰输出函数后, 重载函数肯定会出现问题的,因为它们都被编译为同一个输出符号了(C语言不支持重载)。
实际上,不使用extern 'C'也是可行的,这时的函数会被编译为C++字符串,如 aaa@qq.com@aaa@qq.com,aaa@qq.com@YAXXZ这些。当客户端也使用C++时,也是能正确的进行隐式调用的。
配对使用__declspec(dllexport)以及__declspec(dllimport):
这时要考虑一个情况:若dll1.cpp是源,dll2.cpp使用dll1中的函数,但同时dll2也是一个dll,也要输出一些函数供client.cpp使用。那么在dll2中要如何声明所有的函数呢?它里面既包含了从dll1中引入的函数,又包含了自己要输出的函数。此时就需要使用关键字__declspec(dllexport)和__declspec(dllimport)了。前者用来修饰本dll中的输出函数,后者用来修饰从其他dll中引入的函数。
为了验证这个问题,包括源代码dll1.h,dll1.cpp,dll2.h,dll2.cpp,client.cpp:
dll1.h:
#ifdef DLL_DLL1_EXPORTS
#define DLL_DLL1_API __declspec(dllexport)
#else
#define DLL_DLL1_API __declspec(dllimport)
#endif
DLL_DLL1_API void FuncInDll1(void);
DLL_DLL1_API void FuncInDll1(int);
dll1.cpp:
#define DLL_DLL1_EXPORTS
#include <objbase.h>
#include <iostream>
#include "dll1.h"
DLL_DLL1_API void FuncInDll1(void)
{
std::cout<<"FuncInDll1 is called!"<<std::endl;
}
DLL_DLL1_API void FuncInDll1(int a)
{
std::cout<<"FuncInDll1 is called! input param = "<<a<<std::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;
}
dll2.h:
#include"dll1.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);
dll2.cpp:
#define DLL_DLL2_EXPORTS
#include <objbase.h>
#include <iostream>
#include "dll2.h"
#pragma comment(lib,"dll1.lib")
DLL_DLL2_API void FuncInDll2(void)
{
FuncInDll1();
std::cout<<"FuncInDll2 is called!"<<std::endl;
}
DLL_DLL2_API void FuncInDll2(int a)
{
FuncInDll1(10);
std::cout<<"FuncInDll2 is called! input param = "<<a<<std::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;
}
client.cpp:
#include "dll2.h"
#include <iostream>
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll2.lib")
#pragma comment(lib,"dll1.lib")
int main(void)
{
std::cout<<"call dll"<<std::endl;
FuncInDll1();//只要这样我们就可以调用dll里的函数了
FuncInDll1(5);
FuncInDll2();
FuncInDll2(13);
return 0;
}
一一编译并执行:
此时可看到:
dll中的全局变量和对象
需要注意语法问题,创建三个问文件,dll_object.h,dll_object.cpp, dll_object_client.cpp中:
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);
void show(void);
// TODO: add your methods here.
};
dll_object.cpp:
#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream>
#include "dll_object.h"
DLL_OBJECT_API void FuncInDll(void)
{
std::cout<<"FuncInDll is called!"<<std::endl;
}
DLL_OBJECT_API int g_nDll = 9;
CDll_Object::CDll_Object()
{
std::cout<<"ctor of CDll_Object"<<std::endl;
}
void CDll_Object::show()
{
std::cout<<"function show in class CDll_Object"<<std::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;
}
编译链接查看dll文件显示可知,从上往下分别为:类CDll_Object,类的构造函数,函数FuncInDll,全局变量g_nDll和类成员函数show:
dll_object_client.cpp:
#include "dll_object.h"
#include <iostream>
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll_object.lib")
int main(void)
{
std::cout<<"call dll"<<std::endl;
std::cout<<"call function in dll"<<std::endl;
FuncInDll();//只要这样我们就可以调用dll里的函数了
std::cout<<"global var in dll g_nDll ="<<g_nDll<<std::endl;
std::cout<<"call member function of class CDll_Object in dll"<<std::endl;
CDll_Object obj;
obj.show();
return 0;
}
编译链接,显示可知在客户端成功访问了dll中的全局变量,并创建了dll中定义的C++对象,还调用了该对象的成员函数:
总结:
1.DLL是对应C语言的动态链接技术,在输出C函数和变量时很方便快捷;而在输出C++类,函数时需使用各种手段,而且解决方案还是不完美,除非客户端也使用C++。
2.只有COM是对应C++语言的技术
3.在只有一个调用的时候,使用显式调用较为合理,就是当客户端不是C/C++时,这时无法使用隐式调用的,例如用VB来调用C++写的dll
4.def的功能实际上是相当于extern 'C' __declspec(dllexport)的,所以它也是仅能处理C函数,而不能处理重载函数,而__declspec(dllexport)和__declspec(dllimport)的配合使用可以在任何情况下使用,因此不推荐用def。
5.从其他语言调用dll,有两大问题:
一是函数符号问题,若使用extern 'c',则函数名保持不变,调用较为方便,但不支持函数重载等一系列的C++功能;若不使用extern 'C',则调用前要查编译后的符号,不方便。
二是函数调用压栈顺序问题,即__cdecl和__stdcall问题。__cdecl是常规的C/C++调用约定,这种调用约定下,函数调用后栈的清理工作是由调用者来完成的。__stdcall是标准的调用约定,这种调用约定下,函数会在返回到调用者之前就将参数从栈中删除,即由被调用者清理栈。
这两个问题dll都不能很好的解决,但是在COM中,都能完美的解决。所以,要在Windows平台实现语言无关性,只能使用COM中间件。
因此,除非客户端使用C++,否则dll是不便于支持函数重载,类等C++特性的。dll对C函数支持很好,这也就是为什么windows的函数库使用C加dll实现的理由吧。
参考该网址,感谢大牛。