利用__declspec(dllexport)和__declspec(dllimport)在Windows平台编写和使用DLL的小例子

前言

关于 __declspec(dllexport)__declspec(dllimport) 这两个关键字在上大学期间就没见过几次面,直到毕业后在公司项目的代码中又遇到过几次,每次也是绕着走,生怕和它产生什么联系,只知道它和动态链接库 DLL 有关,但是当前这个项目中几乎没有用到自己写的动态链接库,所以我也就心安理得的躲了它这么久。

最近看一些开源项目的源码时又发现了这两个关键字,此时凭借自己掌握的知识和学习方法再来看这两个关键字,发现也没有什么值得害怕的地方,其实简单来说就是 __declspec(dllexport) 是用来说明指定类和函数需要从 DLL 中导出的,而 __declspec(dllimport) 是用来说明指定的类和函数是从DLL中导入的。

说明

  • __declspec(dllexport)__declspec(dllimport) 只在 Windows 平台才有,用来说明类或函数的导出和导入。
  • 在 Linux 平台上源文件中的所有函数都有一个的visibility属性,默认导出。如果要隐藏所有函数导出,则需要在GCC编译指令中加入 -fvisibility=hidden 参数。
  • 生成 dll 的同时还会生成对应的 lib 文件,一般是一些索引信息,记录了 dll 中函数的入口和位置,这在之前还真的不知道,原来一直以为 lib 只是静态库文件呢。

疑问

  1. 为什么要导入导出,直接把代码拿过来一起编译不好吗?

想要一起编译前提是你得有源代码,如果人家就给你一个动态库或者静态库,你想把源代码放到一起编译的愿望根本实现不了。

  1. 为什么要分为静态库和动态库?搞这么麻烦,还要导入导出。

这具体的就要查查他们两者的优缺点了,每种事务的产生必要有其产生的原因,比如静态库,很可能就是一个程序员今天在A工程写了一个读取文件的类,过一段时间又在B工程写了一个读取文件的类,代码都差不多,不久又在C工程中直接把代码复制过来改一改又写了一份,这时想到干脆了写个“静态库”这种东西吧,相同的代码直接封装到库中,哪个工程需要就直接拿过来编译,也不需要再复制代码了。

又比如动态库,前面的静态库解决了代码重复开发和维护的问题,但是读取文件的静态库中的代码在A、B、C三个工程中都存在一份,导致每个可执行程序都很大,可不可以共用一份呢?结果又发明了动态库,在编译时只指定函数的入口地址,运行时才加载动态库,这样就使得可执行程序体积大大缩小。

以上内容纯粹我个人想像的,真正发明静态库和动态库是由于什么原因,大家可以自行去了解…

  1. 动态库要比静态库好吗?

个人感觉合适的才是最好的,不存在动态库要比静态库好的说法,最起码不是全都好,动态库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小,但是运行时要去加载库会花费一定的时间,执行速度相对会慢一些,总的来说静态库是牺牲了空间换时间,而动态库是牺牲了时间换空间。

  1. .h(头文件) .lib(库文件) .dll(动态链接库文件) 之间的联系和区别

.h 文件是编译时需要的, .lib 是链接时需要的, .dll 是运行时需要的。如果有 .dll 文件,那么 .lib 一般是一些索引信息,记录了 .dll 中函数的入口和位置,.dll 中是函数的具体的执行内容。如果只有 .lib 文件,那么这个 .lib 文件是静态编译出来的,索引和实现都在文件中。

DLL的编写与使用

前面说了这么多,其实就是想带大家先了解一下动态链接库 DLL ,接下来开始编写一个DLL并在另一个工程中使用它,前提是你已经会使用开发工具VS,如果不会先查查教程。

测试环境

  • VS2013随意版(个人感觉这个版本启动能快一点)
  • Win10畅想版(我也不知道啥版本)

编写DLL

编写 DLL 的方法不知一种,这里只简单介绍一种,对于直接写 .def 文件的方法这里不会展开,尽量依靠开发工具一步步向下执行就好,其实当你理解了开发工具的是怎样工作的,一切就没有那么神秘了,有些步骤直接修改配置文件也是可以实现的,只不过开发工具给我们提供了界面,操作起来更加方便了而已,下面我们开始编写:

  1. 打开VS新建项目,选择Win32项目,项目名称GenDLL,解决方案名称DLLExample,点击确定:

dll_1

  1. 直接下一步,应用程序类型选择DLL,点击完成:

dll_2

  1. 项目会自动创建一个GenDLL.cpp文件,我们在手动创建一个GenDLL.h文件,两个文件中编写如下代码:
1
2
3
4
5
6
7
8
9
// GenDLL.h

#ifdef GENDLL_EXPORTS
#define TEST_API __declspec(dllexport)
#else
#define TEST_API __declspec(dllimport)
#endif

TEST_API int add(int a, int b);
1
2
3
4
5
6
7
8
9
// GenDLL.cpp : 定义 DLL 应用程序的导出函数。

#include "stdafx.h"
#include "GenDLL.h"

TEST_API int add(int a, int b)
{
return a + b;
}

这段代码中有一个 TEST_API 是我在头文件中自定义的,当存在GENDLL_EXPORTS宏时, TEST_API 代表 __declspec(dllexport) 也就是导出函数,当不存在GENDLL_EXPORTS宏时, TEST_API 代表 __declspec(dllimport) 表示导入函数,而 GENDLL_EXPORTS 这个宏是与项目名相关的,自动生成的宏,在 DLL 项目中存在格式为 “大写项目名_EXPORTS”。

也就是说同一个头文件中计算加法的函数 addGenDLL 这个生成 DLL 的项目中表示导出函数,在其他使用这个 DLL 的项目中表示导入函数。

  1. 编译看输出发现有GenDLL.lib和GenDLL.dll两个文件:
1
2
3
4
5
6
7
8
1>------ 已启动生成:  项目: GenDLL, 配置: Debug Win32 ------
1> stdafx.cpp
1> dllmain.cpp
1> GenDLL.cpp
1> 正在创建库 c:\users\administrator\documents\visual studio 2013\Projects\DLLExample\Debug\GenDLL.lib
和对象 c:\users\administrator\documents\visual studio 2013\Projects\DLLExample\Debug\GenDLL.exp
1> GenDLL.vcxproj -> c:\users\administrator\documents\visual studio 2013\Projects\DLLExample\Debug\GenDLL.dll
========== 生成: 成功 1 个,失败 0 个,最新 0 个,跳过 0 个 ==========

使用DLL

  1. 在DLLExample这个解决方案下添加一个新项目,命名为UseDLL,然后点击确定:

dll_3

  1. 直接下一步,应用程序类型选择“控制台应用程序”,点击完成:

dll_4

  1. 在文件UseDLL.cpp文件中引用之前GenDLL项目的头文件,编写使用 add 函数的代码:
1
2
3
4
5
6
7
8
9
10
11
12
// UseDLL.cpp : 定义控制台应用程序的入口点。

#include "stdafx.h"
#include <iostream>
#include "../GenDLL/GenDLL.h"

int _tmain(int argc, _TCHAR* argv[])
{
std::cout << "100+1=" << add(100, 1) << std::endl;
system("pause");
return 0;
}
  1. 编译代码发现报错,提示有一个无法解析的外部命令:
1
2
3
4
5
6
7
8
9
1>------ 已启动生成:  项目: UseDLL, 配置: Debug Win32 ------
1> UseDLL.cpp
1> stdafx.cpp
1> 正在生成代码...
1>UseDLL.obj : error LNK2019: 无法解析的外部符号 "__declspec(dllimport)
int __cdecl add(int,int)" (__imp_?add@@YAHHH@Z),该符号在函数 _wmain 中被引用
1>c:\users\administrator\documents\visual studio 2013\Projects\DLLExample\Debug\UseDLL.exe :
fatal error LNK1120: 1 个无法解析的外部命令
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========

提示这个错误本意就是说链接没有找到函数实现,链接需要什么文件,前面提到需要lib文件,那么我们设置一下,让UseDLL工程能够找到GenDLL.lib文件。

  1. 打开UseDLL工程的属性,在“配置属性->链接器->输入->附加依赖项”中添加GenDLL.lib:

dll_5

  1. 然后在“配置属性->链接器->常规->附加库目录”中添加GenDLL.lib所在路径“../Debug”即可成功编译:

dll_6

  1. 直接运行就可以看到调用DLL的结果,因为这两个工程在同一解决方案下,所以最终UseDLL.exe和GenDLL.dll在同一目录下,这样不会报找不到DLL的错误

dll_7

  1. 如果是不同的目录就会像下图那样,提示找不到GenDLL.dll,只要把GenDLL.dll复制到和UseDLL.exe相同目录即可:

dll_8

加载DLL

上面提到当运行程序找不到 DLL时可以把 DLL 放到可执行程序程序的目录,有时运行大型软件找不到 DLL 时,我们也会下载一个放到System32目录,其实程序在加载 DLL 的时候是会按照一定顺序的,这些目录包括:包含exe文件的目录、进程的当前工作目录、Windows系统目录、Windows目录、Path环境变量中的一系列目录等等,这些目录的搜索顺序还会受到安全 DLL 搜索模式是否启用的影响。

所以说如果不是对DLL 放置的位置有特殊要求,那么直接放在exe文件所在的目录就好了,一般也是会优先搜索的。

总结

  • Windows上才有 __declspec(dllexport)__declspec(dllimport)
  • .h 文件是编译时需要的, .lib 是链接时需要的, .dll 是运行时需要的
  • 程序运行时加载 DLL 一般优先从exe文件的所在目录优先加载
Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客