我们在编写动态库时,经常会用到类似extern “C”, __declspec(dllexport)的修饰符,以及有时候还会有def文件,__declspec(dllexport)和def文件是生成导出函数的两种方式,选用不同的导出方式对dll中的导出函数有什么影响,以及extern “C”的使用与否有什么区别,本文对上述限定符分别进行验证。
使用vs2017生成一个默认的dll工程,新建一个.h文件,加入以下代码:
bool __stdcall Test(int a, int b)
{
std::cout << __FUNCTION__ << std::endl;
return true;
}
编译该dll工程,生成了Dll1.dll,使用Dependency Walker打开会发现该dll没有导出函数:
一,__declspec(dllexport)
给Test函数加上__declspec(dllexport)修饰符,那么它就是一个导出函数了,编译dll工程,使用Dependency Walker打开Dll1.dll,此时有一个导出函数,但是函数名却不是代码中的Test,而是一个加了很多字符的新函数:?Test@@YG_NHH@Z:
这是由于我们的导出函数是__stdcall的调用约定,?表示一个函数的开始,函数名后面以"@@YG"标识参数表的开始,后跟参数表,比如H表示一个int类型,具体的细节可以去查阅资料,本文重点不在这。
我们再给导出函数加上一个extern “C”的限定,打开编译后的Dll1.dll:
此时的导出函数为_Test@8,如果大家熟悉c语言的dll导出函数命名规则的话,就知道这是c语言的命名规范,@8表示参数占8个字节。也就是说extern "C"会以c命名规范来修改我们的导出函数名(准确来说,extern “C” 是告诉编译器,让它按C的方式编译),它只能用于导出全局函数这种情况 而不能导出一个类的成员函数。
二,.DEF模块定义文件
去掉Test函数前的__declspec(dllexport)修饰符,新建一个Dll1.def文件
在def文件中加入以下内容:
LIBRARY
EXPORTS
; Explicit exports can go here
Test
其中Test就是我们的导出函数,编译工程,打开生成的Dll1.dll:
终于看到了熟悉的Test函数了,先不要激动,我们再来给Test加上extern “C”试试,打开编译后的Dll1.dll,可以发现使用def文件声明导出函数生成的dll中导出函数符号与代码中函数名完全一致,且加不加extern “C”修饰都是一样的。
三,__declspec(dllexport)和def文件的区别
动态库的加载使用有两种方式:显示调用和隐式调用。显示调用指的是使用LoadLibrary将dll加载到我们进程空间内,然后再调用GetProcAddress获取指定名称的导出函数的地址,转换为我们声明的对应参数和返回值的指针,调用这个指针,则完成dll中导出函数的使用;隐式调用指的是依赖于.h文件,和.lib文件,此处的.lib中保存的是dll的导出函数符号表,并不保存具体的代码实现,编译时编译器会根据lib自动找到对应导出函数的声明,完成编译,然后在运行时,再动态加载lib对应的dll文件,很明显,lib中保存了对应dll的名称,用notepad++打开Dll1.lib可以验证:
1,显式调用
1,def方式
新建一个win32控制台工程,测试代码如下:
#include <iostream>
#include <windows.h>
typedef bool(__stdcall *Fun)(int a, int b);
int main()
{
HMODULE hLib = LoadLibraryA("Dll1.dll");
if (nullptr == hLib)
{
std::cout << "LoadLibraryA fail, error:" << GetLastError() << std::endl;
return 0;
}
Fun fun = (Fun)GetProcAddress(hLib, "Test");
if (nullptr == fun)
{
std::cout << "GetProcAddress fail, error:" << GetLastError() << std::endl;
return 0;
}
fun(1, 2);
std::cout << "Hello World!\n";
getchar();
}
输出结果如下:
很明显,使用def方式的导出函数可以显示加载并调用。
2,__declspec(dllexport)方式
此处我们先不加extern “C”,使用同样的测试代码,输出如下:
报错127,找不到指定的函数。然后我们再加上extern “C”,依旧还是报错127,大家可以自己验证。
那么通过__declspec(dllexport)修饰过的导出函数是不能通过LoadLibrary来显示加载调用嘛,我们从两个方面再来验证这个问题:
第一种尝试:
修改GetProcAddress的导出函数名,根据上面我们使用Dependency Walker查看的__declspec(dllexport)的非extern “C”模式的导出函数名为:?Test@@YG_NHH@Z,我们将代码改为:
Fun fun = (Fun)GetProcAddress(hLib, "?Test@@YG_NHH@Z");
显示加载调用成功。
在这里插入图片描述
第二种尝试:
在dll工程中加入代码:
#pragma comment(linker, "/EXPORT:Test=?Test@@YG_NHH@Z")
这时我们再查看dll的导出函数,就会发现函数名字表中已经有了我们想要的Test。
但我们发现原来的那个 ?Test@@YG_NHH@Z 函数还在,这时就可以把 __declspec() 修饰去掉,只需要 pragma 指令即可。这样导出函数表中就只有一个Test函数了。
由此可见,在不修改原始dll代码的基础上,通过__declspec(dllexport)方式实现导出函数的dll库,也是可以通过LoadLibrary来显示加载调用的。
2,隐式调用
通过.h和.lib加载并使用dll中的导出函数,__declspec(dllexport)和def没有区别,此处就不再演示,大家可以自己验证。(前提是工程中配置了生成导出符号文件.lib)
四,结论
根据上述测试代码验证,我们可以得到以下结论:
1,dll要实现导出函数有两种方式:使用__declspec(dllexport)修饰导出函数,或者新增def文件加入导出函数名。
2,使用def文件生成的dll导出函数,加不加extern “C”都没区别,最终符号表中的符号就是代码中的函数名,可以直接用LoadLibrary来进行显示调用。
3,使用__declspec(dllexport)修饰的导出函数,不能直接使用LoadLibrary来显示调用,必须要结合lib来隐式调用,这是因为编译器帮我们进行了导出函数名转换这一步骤,我们也可以使用以下命令来进行手动转换(在dll中使用),此时可以不再需要__declspec(dllexport)。ps:可是去掉了又如何知道实际的导出函数名呢,这个用法比较尴尬 —_—
#pragma comment(linker, "/EXPORT:Test =?Test@@YG_NHH@Z ")
4,extern “C” 声明只对根据c++规则进行修改的导出函数才有用,并将该导出函数按照c的规则进行重命名,比如?Test@@YG_NHH@Z =》_Test@8。没有修改修改命名的导出函数,加不加extern “C” 没区别,比如def模块定义文件实现的导出函数。
5,如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数。
备注:如果大家感兴趣的话,可以自行验证一下c语言的dll导出函数。