C++中的库文件导入与导出
前言
C++的库文件分为两种:lib文件和dll文件,前者是静态的,会在build时就被打包到exe内,单独的一个exe文件就可以运行,而后者是动态的,不会被打包到exe内,除了exe,还需要对应的dll文件一起才可以运行。
C++的库文件分为两个部分,头文件和对应的cpp库文件,这也意味着我们在使用库文件时,需要用到这两个地方的路径。
举一个例子,创建一个空项目,创建Dependencies目录,里面存放网上下载的OpenGL的glfw内容,如下图所示:
include里存放了所有的头文件,lib-vc2017是用VS2017编译好的库文件的内容,里面有三个文件,其中glfw3.lib是预先编译好的静态库文件,glfw3.dll就是动态库文件,而glfw3dll.lib则是用来记录glfw3.dll里面的函数、symbol等内容的位置的,通过这个lib,Linker可以在dll中找到我们需要的函数,可用来加快程序运行效率,如果没有glfw3dll.lib文件程序也是可以执行的,只不过运行时计算机就会前往glfw3.dll里面去寻找我们需要的函数:
关于库文件在VS中的配置
项目属性中添加头文件
在项目属性的C+±>General里,如下图所示:
值得注意的是,在VC++ Directories下,也有一栏叫做Include Directories,这里设置的是整个VS2017的include文件目录(早期的VS把它放在了tools->options里面),比如说iostream这种的头文件,这里不要设置错了。
添加lib文件
在添加完头文件后,程序就可以编译了,但是运行确会报Link Error,对于静态库文件,需要在项目属性中设置lib文件的路径 ,既然是在Link阶段干的事情,自然是在Link界面里进行设置,如下图所示:
具体有两种方法:
第一种是简单的输入lib文件的路径,也就是在Input里面输入对应文件的路径,让Linker把这个文件Link进来,如下图所示:
点确定后可以成功Build项目,如下图所示:
可以看到这么长一串的路径在这里不太美观,因为其他的输入好像都是简单的lib文件名,如下图所示:
所以这里还有另外的方法,就是把lib文件的路径加入到Linker属性界面中,如下图所示,这样就可以在Input里面只输入glfw3.lib就可以了。
添加dll文件(通过lib定位)
前面演示了添加lib文件到项目中,这里演示添加dll文件,很多项目里dll和lib库的头文件是不一样的,但这里用到的glfw库的头文件是一样的,所以C++配置里头文件的目录不需要更改。
与前面类似,不过在Input里面不用glfw3.lib而是改用glfw3dll.lib,(glfw3dll.lib是在生成glfw3.dll时同时生成的lib文件,记载了dll里面各种符号和函数的位置)然后再把对应的glfw3.dll放到Debug下的HellowDLL的exe所在的目录下,因为exe默认默认调用DLL的路径就是exe所在的目录,这个路径也是可以改的,具体怎么改我还不太清楚。
也就是说,正常我们生成一个dll文件,其实还是需要对应的lib和dll文件,这样在使用该项目的属性中,需要在Input里面输入对应lib文件的路径和文件名,再把dll放到exe的同名目录下即可,当然,如果两个项目在同一个Solution下,我们可以直接使用项目引用(具体怎么操作见下一条)
添加dll文件(不通过lib定位)
具体使用一个裸的dll文件,需要用到显示链接,用C++的API去加载对应的DLL,文章最后面有提到这个事情
一个Solution下的一个项目生成lib以供另外一个项目使用
按照前面讲过的,设置项目A的生成属性为.lib文件,然后在项目B中添加A项目的头文件路径,再在项目B中的Linker界面输入项目A生成的Lib文件的目录和加入文件名,这样可以实现,但是Visual Studio提供了另外一种便捷的方法,就是项目引用,可以直接实现项目B使用项目A的lib或dll文件,这样我就不用辛苦的去手动设置路径了,比如说RecastDemo里面,所有实现的功能都是以lib格式生成的,最后演示的Demo就引用了所有这些项目,如下图所示:
组件都是用lib格式生成的
而演示Demo和测试项目引用了所有的lib组件,当Rebuild RecastDemo时,所有引用的项目都会被先Rebuild:
关于项目引用
VS执行项目引用的时候,实际上有一行对应的命令行操作,比如我这里有一个项目Hazel和项目Sandbox,Sandbox引用了Hazel,那么在Sandbox项目属性->linker->Command Line里,可以看到Link阶段Link了Hazel生成的dll对应的lib文件,我们就不需要手动再去Link下面添加对应的lib文件了,如下图所示:
C++语言生成dll的方法
前面总结的基本都是项目设置问题,具体C++怎么写代码生成dll接口呢?
正常对于一个想要做成dll的函数,在Windows平台可以在函数声明前面加上__declspec()关键字,如下所示:
namespace myDllNamespace
{
// exportDLL.h
__declspec(dllexport) void myFunction(); // 导出函数到dll中
}
注意这里把函数放到了自己定义的namespace里,不然别人导入dll后,myFunction就是一个全局函数,这很蠢,老式的C语言选择了复杂命名这样的方式,像glfw库那样,所有该dll下的函数都以glfw开头,叫做glfwInit()、glfwWindow()这种API,不过不推荐这么写
然后在导入该dll的工程中,我们也需要定义一个函数,表明这个函数是我们从dll导入的
// import.h
__declspec(dllimport) void myFunction(); // 导入dll中的函数
这也是为什么正常的dll导出头文件和dll导入的头文件是不一样的,相同的函数前面的前缀不同。
但通过宏,可以把两个头文件合并为一个,比如glfw的头文件却只用了一个头文件,如下所示,当使用不同的宏的时候,会随之导出dll、导入dll等操作:
/* GLFWAPI is used to declare public API functions for export
* from the DLL / shared library / dynamic library.
*/
#if defined(_WIN32) && defined(_GLFW_BUILD_DLL) //Build DLL时导出
/* We are building GLFW as a Win32 DLL */
#define GLFWAPI __declspec(dllexport)
#elif defined(_WIN32) && defined(GLFW_DLL) //使用DLL时导入
/* We are calling GLFW as a Win32 DLL */
#define GLFWAPI __declspec(dllimport)
#elif defined(__GNUC__) && defined(_GLFW_BUILD_DLL) //Linux平台使用dll
/* We are building GLFW as a shared / dynamic library */
#define GLFWAPI __attribute__((visibility("default")))
#else
/* We are building or calling GLFW as a static library */
#define GLFWAPI //其他状态就啥也不用,因为static library已经嵌入到exe里了
#endif
理论上这样就可以了,但是我实际操作还是遇到了相关代码看不懂,这里举几个例子:
** extern “C”**
工作里我看到有这么定义导出dll头文件的
#ifdef _GNUC_
#include<jni.h>
#define EXPORT_API JNIEXPORT
#else
#define EXPORT_API __declspec(dllexport)
#endif
extern "C"
{
EXPORT_API bool myFunction(){return true;} // 实际上定义是在cpp文件完成的
}
这里就涉及到了C语言与C++语言的区别,C语言不支持函数重载,C++语言支持函数重载,C++里通过Compiler用改变(mangle)函数名的方式来实现函数重载,通过在函数前面加上extern "C"限定符,能够规定Linker按照C语言的方式进行Link,能让C++编译器在编译该函数时不改变函数名。
举个例子,下列代码的func1没有问题,func2编译就会报错,因为C语言的Linkage方法里不支持函数重载:
什么时候使用extern “C”
当在C++中使用C语言的模块的时候,需要用extern "C"把对应的文件声明给括起来,表面这是用C语言写的,应该用C对应的Linkage的方式去Link
我们再回到之前的问题,为什么导出dll之前要加上extern"C"这一行代码:
extern "C"
{
EXPORT_API bool myFunction(){return true;}
有两种可能,第一,函数里面使用的c语言的函数,第二,为了方便查看函数名。
如果用C的Linkage规范,编译后的函数名更容易辨认,举个例子,有一个函数void WINAPI SampleFunc(void)
,C++规范编译后的名字是SampleFunc@@YGXXZ
,而C编译后的名字是_SampleFunc@0
,0代表没有参数(void),明显比后者直观很多。
隐式链接
隐式链接其实前面已经提到过了,通过项目属性里配置路径,可以完成隐式链接DLL,隐式链接需要三个内容:包含DLL函数声明的头文件、索引DLL相关函数位置的lib文件和代表DLL本身的.dll文件。
使用隐式链接的dll时,如果生成的项目与使用的项目在同一个Solution下,可以直接设置项目引用使用,如果是只有dll对应的.h、.lib和.dll文件,则需要在使用的工程中明确路径,既可以在项目属性中设置,也可以在脚本中表示引入了该dll,只不过这种情况下,需要把对应的lib文件和dll文件放在exe所在的目录下,如下所示:
//testDLL.h
#pragma comment(lib,"MyDll.lib") //会检索exe所在的目录
extern "C"_declspec(dllimport) int myFunc(); // 引入dll对应的函数
//TestDll.cpp
#include
#include"Dlltest.h"
void main()
{
// 调用dll
}
显示链接
显示链接支持在程序运行过程中动态加载和卸载dll,不过实现显式链接要麻烦一些。在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。具体操作可以参考显示链接
附上讲C++的dll底层原理的视频链接,需要科学上网才能浏览:
https://www.youtube.com/watch?v=JPQWQfDhICA