动态链接库开发说明
Posted hanford
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态链接库开发说明相关的知识,希望对你有一定的参考价值。
目录
第1章基本概念
1.1 一个简单的例子
下面将使用VC++创建一个动态链接库文件。这个文件将导出两个函数StringReverseA、StringReverseW,前者将一个ANSI字符串逆序,后者将一个Unicode字符串逆序。
1.1.1 新建一个VC++项目
对于VC++6.0而言,项目类型请选择Win32 Dynamic-Link Library。输入项目名称后,单击"OK"按钮。
在接下来的界面里,选择"An empty DLL project",然后单击"Finish"按钮。
在接下来的界面里单击"OK"按钮完成项目创建。
对于VC++9.0(即VC++2008)而言,项目类型请选择Win32。输入项目名称后,单击"确定"按钮。
在接下来的界面里,请选择"应用程序设置"下的"DLL"和"空项目"。单击"完成"按钮完成项目创建。
1.1.2 添加源文件
对于VC++6.0而言,在Workspace 窗口的 FileView 选项卡内,右键单击"Test files",在右键菜单里单击【Add Files to Project...】菜单项
输入源文件名后,单击"OK"按钮
弹出对话框里询问是否在项目里增加Test.c这个文件的引用。请单击"是"按钮。
此时鼠标双击Test.c。因为这个文件还不存在,VC++6.0会提示是否创建,请单击"是"按钮。
对于VC++9.0而言,在解决方案资源管理器里,右键单击"Test",在右键菜单里单击【添加】【新建项】菜单项。
接下来的界面内,请选择"C++文件(.cpp)",并输入源文件名Test.c,然后单击"添加"按钮。完成Test.c文件的添加和创建。
1.1.3 输入源代码
在Test.c里输入如下源代码:
#include <windows.h>
/***************************************************************************\\ 将一个 Unicode 字符串逆序 \\***************************************************************************/ __declspec(dllexport) wchar_t* WINAPI StringReverseW(wchar_t*wzStr) { if(wzStr) { int p1 = 0; int p2 = wcslen(wzStr) - 1; wchar_t t;
while(p1 < p2) { t = wzStr[p1]; wzStr[p1++] = wzStr[p2]; wzStr[p2--] = t; } } return wzStr; }
/***************************************************************************\\ 将一个 ANSI 字符串逆序 \\***************************************************************************/ __declspec(dllexport) char* WINAPI StringReverseA(char*szStr) { if(szStr) { int nLenA = strlen(szStr) + 1; int nLenW = MultiByteToWideChar(CP_ACP,0,szStr,nLenA,NULL,0); wchar_t*pStrW = (wchar_t*)malloc(nLenW * sizeof(wchar_t));
MultiByteToWideChar(CP_ACP,0,szStr,nLenA,pStrW,nLenW); StringReverseW(pStrW); WideCharToMultiByte(CP_ACP,0,pStrW,nLenW,szStr,nLenA,NULL,NULL); free(pStrW); } return szStr; } |
1.1.4 __declspec(dllexport)
__declspec(dllexport)修饰符用来导出函数StringReverseA和StringReverseW。它还可以导出变量和类,这个后面介绍。
1.1.5 WINAPI
WINAPI 其实就是__stdcall。以StringReverseA为例,调用它时,参数szStr将被压入栈中,从StringReverseA返回时,参数szStr需要出栈。__stdcall表示由StringReverseA自己执行出栈操作。假如将__stdcall去掉或换为__cdecl,则由调用StringReverseA的函数负责执行出栈操作。说了这么多,最重要的是:某些语言,如VB6.0只支持__stdcall,所以为了让这个dll被尽可能多的编程语言支持,请使用WINAPI。
1.1.6 导出符号
现在可以编译程序,生成Test.dll了。使用eXeScope6.30打开Test.dll,可以看到Test.dll确实导出了两个函数。请注意:每个导出函数都有一个序号,它是一个正整数。
不过有意思的是:导出函数的名称并不是StringReverseA和StringReverseW,而是_StringReverseA@4和_StringReverseW@4。
如果把Test.c改名为Test.cpp,则导出的名称更为复杂。请参考下图:
这是什么原因呢?因为Test.c的扩展名为c,VC++使用C编译器进行编译。Test.cpp的扩展名为cpp,VC++使用C++编译器进行编译。C++为了实现函数重载,编译时会根据参数类型和个数对函数名进行再次命名。
1.1.7 DEF文件
如何防止VC++编译器生成dll时将导出函数名更改掉?答案就是使用模块定义文件。请在VC++项目里增加模块定义文件Test.def。这个文件名可以是1.def、A.def……只要扩展名是def即可。编辑Test.def,使其内容如下:
EXPORTS StringReverseA StringReverseW |
上述内容表示:导出函数StringReverseA和StringReverseW。此时,这两个函数前面的__declspec(dllexport)修饰符将不再需要。
DEF文件的功能还有很多,具体请参考MSDN。
1.2 调用动态库
生成的动态库文件可以被多种编程语言使用。限于篇幅下面仅介绍VC++如何调用动态库。
1.2.1 隐式链接
编译动态库文件时,同时会生成Lib文件。使用动态库的VC++程序可以链接这个Lib文件,这就是隐式链接。可参考的代码如下:
#include <windows.h> #include <stdio.h>
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); #pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")
void main() { char szStr[] = "隐式链接动态库"; puts(StringReverseA(szStr)); } |
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr); 是函数声明。修饰符__declspec(dllimport)表示这是一个导入函数。去除这个修饰符不影响程序的编译、运行,但有了__declspec(dllimport)之后,生成的代码更小,运行更快。
注意:对于C++程序而言,可能需要这样声明函数:
extern "C"
{
__declspec(dllimport) char* WINAPI StringReverseA(char*szStr);
}
extern "C" 的作用是:告诉C++编译器连接时不要以C++语法修改StringReverseA的名称。为什么说是"可能"需要extern "C"呢?这与dll的编译有关系。如果使用C编译器编译dll,则需要extern "C";如果使用C++编译器编译dll,则不需要extern "C"。
#pragma comment(lib,"D:/VC6/Test/Debug/Test.lib")表示链接的时候使用D:\\VC6\\Test\\Debug\\Test.Lib文件。就是编译动态库时产生的那个Lib文件。
采用隐式链接,运行程序的时候动态库文件首先被加载至内存。系统如何定位dll文件呢?其搜索顺序为:exe所在目录、当前目录(GetCurrentDirectory)、System32目录(GetSystemDirectory)、Windows目录(GetWindowsDirectory)、环境变量PATH指定的目录。不用记这么多,最保险的做法就是将dll和exe放在同一文件夹下。如果为了多个exe程序共享一个dll,请将这个dll文件复制到System32目录下。
1.2.2 显式链接
显式链接可以灵活控制动态库文件的加载、卸载。其使用步骤如下:
1、使用LoadLibrary函数载入动态库文件至内存;
2、使用GetProcAddress函数获得导出函数的地址;
3、调用导出函数;
4、使用FreeLibrary卸载动态库文件。
可参考如下代码:
#include <windows.h> #include <stdio.h>
void main() { HINSTANCE hDll = LoadLibrary("Test.dll"); //载入动态库文件 if(hDll) {//载入成功 char* (WINAPI*pfn)(char*szStr) = NULL; //声明一个函数指针 //获得函数StringReverseA的指针 #ifdef __cplusplus (FARPROC&)pfn = GetProcAddress(hDll,"StringReverseA"); #else (FARPROC)pfn = GetProcAddress(hDll,"StringReverseA"); #endif if(pfn) {//成功获得函数指针 char szStr[] = "显式链接动态库"; pfn(szStr); //调用函数,等价于(*pfn)(szStr) puts(szStr); } FreeLibrary(hDll); //卸载动态库文件 } } |
需要说明的是
1、LoadLibrary("Test.dll")在载入Test.dll时是有搜索顺序的:首先在exe所在目录查找,然后在当前目录(GetCurrentDirectory)下查找,然后在System32目录下查找……具体请参考MSDN帮助;
2、注意获得函数指针的C代码和C++代码是不同的,它们通过#ifdef __cplusplus这个条件编译语句来区分。或者使用C/C++通用的代码:
typedef char* (WINAPI*STRINGREVERSEA)(char*szStr);
STRINGREVERSEA pfn = (STRINGREVERSEA)GetProcAddress(hDll,"StringReverseA");
3、GetProcAddress的第二个参数可以指定函数名,如:"StringReverseA"。还可以指定为序号,如:GetProcAddress(hDll,(LPCSTR)1)或GetProcAddress(hDll,MAKEINTRESOURCEA(1))。其中1表示导出函数的序号为1。GetProcAddress如何区分第2个参数是名称还是序号?对于字符串而言,首地址是一定大于0xFFFF的,而序号必须小于等于0xFFFF。这就是判断的依据。使用序号定位导出函数,效率上会高一些,但是这样的代码不利于阅读和维护;
4、关于FreeLibrary,需要说明的是:不能自己释放自己。如下面的函数在Test.dll内,其意图是自己释放自己。实际上它是行不通的:
void FreeMyself(HINSTANCE hDll)
{
FreeLibrary(hDll);
}
1.3 导出数据
dll导出数据很简单,下面是一个示例:
__declspec(dllexport) int nDataInDll;
或者
extern "C"
{//C++编译时,防止重命名导出符号nDataInDll
__declspec(dllexport) int nDataInDll;
}
或者使用DEF文件,其内容如下
EXPORTS nDataInDll DATA |
1.3.1 隐式链接
客户端程序隐式链接dll时,使用导出数据很简单。首先是按下列语法声明变量,然后就可以使用了。
extern "C" //是否使用extern "C"需要根据实际情况而定
{
__declspec(dllimport) int nDataInDll;
}
1.3.2 显式链接
客户端程序显式链接dll时,使用导出数据稍显麻烦,其代码如下:
HINSTANCE hDll = LoadLibrary("Test.dll"); //载入动态库文件 if(hDll) {//载入成功 int* nDataDll = NULL; #ifdef __cplusplus (FARPROC&)nDataDll = GetProcAddress(hDll,"nDataInDll"); #else (FARPROC)nDataDll = GetProcAddress(hDll,"nDataInDll"); #endif //使用数据 ... ... ... FreeLibrary(hDll); //卸载动态库文件 } |
注意:GetProcAddress获得的是数据的地址。
1.4 导出类
导出类的语法有两种。方法1是定义类的时候同时定义导出,方法2是先声明类导出,再定义类。方法2比方法1灵活,但有时会有限制。
方法1: class __declspec(dllexport) CTest { public: int m_nValue; CObj m_obj; }; |
方法2: //类声明,说明是一个导出类 class __declspec(dllexport) CTest; class CTest { public: int m_nValue; CObj m_obj; }; |
导出类的实质其实就是把类的成员函数给导出了。
1.4.1 成员类
以上面的代码为例,实例化CTest时需要构造m_obj。因此CObj也必须被导出,否则编译的时候会产生警告,客户程序可能无法正常构造CTest类(Debug版正常,Release版分配内存但不调用构造函数)。
1.4.2 导出模板类
首先看下面的代码
template <class TYPE> class CTemplate { public: CTemplate() { a = 0; } public: TYPE a; };
template class __declspec(dllexport) CTemplate<int>; template class __declspec(dllexport) CTemplate<double>;
class __declspec(dllexport) CUseTemplate { public: CTemplate<int> i; CTemplate<double> d; }; |
导出CUseTemplate时,CTemplate<int>和CTemplate<double>也应该被导出。
注意:类模板是无法导出的,如下面的代码无法导出CTemplate。
template <class TYPE> class __declspec(dllexport) CTemplate { public: CTemplate() { a = 0; } public: TYPE a; }; |
如果不想导出模板类,请修改成员变量为指针类型。这样的话,成员变量的构造、析构将在DLL内完成,而不是在客户程序里完成。