对Windows下的动态库编程并不很熟悉。下午利用一点时间写了个原型,实现过程中想到许多问题,网上搜到许多文章,学到不少知识,但感觉比较杂乱,于是决定理一理,就有了这篇博文。
一、 静态库与动态库
库(library),一般是一种可执行的二进制格式,被操作系统载入内存执行。
静态库:
在链接的时候,链接器将目标文件(.obj)和用到的静态库一起打包到最后生成的可执行文件中。因此,使用了静态库的可执行程序存储在磁盘上的空间就比较大。Windows上的静态库是.lib文件(但和dll文件的.lib文件是不同的,下面会有阐述)。
动态库:
在可执行程序被加载到内存中执行的时候,才会去加载动态库。Widows上的动态库是dll文件(Dynamic Linked Library)。
静态库与动态库的区别:
1. 使用了静态库的程序存储在磁盘上的空间比使用了动态库的程序要多。
2. 使用了动态库的程序,若有多个副本在内存中执行,又或者是不同的程序但都使用了同一个动态库,则它们所调用的动态库在内存中只有一份,所以很节省内存空间;而使用了静态库的程序的多个副本在内存中时,它们所使用的库所占的内存也是多份,因此浪费空间。
3. 在库需要升级的时候,使用动态库的程序只需要升级动态库就好(假设接口不变),而使用了静态库的程序则需要升级整个程序。
在Windows操作系统中,Visual Studio使用lib.exe作为库的管理工具,负责创建静态库和动态库。
Window与Linux执行文件格式不同,在创建动态库的时候有一些差异:
- 在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数作为初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。
- Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
二、在Windows下创建和使用静态库
创建静态库有3个方法:
创建静态库方法一
1. 通过使用带编译器选项 /c 的 Cl.exe 编译代码 (cl/c StaticMath.cpp),创建名为“StaticMath.obj”的目标文件。
2. 然后,使用库管理器 Lib.exe 链接代码 (lib StaticMath.obj),创建静态库StaticMath.lib。
创建静态库方法二
创建Win32控制台程序时,勾选静态库类型;
创建静态库方法三
工程的“Properties” -> "Configuration Properties” -> ”General”,将ConfigurationType选为Static Library(.lib)。
使用静态库有2个方法:
使用静态库方法一
在 Project’s Properties -> Linker-> Command Line -> Additional Options 添加静态库的完整路径。
使用静态库方法二
在 Project's Properties -> Linker -> General-> Additional Library Directories,添加静态库所在目录;
在 Project’s Properties -> Linker -> Input ->Additional Dependencies,添加静态库的文件名。
注意,静态库的文件名叫xxx.lib,和动态库的导入库文件的文件后缀名相同。二者内容当然是完全不同的:静态库文件当然包含了执行代码和符号表,而动态库的导入库文件则只包含了地址符号表。
三、__declspec(dllexport) 和 __declspec(dllimport)
这2个宏是Windows对动态库(dll)进行编程和使用的时候所特有的,在Linux系统上则不需要这2个宏。
先来看看它们的作用:
1. __declspec(dllexport)可以被用来修饰一个函数或一个类,以表明这是一个导出函数或导出类。所以,这个宏是用在为dll做编程实现的时候的。
当修饰类的时候,该宏要写在class关键字的后面、类名的前面;
当修饰函数的时候,要写在函数声明的前面,而因为name mangling的问题,在此宏的前面还要写上extern“C”. 比如:
extern "C" MYDLL_DECL_EXPORT void say_hello();
2. __declspec(dllimport) 被用来在调用dll的程序里,表明该程序要调用的某个函数是import自某动态库的。所以,该宏的具体位置是在对dll进行描述的头文件中的。
3. 从以上可以看出,在dll的实现中,我们需要__declspec(dllexport)来表明这些函数和类是导出函数和导出类,而在使用dll的程序中,又要用__declspec(dllimport)来表明它所描述的函数或类是来自于某dll。那么这样的话,岂不是需要2个不同但又很相近的头文件来做这些函数和类的声明了吗?能否将这2个函数合并成一个呢?答案是可以的 – 使用宏进行判断:当宏A存在时,就认为宏B是__declspec(dllexport),否则就认为宏B是__declspec(dllimport)。具体实例如下:
#ifdef MYDLL_EXPORTS
#define MYDLL_DECL_EXPORT __declspec(dllexport)
#else
#define MYDLL_DECL_EXPORT __declspec(dllimport)
#endif
所以,在dll的实现中,需要在Preprocessor Definitions里定义MYDLL_EXPORTS,而在dll的使用者那里就不需要定义MYDLL_EXPORTS了。
四、动态库的调用方式
动态库的调用方式有2种:隐式调用 和 显式调用。
1. 隐式调用
和调用静态库方法二类似,除了需要头文件之外,还要:
在 Project’s Properties -> Linker-> General -> Additional Library Directories,添加动态库的导入库文件(即.lib文件)的所在目录;
在Project’s Properties -> Linker -> Input ->Additional Dependencies,添加动态库的导入库的文件名。
隐式调用的优点是,既可以使用导出函数,也可以使用导出类。
2. 显式调用
应用程序在运行时显式加载 DLL:
a. 调用 LoadLibrary(或相似的函数)以加载 DLL 和获取模块句柄。
b. 调用 GetProcAddress,以获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL的函数,编译器不生成外部引用,故无需与导入库链接。
c. 使用完 DLL 后调用 FreeLibrary。
显式调用有一个比较大的问题,就是很难使用导出类。很难并不是不可以,可以用专业工具修改.def并将类也声明为extern“C”的方式来做到,但是是不推荐这样做的。具体文献可以参考这里:
http://www.cppblog.com/codejie/archive/2009/09/24/97141.html
http://blog.csdn.net/denny_233/article/details/7255673
五、如何找到动态库
在上一节中,细心的读者会发现,我们只是在VS中设置和.lib相关的选项,就可以使用动态库了。但是可执行文件又是如何找到dll文件的呢?毕竟.lib文件未必需要和dll文件放在一起。
其实,搜索动态库文件dll的顺序是这样的:
1. 包含EXE文件的目录,
2. 进程的当前工作目录,
3. Windows系统目录,
4. Windows目录,
5. 列在Path环境变量中的一系列目录。
六、创建和使用动态库的程序实例
以上都是理论部分,下面来实践一下。
首先,我们来创建一个动态库,叫做MyDll.dll. 这首选需要在VS下创建一个dll的工程,具体过程就不赘述了。总共只需要2个文件,MyDll.h 和 MyDll.cpp,详细代码如下:
MyDll.h如下:
#ifndef MYDLL_H
#define MYDLL_H
#ifdef MYDLL_EXPORTS
# define MYDLL_DECL_EXPORT __declspec(dllexport)
#else
# define MYDLL_DECL_EXPORT __declspec(dllimport)
#endif
class MYDLL_DECL_EXPORT MyDll {
public:
MyDll(int x=0, int y=0);
MyDll(const MyDll &) = delete;
~MyDll(){}
MyDll operator = (const MyDll &) = delete;
long Add();
long Sub();
int GetFirstNumber();
void SetFirstNumber(int x);
int GetSecondNumber();
void SetSecondNumber(int y);
private:
int mFirstNum;
int mSecondNum;
};
extern "C" MYDLL_DECL_EXPORT void say_hello();
#endif
MyDll.cpp如下:
#include "MyDll.h"
#include <iostream>
MyDll::MyDll(int x, int y) :mFirstNum(x), mSecondNum(y){}
long MyDll::Add() {
return long(mFirstNum) + long(mSecondNum);
}
long MyDll::Sub() {
return long(mFirstNum - mSecondNum);
}
int MyDll::GetFirstNumber() {
return mFirstNum;
}
int MyDll::GetSecondNumber() {
return mSecondNum;
}
void MyDll::SetFirstNumber(int x) {
mFirstNum = x;
}
void MyDll::SetSecondNumber(int y) {
mSecondNum = y;
}
// standalone function but also exposed
MYDLL_DECL_EXPORT void say_hello() {
std::cout << "Hello, World!" << std::endl;
}
这个dll工程build后,就可以生成MyDll.dll和MyDll.lib了。但是要注意,在Preprocessor Definitions中要记得添加上MYDLL_EXPORTS宏。否则就没啥导出的了。
然后,再建一个Windows Console工程,里面只需要一个cpp文件:
#include <Windows.h>
#include "../MyDll/MyDll.h"
#include <iostream>
using namespace std;
void way_1_with_lib_and_dll() {
cout << "In way 1: with lib and dll" << endl;
MyDll m1(10, 20);
long add_result = m1.Add();
cout << "add result is " << add_result << endl;
cout << "Way 1 ends" << endl;
}
void way_2_with_loadlibrary() {
cout << "In way 2: with loadlibrary" << endl;
HMODULE handle = LoadLibrary(L"C:\\Users\\Finix\\Documents\\Visual Studio 2013\\Projects\\TestDLL\\Debug\\TestDLL.dll");
if (handle) {
cout << "handle Not NULL!" << endl;
typedef void(*SAYHELLO)();
SAYHELLO func = reinterpret_cast<SAYHELLO>(GetProcAddress(handle, "say_hello"));
func();
if (!FreeLibrary(handle)) {
cout << "Free failed!" << endl;
}
}
cout << "Way 2 ends" << endl;
}
int main() {
way_1_with_lib_and_dll();
way_2_with_loadlibrary();
return 0;
}
此文件中使用了2种方法来调用MyDll.dll,即隐式调用和显式调用。最后将第二个工程设置为Startup工程,build后再按Ctrl + F5,就可以看到执行结果了。