如何生成一个DLL 参考
在VC++6.0开发环境下,打开File\New\Project选项,可以选择Win32 Dynamic-Link Library或MFC AppWizard【dll】来以不同的方式创建Non-MFC DLL、Regular DLL、Extension DLL等不同种类的动态链接库。下面以选择Win32 Dynamic-Link Library方式来创建一个DLL(实现加法运算):
1、创建一个Win32 Dynamic-Link Library方式的空工程,取名为myDLL
2、分别添加头文件(.h)和源文件(.cpp)
- // mydll.h file
- extern "C" _declspec(dllexport) int add(int a, int b);
- //mydll.cpp file
- #include "mydll.h"
- int add(int a, int b) //该DLL需要导出的函数功能:加法
- {
- return a + b;
- }
说明:
(1)前面的 extern “C” 告诉编译器函数可以在本模块或其他模块中使用,其中“C”表明需按照C语言方式编译和连接它,因为C++编译时,会对函数名进行修饰,用于实现函数重载,而C里面没有这个功能,所以需要用extern "C"在头文件进行声明的时候加以区分,以便链接时能进行正确地函数名查找。
(2)_declspec(dllexport)为导出函数关键字,意为需从DLL中导出该函数,以便使用。
3、编译连接
在进行编译连接后会在Debug目录下找到DLL文件和对应的lib文件
六、如何调用一个DLL
下面实现两种调用方式:单独.dll 和.h + .lib + .dll结合
注:需把对应的 .dll 文件以及.lib 文件和.h文件(结合方式时)拷贝至调用的程序目录下
(1)单纯使用.dll
- #include<wtypes.h>
- #include <winbase.h>
- #include <iostream>
- #include<tchar.h>
- _declspec(dllimport) int Add(int a, int b); //导入声明,亦可以不加,如果加上可加快程序运行
- typedef int(*pAdd)(int a,int b);
- int main()
- {
- HINSTANCE hDLL;
- pAdd Add;
- hDLL=LoadLibrary(_T("mydll.dll")); //加载 DLL文件
- if(hDLL == NULL)std::cout<<"Error!!!\n";
- Add=(pAdd)GetProcAddress(hDLL,"add"); //取DLL中的函数地址,以备调用
- int a =Add(5,8);
- std::cout<<"a: "<<a<<std::endl;
- FreeLibrary(hDLL);
- return 0;
- }
(2).h + .lib + .dll 结合方式
- #include<wtypes.h>
- #include <winbase.h>
- #include <iostream>
- #include "mydll.h"
- #pragma comment(lib,"mydll.lib") //将mydll.lib库文件连接到目标文件中(即本工程)
- extern "C"_declspec(dllimport) int add(int a,int b);
- int main()
- {
- int a =add(5,8);
- std::cout<<"a: "<<a<<std::endl;
- return 0;
- }
#
理解 __declspec(dllexport)和__declspec(dllimport)
1、解决的问题:
考虑下面的需求,使用一个方法,一个是提供者,一个是使用者,二者之间的接口是头文件。头文件中声明了方法,在提供者那里方法应该被声明为__declspec(dllexport),在使用者那里,方法应该被声明为__declspec(dllimport)。二者使用同一个头文件,作为接口,怎么办呢?
2、解决办法:
使用条件编译:定义一个变量,针对提供者和使用者,设置不同的值。
1 #ifndef DLL_H_ 2 #define DLL_H_ 3 4 #ifdef DLLProvider 5 #define DLL_EXPORT_IMPORT __declspec(dllexport) 6 #else 7 #define DLL_EXPORT_IMPORT __declspec(dllimport) 8 #endif 9 10 DLL_EXPORT_IMPORT int add(int ,int); 11 12 #endif
首先新建一个dll工程。里面有2个文件,如下:
dll1.h
#ifdef DLL1_API #else #define DLL1_API _declspec(dllimport) #endif DLL1_API int add(int a,int b); DLL1_API int subtract(int a,int b); /* _declspec(dllimport) int add(int a,int b); _declspec(dllimport) int subtract(int a,int b); */ class DLL1_API Point //把整个类导出 { public: void output(int x,int y); };
如何只导出某个类的成员函数.
DLL1_API void output(int x,int y);
导出某个函数,在外部还是可以使用这个类,Point x;只是只可以使用这个类导出的函数,不能用其他的函数。
dll1.cpp
#define DLL_API _declspec(dllexport) #include"dll1.h" #include<windows.h> #include<stdio.h> int add(int a,int b) { return a+b; } int subtract(int a,int b) { return a-b; } void Point::output(int x,int y) { HWND hwnd=GetForegroundWindow(); HDC hdc=GetDC(hwnd); char buf[20]; memset(buf,0,20); sprintf(buf,"x=%d,y=%d",x,y); TextOut(hdc,0,0,buf,strlen(buf)); ReleaseDC(hwnd,hdc); }
mfc对话框里面有3个按钮,按钮相应程序如下:
应该包含头文件#include "dll1.h"
void CDll1TestDlg::OnBtnAdd() { // TODO: Add your control notification handler code here CString str; str.Format("5+3=%d",add(5,3)); MessageBox(str); } void CDll1TestDlg::OnBtnSub() { // TODO: Add your control notification handler code here CString str; str.Format("5-3=%d",subtract(5,3)); MessageBox(str); } void CDll1TestDlg::OnBtnOutput() { // TODO: Add your control notification handler code here Point pt; pt.output(5,3); }
解决名字改编问题:
c++编译器在生成一个dll时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字是不一样的。这样,如果利用不同的编译器分别生成和访问该dll的客户端程序的话,后者在访问该dll的导出函数时就会出现问题。
把头文件类Point注释起来。
在dll1.h:
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif
DLL1_API int add(int a,int b);
DLL1_API int subtract(int a,int b);
在dll1.cpp前面:
#define DLL_API _declspec(dllexport) 加上 extern "C"
#define DLL_API extern "C" _declspec(dllexport)
利用限定符:extern "C" 可以解决c++和C语言之间相互调用时函数命名的问题。但是这个方法有一个缺陷,就是不能用于导出一个类的成员函数,只能用于导出全局函数这种情况。这就是我吗为什么要将Point类的代码注释起来的原因了。
另外,如果导出函数的调用约定发生了改变,那么即使使用了限定符:”extern C,那么函数的名字仍会发生改编。为了说明这种情况,在dll.h的add,subtract函数前加上_stdcall 关键字:
DLL1_API int _stdcall add(int a,int b);
DLL1_API int _stdcall subtract(int a,int b);
在dll1.cpp源文件中,add函数前也要加上_stdcall关键字:
#define DLL_API extern "C" _declspec(dllexport) #include"dll1.h" #include<windows.h> #include<stdio.h> int _stdcalll add(int a,int b) { return a+b; } int _stdcall subtract(int a,int b) { return a-b; }
如果没有添加_stdcall关键字,那么函数调用约定就是C调用约定,标准调用约定就是WINAPI调用约定,也就是pascal调用约定,这个约定与C调用约定不一样。
也就是说,如果函数的调用约定发生了变化,即使在声明这些函数时使用了extern "C"限定符,他们的名字仍然会发生改编,我们知道,c和delphi调用约定是不一样的,后者使用的是pascal调用约定,即标准调用约定:_stdcall.如果现在需要利用C语言编写一个dll,然后delphi编写的客户端程序访问的话,那么在导出函数时,应指定其使用标准的函数调用约定。但是,这仍会出现问题,因为这时函数名称会发生改编。这种情况下,可以通过一个称为模块定义问题(DEF)的方式来解决名字改编问题。
新建一个dll2工程。
dll2.cpp
int add(int a,int b)
{
return a+b;
}
int subtract(int a,int b)
{
return a-b;
}
增加一个文本文件dll2.def
里面内容如下:
LIBRARY dll2
EXPORTS
add
subtract
用dumpbin执行时函数名字还是原来定义的名字。
显示加载问题
上面的例子都是通过隐式链接加载方式来实现对动态链接库的访问,下面将采用动态加载方式来访问动态链接库。
将最新的dll2.dll文件复制到DLLTEst目录下(不需要dll2.lib),然后在DLLTEst工程中,将dlltestdlg.cpp 文件中包含dll1.h文件的那行代码注释起来,并在该工程设置对话框的link选项卡上,删除对dll1.lib文件的链接。
HMODULE WINAPI LoadLibrary( _In_ LPCTSTR lpFileName );
Loads the specified module into the address space of the calling process. The specified module may cause other modules to be loaded.
LoadLibrary函数不仅可以加载dll,还可以加载可执行模块(.exe).一般来说,当加载可执行模块时,主要是为了访问该模块内的一些资源,例如对话框资源、位图资源或图标资源等。函数返回类型是HMODULE,该类型和HINSTANCE类型可以通用。
GetProcAddress
Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
FARPROC WINAPI GetProcAddress( _In_ HMODULE hModule, _In_ LPCSTR lpProcName );
void CDll1TestDlg::OnBtnAdd() { HINSTANCE hInst; hInst=LoadLibrary("dll2.dll"); typedef int (*ADDPROC)(int a,int b);//定义函数指针类型 ADDPROC add=(ADDPROC)GetProcAddress(hInst,"add"); if(!add) { MessageBox("获取函数地址失败"); return; } CString str; str.Format("5+3=%d",add(5,3)); MessageBox(str); }
完成后程序可以正常执行。通过本例可以看出,动态加载dll时,客户端不再需要包含导出函数的头文件和引入库文件,只需要.dll文件即可。
(隐式链接和动态连接各有其优缺点,实际上,采用隐式方式访问dll时,在程序启动时也是通过调用LoadLibrary函数加载该进程需要的动态连接库的。)