针对动态加载方式的C/C++动态链接库编写

0、前言
笔者为客户提供C/C++动态链接库调用WEBSOCKET功能时,最初错误地认定客户采用静态加载的方式使用DLL库,导致使用其它编程语言的客户无法使用。考虑到为客户服务常常要跨语言和跨IDE,最好的DLL库的使用方式是动态调用,并且要减少DLL库的依赖库,避免对Windows下VS自带库的调用。本文针对动态调用提出一起DLL编写注意事项。

1、静态调用与动态调用
1.1 静态调用
使用这种方式调用DLL库的步骤(摘自网上url)为,youApp是你DLL的工程名,需要dll\lib\h头文件:
①把你的youApp.DLL拷到你目标工程(需调用youApp.DLL的工程)的Debug目录下;
②把你的youApp.lib拷到你目标工程(需调用youApp.DLL的工程)目录下;
③把你的youApp.h(包含输出函数的定义)拷到你目标工程(需调用youApp.DLL的工程)目录下;
④打开你的目标工程选中工程,选择Visual C++的Project主菜单的Settings菜单;
⑤执行第4步后,VC将会弹出一个对话框,在对话框的多页显示控件中选择Link页。然后在Object/library modules输入框中输入:youApp.lib
⑥选择你的目标工程Head Files加入:youApp.h文件;
⑦最后在你目标工程(*.cpp,需要调用DLL中的函数)中包含你的:#include "youApp.h"
此种调用方式的优点是:DLL的函数名是通过.h文件和lib文件寻找到,实际的接口名可能和函数名不一致,但是不会导致无找不到入口的情况。它的缺点就是:非C/C++语言无法加载.h头文件,跨语言时会遇到各种问题。
1.2 动态调用
动态调用的方法,先LoadLibrary,再GetProcAddress(即找到DLL中函数的地址),不用后FreeLibrary。具体示例代码(摘自网上)如下:

{
    HINSTANCE hDllInst = LoadLibrary("youApp.DLL");
    if(hDllInst){
        typedef DWORD (WINAPI *MYFUNC)(DWORD,DWORD);
        MYFUNC youFuntionNameAlias = NULL; // youFuntionNameAlias 函数别名
        youFuntionNameAlias = (MYFUNC)GetProcAddress(hDllInst,"youFuntionName");
        // youFuntionName 在DLL中声明的函数名
        if(youFuntionNameAlias){
            youFuntionNameAlias(param1,param2);
        }
    FreeLibrary(hDllInst);
    }
}

动态调用优点在于:不需要依赖.h和lib文件,更加便捷。缺点在于:生成DLL库接口名需要和函数名一致,否则无法找到函数的入口点

2、DLL库接口名和函数名的关系
2.1 接口名和函数名分析
如果接口名和函数名不一致找不到dll中的函数,出现“无法定位程序输入点”的问题,如下图所示。

对于DLL库,查看它的接口名称可以使用Depends工具,如需要可以联系我。使用depends可以看到如下示例,在这个例子中Function的名称即接口名称,它和函数名称是一致,这样可以正常调用DLL。

而函数名称与接口名不一致情况笔记忘了保存,举例说明,sendMessage的Function name为_sendMessage@12,其中符号"_"和"@12"导致接口名和函数名不一致。这种情况使用在GetProcAddress函数中将函数名作为参数是无法找到函数入口点的。所以使用动态调用时,最好确保函数名和接口名的一致。
2.2 如何确保函数名和接口名一致
C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样,因此改编后的名字也是不同的(一般涉及到C++ 中的重载等)。一般而言,生成dll有两种方法,一是使用def文件,二是在函数定义前加_declspec(dllexport)。如果要导出C++文件中的函数,并且不让编译器改动函数名,用def文件导出函数并且在函数名前加extern "C"。
def的定义示例如下:

LIBRARY PrinterManager
EXPORTS
	initPrinterManager
	setRecvDataCallback
	sendMessage
	closePrinterManager

关于DLL导出名如下图(来源于网上文章url),如果采用extern "C"和def则函数名和接口名一致,C++采用_declspec(dllexport)即函数名和接口名一致。其它情况函数名会被编译器改编。


3、参数入栈顺序(__stdcall和__cdecl),参考网上文章url
接口函数最好不用使用std中的容器,如string和vector等。这是因为计算机给这个函数传递参数,传递参数的工作必须由函数调用者和函数本身来协调,即函数传递参数的方式需要一致才能正确传递参数。计算机使用栈来支持参数传递,函数调用时,调用者依次把参数压栈,然后调用函数,函数被调用以后,在堆栈中取得数据,并进行计算。函数计算结束以后,或者调用者、或者函数本身修改堆栈,使堆栈恢复原装。
问题出现了,当参数个数多于一个时(数组为多个参数),按照什么顺序把参数压入堆栈,函数调用后,由谁来把堆栈恢复原装 在高级语言中,通过函数调用约定来说明这两个问题。常见的调用约定有:stdcall、cdecl、fastcall等,本文仅介绍stdcall和cdecl。
3.1 __stdcall
声明方法:int __stdcall function(int a,int b)
__stdcall的调用约定意味着:1)参数从右向左压入堆栈,2)函数自身修改堆栈 3)函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸。
在DLL的生成代码中和使用代码中都需要声明__stdcall。跨语言推荐使用该方法
3.2 __cdecl
cdecl调用约定又称为C调用约定,是C语言缺省的调用约定,它的定义语法是:
    int function (int a ,int b) //不加修饰就是C调用约定
    int __cdecl function(int a,int b)//明确指出C调用约定
参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,故跨语言不应该使用该方法。

4、减少DLL库的依赖库,避免对Windows下VS自带库的调用
如果对外提供的DLL库使用VS自带库,那么其它语言很有可能就因为没有VS自带库而无法运行。根据笔者的经验,以下两个步骤一定要使用来将减少DLL库的依赖
4.1 步骤一:发布注意使用release方式而不能是debug
选择方法如下图所示:

4.2 步骤二:将项目中的MD改为MT
/MT是 "multithread, static version ” 意思是多线程静态的版本,定义了它后,编译器把LIBCMT.lib 安置到OBJ文件中,让链接器使用LIBCMT.lib 处理外部符号。
/MD是 "multithread- and DLL-specific version” ,意思是多线程DLL版本,定义了它后,编译器把MSVCRT.lib 安置到OBJ文件中,它连接到DLL的方式是静态链接,实际上工作的库是MSVCR80.DLL。
故采用MD的方式会使用额外的库,如vcruntime140.dll或msvcp140.dll,而这两库在ISV处很可能是没有的。
修改方法:
①打开项目的“属性页”对话框;
②展开“C/C++”文件夹;
③选择“代码生成”属性页;
④修改“运行库”属性。
如下图所示:


5、总结
C/C++动态链接库的使用充满了复杂,很多新接触者会无缘无故地陷入各种问题当中,故写本文给看到的人以减少他们的弯路。特别感谢阿里实习同事的帮助,他们在我确决问题中给了我不少的指点。

posted on 2016-08-23 17:48  ycloneal  阅读(14058)  评论(0编辑  收藏  举报