欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

编写一个dll文件

程序员文章站 2023-12-26 23:22:21
...

使用记事本创建一个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;
}


1.在管理员权限下使用Developer Command Prompt for VS2013编译该文件,并生成中间文件:dll_nolib.obj文件:

编写一个dll文件

2.将该obj文件链接生成dll文件:

编写一个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加载到内存中,但是无法找到dll中的函数。

使用dumpbin命令可查看dll中的函数,可看到dll_nolib.dll中并没有函数:

编写一个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文件


链接dll_def.obj与dll_def.def文件生成dll_def.dll:

编写一个dll文件


使用dumpbin查看生成的dll_def.dll文件,发现该dll输出了函数FuncInDll:

编写一个dll文件


再来显式调用一下上面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;
}

编译执行后:

编写一个dll文件



使用__declspec(dllexport)在源代码中定义dll的输出函数:

写法同上,去掉def文件,在每个要输出的函数前面加上声明__declspec(dllexport),如下:

编写一个dll文件



编译链接dumpbin查看如下即可:

编写一个dll文件

编写一个dll文件

编译后的函数名为 aaa@qq.com@YAXXZ,而非FuncInDll。这是由于C++编译器基于函数重载的考虑,会更改函数名,若是这样,在使用显式调用的时候,也必须使用这个更改后的函数名,这会带来很大的麻烦。为了避免这种现象,使用extern "C"指令来命令C++编译器以C编译器的方式来命名该函数,因此修改后的函数为:

编写一个dll文件





再次编译链接查看dll输出函数,看看到显示的名字正常了,这样之后,显式调用时只需要用FuncInDll函数名就可以了,实际上extern 'c'相当于一个编译开关,它可将C++函数编译为C函数的名字,保持编译后的函数符号等于源代码中的函数名:

编写一个dll文件



隐式调用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文件



上面的这种隐式调用方法很不错,但是在调用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文件


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文件

编写一个dll文件


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++对象,还调用了该对象的成员函数:

编写一个dll文件


总结:

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实现的理由吧。


参考该网址,感谢大牛。

相关标签: dll

上一篇:

下一篇: