新建项目,选择Windows桌面向导,命名项目名称为Dll1
应用程序类型中选择动态链接库,勾选空项目。
项目创建好后,创建一个Dll1.cpp源文件。
如何导出
应用程序想要访问某个DLL中的函数,这个函数必须是已经导出的函数。导出需要用以下限定声明。
__declspec( dllimport ) declarator
__declspec( dllexport ) declarator
这两个属性明确定义了DLL与客户端的接口,客户端可以是可执行文件或其他DLL。将函数声明为dllexport之后,就不需要在模块定义(.DEF)文件中导出名称了。
These attributes explicitly define the DLL’s interface to its client, which can be the executable file or another >DLL. Declaring functions as dllexport eliminates the need for a module-definition (.DEF) file, at least with >respect to the specification of exported functions. Note that dllexport replaces the __export keyword.
在Dll1.cpp中写入下列代码,注意,这是导出的函数。
_declspec(dllexport) int add(int a, int b)
{return a + b;
}_declspec(dllexport) int subtract(int a, int b)
{return a - b;
}
编译,在项目目录下会出现Dll1.lib和Dll1.dll文件。
我们看到Dll1.dll已经导出了两个函数:add和subtract,我们再编一个测试程序测试这个动态链接库。新建一个基于对话框的MFC引用程序,工程名为:DllTest,并在该工程的主对话框上放置两个按钮,给按钮添加响应函数,在响应函数中分别调用前面的add和subtract函数。
为了能正确的调用函数,我们需要做到或者了解以下几件事情。
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);
测试程序需要链接正确的引入库文件
在项目的链接器中选择附加依赖项,填入刚刚生成的引入库文件——Dll1.lib,如下图所示。
在常规选项卡中设置附加库目录,把刚才生成Dll1.lib的目录添加进去。
再次编译,成功生成了TestDll.exe,运行程序,单击按钮可以获得正确的结果。
查看可执行程序加载的动态库信息
如果我们想查看某个可执行程序的输入信息,以及其加载的DLL信息,也可以使用dumpbin命令来实现。在命令行方式下,进入DllTest.exe文件所在的目录下,然后输入下述命令并回车
dumpbin -imports TestDll.exe
这里面显示了所有加载的DLL信息,比如图中红色矩形框内,显示此可执行程序加载了DLL1.dll库,且调用了库里的两个函数:add和subtract
将动态链接库放在正确的目录下
上述DllTest.exe程序开始运行时,系统将为它分配一个4GB的地址空间,然后加载模块会分析该应用程序的输入信息,从中找到该程序将要访问的动态链接库信息,然后在用户机器上搜索这些动态链接库,进而加载他们。搜索的顺序是:
① 程序的执行目录
程序的执行目录就是指可执行文件所在的目录。
② 当前目录
一般是指活动应用程序启动的目录,应用程序可以通过调用GetCurrentDirectory函数来确定哪个目录是当前目录;也可以通过SetCurrentDirectory函数来修改当前目录。
③ 系统目录
Windows系统目录指的是操作系统的主要文件存放的目录。
④ path环境变量中所列的路径
“环境变量”是操作系统工作环境设置的一些选项或属性参数。每个环境变量由变量名和文件路径组成的,可以设置很多个环境变量。我们一般使用环境变量指定一个文件夹的位置,或一个应用程序的位置等。而path环境变量只是众多环境变量的其中一个,它的变量名叫做“path”,与其他环境变量没有什么区别,只不过“path”这个环境变量经常用到而已。
Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。path环境变量的查看和设置方式如下:通过 右键计算机——>属性——>高级系统设置,进入其中的环境变量即可设置。
DLL的搜索目录就是以上4个,那么,为了能让测试程序在运行时正确加载DLL,需要将DLL放置于上述任意一个目录内。这里,我们选择放在 TestDll.exe所在的目录下。
上述动态链接库虽然初步得以实现,但是使用起来非常不方便,原因是动态链接库一般都会交给客户程序,但是客户端怎么知道你的动态链接库有什么函数?只能通过一些工具(例如dumpbin)查看导出函数,并猜测函数原型。这种做法对DLL的调用很不方便。
因此,常规的做法是在编写动态链接库时,提供一个头文件,在此文件中提供DLL导出函数原型的声明,以及函数有关的注释文档。
此外,这个头文件也可以由动态链接库程序自身使用,实现接口和实现的分离。
首先,修改DLL1项目,在项目中添加头文件dll1.h,内容如下:
//dll1头文件
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif // DLL1_APIDLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
//dll1实现文件
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"int add(int a, int b)
{return a + b;
}int subtract(int a, int b)
{return a - b;
}
程序编译时,头文件不参与编译,源文件单独编译。因此,在编译上述Dll1的cpp文件时,首先定义了DLL1_API宏,将其定义为:_declspec(dllexport)。然后再把头文件包含进来,这将展开头文件,展开之后,首先判断DLL1_API已经定义了,所以不再重新定义该宏,因此add和subtract函数声明的原型都是_declspec(dllexport),表明这两个函数是动态链接库的导出函数。
之后,将这个DLL交由其他程序使用时,只要后者没有定义DLL1_API宏,那么该宏的定义就是:_declspec(dllimport),即add和subtract函数是导入函数。通过上述方法,dll1.h头文件实现了既可以在动态链接库中使用,又可以在客户端使用的目的。
在TestDll项目中,只需要包含此头文件,重新编译运行即可。
从DLL中导出C++类很简单,只需要在类名称前面加上修饰符_declspec(dllexport)即可。例如,我们在Dll1.h头文件中加入如下代码。
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif // DLL1_APIDLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);class DLL1_API Point
{
public:void OutPut(int x, int y);
};
实现文件为
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"
#include
#include 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);CString str;str.Format(_T("x=%d,y=%d"), x, y);TextOut(hdc, 0, 0, str, wcslen(str));ReleaseDC(hwnd, hdc);
}
编译,然后我们查看DLL文件的导出情况。
发现导出了类Point以及其中成员函数OutPut.
我们在测试程序中新增一个按钮,添加按钮的响应函数,代码为
void CTestDllDlg::OnBnClickedBtnOutput()
{// TODO: 在此添加控件通知处理程序代码Point pt;pt.OutPut(5, 3);
}
编译,运行,算式已经写到了客户区。这表明对DLL中的类成员函数调用成功。
现在还有个问题,如果只想导出类中的某个函数怎么办?很简单,不要在类名前加_declspec(dllexport)限定符,只在类中特定函数前加即可。
C++编译器在生成DLL时,会对导出的函数进行名称改编,并且不同的编译器使用的改变规则不一样。这样,如果利用不同的编译器分别生成DLL和访问该DLL的客户端程序的话,后者在访问该DLL的导出函数时就会出现问题。例如,如果用C++语言编写一个DLL,那么用C语言编写的客户端程序访问该DLL中的函数时就会出现问题。因为后者将使用函数原始名称来调用DLL中的函数,而C++编译器已经对该名称进行了改编,所以C语言编写的客户端程序就找不到所需的DLL导出函数。
鉴于以上原因,我们希望动态链接库文件在编译时,导出函数的名称不要发生改变。有以下两种方法可以实现。
//Dll1.h头文件
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif // DLL1_APIDLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);/*
class DLL1_API Point
{
public:void OutPut(int x, int y);
};
*/
//Dll1.cpp 头文件
#define DLL1_API extern "C" _declspec(dllexport)
#include "Dll1.h"
#include
#include 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);CString str;str.Format(_T("x=%d,y=%d"), x, y);TextOut(hdc, 0, 0, str, wcslen(str));ReleaseDC(hwnd, hdc);
}
*/
然后,我们利用dumpbin查看Dll1.dll的导出函数,结果如下。可以看出导出函数的名称均未改编。
//Dll2.h
int add(int a, int b);
int subtract(int a, int b);
实现文件为
//Dll2.cpp
#include "Dll2.h"int add(int a, int b)
{return a + b;
}int subtract(int a, int b)
{return a - b;
}
再在工程中添加.def文件(见下面代码),LIBRARY语句用来指定动态链接库的内部名称,该名称一定要与生成动态链接库的名称一致。EXPORTS语句的作用是表明DLL将要导出的函数。当链接器在链接时,会分析这个DEF文件,当发现EXPORTS语句下面有add和subtract这两个符号名,并且他们与源文件中定义的add和subtract函数的名字是一致的时候,它就会以add和subtract两个符号名导出相应的函数。
LIBRARY DLL2
EXPORTS
add
subtract
编译之后,利用dumpbin查看导出函数,名称没有发生改编。
本文之前的测试程序采用的都是隐式加载DLL的方式,具体来说就是程序在编译阶段通过导入库将需要加载的DLL信息保存在程序中,等到程序启动时,加载模块确保所有所需DLL均加载在内存中,并映射到调用进程的地址空间中。如果需要的动态库比较多,资源浪费是比较严重的。
而动态链接的方式仅仅是在调用某个函数时才加载DLL,因此效率更高。使用动态方式加载动态链接库时,需要用到LoadLibrary函数。该函数的作用是将指定的可执行模块映射到调用进程的地址空间。
现在,我们在刚才新建的Dll2项目下新建一个基于对话框的MFC工程,名称为TestDll2,然后在对话框资源编辑器上增加一个按钮,并添加该按钮的消息响应函数。
void CTestDll2Dlg::OnBnClickedBtnAnd()
{// TODO: 在此添加控件通知处理程序代码HINSTANCE hInst;hInst = LoadLibrary(_T("Dll2.dll"));typedef int (*ADDPROC)(int a, int b); //定义函数指针类型ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add"); //获取dll的导出函数if (!Add){MessageBox(_T("获取函数地址失败"));return;}CString str;str.Format(_T("5+3="), Add(5, 3));MessageBox(str);
}
TestDll2不用在编译阶段链接Dll2.lib,而是使用了LoadLibrary和GetProcAddress函数。后者可以获取动态链接库中某个函数的地址。这样,就可以通过此地址调用相关的函数了。
这里有三点需要注意: