前言:C以及C++的动态链接库和静态链接库,说起来很简单,但是实际上在创建的过程中有很多的坑,本人也是一路踩了很多坑,查了很多资料,下决定写一篇完整的文章来详细解释使用VS创建C++动态链接库的完整流程。本文的开发环境是VS 2017。
DLL即动态链接库(Dynamic-Link Library)的缩写,相当于Linux下的共享对象。Windows系统中大量采用DLL机制,甚至内核的结构很大程度依赖于DLL机制。Windows下的DLL文件和EXE文件实际上是一个概念,都是PE格式的二进制文件。
1.1 Windows下面的动态链接库与Linux下面的动态链接库的区别
(1)文件后缀不同
Linux动态库的后缀是 .so 文件,而window则是 .dll 文件
(2)文件格式不同
(a)Linux下是ELF格式,即Executable and Linkable Format
在ELF之下,共享库中所有的全局函数和变量在默认情况下都可以被其它模块使用,即ELF默认导出所有的全局符号。
(b)Windows下面是PE格式的文件,即Portable Executable Format
DLL本质上也是PE文件,DLL需要显示地“告诉”编译器需要导出某个符号,否则编译器默认所有的符号都不导出。
(c)动态链接库的文件个数不一样
Linux的动态链接库就只有一个 .so 文件,还有与之对应的头文件,而在Windows下面的动态库有两个文件,
一个是引入库(.LIB)文件,
一个是动态库(.DLL)文件,
需要的头文件(.h)文件
(1)引入库文件包含被DLL导出的函数的名称和位置,对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
(2)DLL文件包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。
总结:从上面的说明可以看出,Windows下面所创建的动态链接库DLL和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。一般的动态库程序有lib文件和dll文件,lib文件是编译时期连接到应用程序中的,而dll文件才是运行时才会被调用的。
1.2 比如有下面的代码
头文件myMath.h
//myMath.h
namespace mycal
{
int add(int a, int b);
int sub(int a, int b);
}
#pragma once
实现代码 myMath.cpp
//myMath.cpp
#include "myMath.h"
int mycal::add(int x, int y)
{
return x + y;
}
int mycal::sub(int x, int y)
{
return x - y;
}
在Linux下,编译成动态链接库之后,会得到一个 xxx.so 文件,现在只要引入头文件,包含动态库路径,就可以正常使用了,但是上面的代码同样在Windows下面,使用VS2017编译成动态链接库之后,的确不会报错,只会的到一个 xxx.dll 文件,(不是还有一个对应的 xxx.lib文件吗,哪里去了呢?)
然后我们新疆一个项目,按照 “头文件路径配置——库文件路径配置”的方法,编写代码,我们也可以调用到add这两个函数,还有语法提示,因为语法提示其实来自于头文件,和库文件没关系,但是编译却不成功了,显示调用的add以及sub都是错误的,这是为什么呢?
这是因为前面说了Windows下面需要显式的告诉编译器,动态库中有哪一些函数是可以导出使用的,上面没有显示说明,即add和sub实际上是不可以使用的,故而会报错,怎么办呢?参见下面。
2.1 解决未生成lib文件以及函数没有显式导出的问题
两种方式来决定动态库中到底哪些函数是可以导出供外部直接使用的,以及与此同时生成与dll对应的 .lib 文件。
(1)MSVC编译器提供了关键字_declspec,来指定指定符号的导入导出,即_declspec属性关键字
_declspec(dllexport) 表示该符号是从本DLL导出的符号:这是在定义DLL中的函数等源代码是必须使用的,如果不显式的到处某一些符号,则使用动态链接库虽然没有语法上的错误,但是她无法编译,因为dll中的函数没有暴露出来,故而找不到。
_declspec(dllimport)表示该符号是从别的DLL中导入的:我们在使用动态链接库DLL中暴露出来的函数的时候,可以直接使用暴露的函数,也可以通过显示地导入函数,编译器会产生质量更好的代码。由于编译器确切地知道了一个函数是否在一个DLL中,它就可以产生更好的代码,不再需要间接的调用转接。如下所示:
__declspec(dllimport) void func1(void); //显示的导入dll中暴露出来的函数
int main(void)
{
func1();
}
后面会专门讲如何使用 __declspec 来创建动态库
(2)使用"xxx.def"文件来声明导入和导出符号
我们也可以不使用__declspec 来创建动态库,我们就按照正常的程序编写,如第一章节里面的 myMath.h和myMath.cpp里面的内容,然后显示的添加一个 xxx.def 文件,怎么添加呢?
如下:右击项目/添加/新建项,选择如下的文件:
默认是使用source.def 我们可以自定义名称。关于这个def文件是如何规定哪些内容是导出的,哪一些是不导出的,这里暂时先不说明了,可以参考下面的几篇文章:
https://blog.csdn.net/qwq1503/article/details/85696279
http://www.cnblogs.com/enterBeijingThreetimes/archive/2010/08/04/1792099.html
2.2 使用 __declspec 来创建动态库的完整过程
(1)新建一个空项目或者是使用DLL模板都可以
我个人习惯使用干净的空项目,没有那么多的附属文件,
添加头文件myMath.h
//myMath.h
namespace mycal
{
__declspec(dllexport) int add(int a, int b);
__declspec(dllexport) int sub(int a, int b);
}
#pragma once
注意:实际上就是在需要定义的函数前面添加一个 __declspec(dllexport) 是两个短下划线开头哦!来表示这两个函数是暴露出来可以供直接使用的,没有暴露的函数,在动态链接库中是没办法使用的。
(2)实现函数的内容
定义一个myMath.cpp文件
//myMath.cpp
#include "myMath.h"
int mycal::add(int x, int y)
{
return x + y;
}
int mycal::sub(int x, int y)
{
return x - y;
}
这个地方和我们平时的实现完全一样,实现的时候不再需要添加 __declspec(dllexport) 了。
(3)生成项目
比如我选择生成release 64位的结果,
生成之后得到如下的结果:
我们发现有一对配套的 xxx.dll 和 xxx.lib 文件,他们的大小不一样哦!
前面说了,window上生成的动态链接库的使用需要三个东西:头文件,dll文件,与dll对应的lib文件。
这里都具备了,现在新建一个项目,如何配置呢?
三步走配置
(1)第一步:配置包含路径——即头文件所在的路径
(2)第二步:配置库路径——即lib所在的路径
(3)第三步:添加链接,——将上面得到的calculate.lib添加到链接器
这里不再详细展开了
代码如下:
#include
#include
int main()
{
int x = 100;
int y = 20;
int a,b;
a = mycal::add(x, y);
b = mycal::sub(x, y);
printf("%d %dn", a, b);
getchar();
return 0;
}
现在生成,生成没有错误,但是运行依然会报错,因为运行的时候需动态链接库,所以还需要配置动态链接库
一共有三种方式,本文采用最简单的方式,直接将动态链接库和可执行文件拷贝到一起即可。
然后运行上面的程序,得到结果如下:
120 80
上面就是整个动态链接库的创建以及使用的过程。
这个工具不需要自己安装,我们通过安装VS,可以直接使用,如下打开VS自带的命令行工具,如下:
这个工具有什么作用呢?简而言之,它可以查看一个 lib文件 dll文件提供了哪一些函数,暴露出来可供使用的,它还可以查看 exe 文件包含了哪一些静态库和动态库。
查看一下它的帮助信息:
F:low_light_video_enhancementSTMKF_TESTstmkf_openmp_vec3b_2017_sourceYUVx64Release>dumpbin
Microsoft (R) COFF/PE Dumper Version 14.16.27034.0
Copyright (C) Microsoft Corporation. All rights reserved.
用法: DUMPBIN [选项] [文件]
选项:
/ALL
/ARCHIVEMEMBERS
/CLRHEADER
/DEPENDENTS
/DIRECTIVES
/DISASM[:{BYTES|NOBYTES}]
/ERRORREPORT:{NONE|PROMPT|QUEUE|SEND}
/EXPORTS
/FPO
/HEADERS
/IMPORTS[:文件名]
/LINENUMBERS
/LINKERMEMBER[:{1|2}]
/LOADCONFIG
/NOLOGO
/OUT:filename
/PDATA
/PDBPATH[:VERBOSE]
/RANGE:vaMin[,vaMax]
/RAWDATA[:{NONE|1|2|4|8}[,#]]
/RELOCATIONS
/SECTION:名称
/SUMMARY
/SYMBOLS
/TLS
(按回车键继续)
/UNWINDINFO
如何使用呢?举几个简单的例子,以本文所创建的动态库和可执行程序作为演示:
(1)查看calculate.dll 暴露出来了哪一些函数可以使用——dumpbin -exports xxxx.dll(xxxx.lib文件也一样的)
F:OpenCV4.1.1_Test2020_01_10calcuDllcalculatex64Release>dumpbin -exports calculate.dll
Microsoft (R) COFF/PE Dumper Version 14.16.27034.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file calculate.dll
File Type: DLL
Section contains the following exports for calculate.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
*************************************************************************
1 0 00001000 ?add@mycal@@YAHHH@Z = ?add@mycal@@YAHHH@Z (int __cdecl mycal::add(int,int))
2 1 00001010 ?sub@mycal@@YAHHH@Z = ?sub@mycal@@YAHHH@Z (int __cdecl mycal::sub(int,int))
****************************************************************************
Summary
1000 .data
1000 .pdata
1000 .rdata
1000 .reloc
1000 .rsrc
1000 .text
我们可以看到,add以及sub是暴露出来了的,上面的星号注释是我自己画的。
(2)查看可执行文件使用了哪一些库——dumpbin -imports xxx.exe
F:OpenCV4.1.1_Test2020_01_10useDlluseCalculateDllx64Release>dumpbin -imports useCalculateDll.exe
Microsoft (R) COFF/PE Dumper Version 14.16.27034.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file useCalculateDll.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
calculate.dll //这是我自己创建的
140002190 Import Address Table
1400029D8 Import Name Table
0 time date stamp
0 Index of first forwarder reference
1 ?sub@mycal@@YAHHH@Z
0 ?add@mycal@@YAHHH@Z
VCRUNTIME140.dll
140002080 Import Address Table
1400028C8 Import Name Table
0 time date stamp
0 Index of first forwarder reference
8 __C_specific_handler
3E memset
api-ms-win-crt-stdio-l1-1-0.dll
140002160 Import Address Table
1400029A8 Import Name Table
0 time date stamp
0 Index of first forwarder reference
0 __acrt_iob_func
1 __p__commode
54 _set_fmode
8C getchar
3 __stdio_common_vfprintf
api-ms-win-crt-runtime-l1-1-0.dll
1400020C8 Import Address Table
140002910 Import Name Table
0 time date stamp
0 Index of first forwarder reference
5 __p___argv
33 _initialize_narrow_environment
4 __p___argc
3D _register_thread_local_exe_atexit_callback
1E _crt_atexit
67 terminate
18 _configure_narrow_argv
28 _get_initial_narrow_environment
23 _exit
16 _cexit
34 _initialize_onexit_table
42 _set_app_type
40 _seh_filter_exe
36 _initterm
55 exit
3C _register_onexit_function
37 _initterm_e
15 _c_exit
api-ms-win-crt-math-l1-1-0.dll
1400020B8 Import Address Table
140002900 Import Name Table
0 time date stamp
0 Index of first forwarder reference
9 __setusermatherr
api-ms-win-crt-locale-l1-1-0.dll
1400020A8 Import Address Table
1400028F0 Import Name Table
0 time date stamp
0 Index of first forwarder reference
8 _configthreadlocale
api-ms-win-crt-heap-l1-1-0.dll
140002098 Import Address Table
1400028E0 Import Name Table
0 time date stamp
0 Index of first forwarder reference
16 _set_new_mode
KERNEL32.dll
140002000 Import Address Table
140002848 Import Name Table
0 time date stamp
0 Index of first forwarder reference
59A TerminateProcess
27E GetModuleHandleW
21D GetCurrentProcess
57B SetUnhandledExceptionFilter
5BC UnhandledExceptionFilter
4E1 RtlVirtualUnwind
382 IsDebuggerPresent
36C InitializeSListHead
2F0 GetSystemTimeAsFileTime
4DA RtlLookupFunctionEntry
4D3 RtlCaptureContext
389 IsProcessorFeaturePresent
450 QueryPerformanceCounter
21E GetCurrentProcessId
222 GetCurrentThreadId
Summary
1000 .data
1000 .pdata
1000 .rdata
1000 .reloc
1000 .rsrc
1000 .text
总结:
查看导入:dumpbin -exports 文件名称
查看导出:dumpbin -imports 文件名称
静态链接库的创建以及使用很简单,创建的时候不需要使用 __declspec ,也不需要使用 xxx.def文件,生成之后就是一个单纯的 xxx.lib 静态库文件,使用的时候,遵循“三步走配置策略”即可,此处不再展开。