1. 问题描述
分布式组件项目使用了Redis,在Windows平台使用QT+VS2010编译环境。但Redis客户端库hiredis在Windows平台只提供静态库,而且必须用VS2013以上的版本才能编译。由于VS2013要更新部分组件才能避免编译错误,最终以VS2015编译hiredis.lib静态库。这样就面临如下问题:
VS2010不支持完整的C++11特性,linux能直接使用std::thread的代码在Windows无法编译。
但使用该组件的应用程序在Windows系统以VS2010编译,不能直接用VS2015编译出的hiredis.lib继续编译应用程序。
因为某个ID变量没有初始化,在linux运行正常,但移植到windows就出现错误。原因是该变量在linux被缺省初始化为0,单在Windows是随机值。
从代码的质量考虑,不能依赖系统的缺省值,必须养成初始化所有变量的习惯。
代码中使用了C++11的thread,在linux和Windows的VS2015运行正常,但在VS2010编译出错。原因是VS2012之前的版本不支持C++11标准。为此不得不大量使用条件编译,改用CreateThread等函数。
github获取的源代码只能编译hiredis.lib,可移植性不理想。最初尝试过直接在VS2015将这个lib工程改为dll,但由此带来的错误极多,无法解决问题。
后来受到网上一篇关于在Windows如何使用hiredis.lib的文章启发,先在exe工程顺利使用hiredis.lib,再仿照这个exe工程创建一个dll工程,将hiredis封装到这个dll中,就命名为hiredis.dll。
原先调用hiredis.lib需要新建一个目录,存放被少量修改的部分开源代码。结果是应用程序不仅要依赖hiredis.lib,还要依赖win32_interop.lib,和hiredis.h之外的头文件win32fixes.h。而linux系统开发应用程序下则只依赖hiredi.so和hiredis.h。封装dll时,新建一个头文件hirediswin.h,不仅包含了原来的hiredis.h,而且直接复制了win32fixes.h中有效的代码,封装的结果是像linux一样,应用程序只需要加载1个hiredis.dll和1个头文件hirediswin.h。
hiredis.dll以hiredis.lib为输入,封装hiredis.lib的接口函数为导出函数,其命名肯定不能和hiredis.lib一致。例如redisConnect,是在dll内封装一个redisConnectWin函数,内部调用hiredis.lib的redisConnect作为导出函数。
但接下来面临的问是:应用程序跨平台调用Redis接口函数,不宜大量使用条件编译,在linux和Windows处理不同的函数名称。解决方法是:
在使用hiredis.dll的应用程序,使用如下条件编译和宏定义:
#ifdefWIN32
#defineredisConnectredisConnectWin
……
#pragmacomment(lib,"hiredis.lib")
#endif
把hiredis.dll导出函数在应用程序中,用宏声明为不带统一后缀的名称,这样就能在不同平台统一调用同名的API函数了。
上述宏定义推荐在应用程序使用,如果在hirediswin.h直接使用,会导致导出函数无限递归调用自己。可行的解决方案是:使用条件编译,和dll工程判别dllimport相同的条件下定义上述宏。即在hirediswin.h这样定义宏函数:
#ifdefHIREDISDLL
#defineMY_EXPORT__declspec(dllexport)
#else
#defineMY_EXPORT__declspec(dllimport)
#defineredisConnectredisConnectWin
……
#endif
其中,宏条件HIREDISDLL在dll工程的pro文件声明:DEFINES+= HIREDISDLL。
hiredis.dll在QT应用程序以预编译指令#pragmacomment(lib,"hiredis.lib")加载就能正常使用,在pro文件指定却不行。目前原因不详。
即使在VS2015封装了hiredis.dll,换台主机用VS2010编译器仍然不能正常编译;解决编译问题之后,还不能正常调试。QT总是提示CDB无法调试。
编译错误显示,使用hiredis.dll出现了链接错误,缺少msvcp140d.dll等库的支持。借助dependency工具查看hiredis.dll,可获取所有依赖库的名称。再使用everyone工具在可正常运行的主机查找msvcp140d等依赖库,逐一复制到目标机,可解决编译问题。
需要注意:
1) 因为环境不同,主机对库的依赖关系虽然一致,但查找路径一般不同。这就可能出现A主机正常,B主机又提示缺少新的依赖库的情况。例如api-ms-win-core-timezone-l1-1-0.dll就是在部署第二台主机报错提示缺少的库。
2) 可用主机上找到的同名库有可能存在不同的版本,仍以api-ms-win-core-timezone-l1-1-0.dll为例,用everyone找到了不同的版本,不确定用哪个,只能一个一个的去试,指导正常为止。
通过编译未必就可以正常调试运行。有一个典型的问题是这样的:hiredis.dll编译为64位,在64位QT环境加载使用。但该机同时还配置了32位QT,缺省的QTCore.dll指向32位版本,结果就是编译正常,但不能调试和运行。
和缺少依赖库的解决方法相同,将64位QTCore.dll复制到编译输出的路径之下。不过这个问题隐蔽性较强,只在调试和运行时出现。
以C/C++的视角比较二者的编译结果,最简单的概括是:静态库更接近于obj文件,只是还需要连接才能形成可执行或动态库文件;而动态库和exe属于统一级别,本质上都是可执行的二进制文件。
事实证明dll的可移植性远胜于lib。用VS2015编译的hiredi.lib不能VS2010编译可执行文件,即使加入了VC140库也不行。但重新封装的hiredis.dll同样由VS2015产生,就能用VS2010编译器编译产生可执行文件了。
这也提供了一个普遍思路:使用动态库能提高模块的可移植性,这也是dll大行其道的原因。
今后,对因为应用程序所在环境的版本原因导致静态库无法编译使用的问题,可遵循以下步骤解决:
1) 在可用环境使用静态库生成exe工程,验证其基本功能;
2) 参照exe工程,创建一个dll工程,封装原来的静态库,为导出函数加上统一的前缀或后缀重新命名,其内部实现就是直接调用静态库的函数;
3) 整合dll对应的头文件,用条件编译定义宏命名,保持调用方能用静态库的函数名称调用dll改名之后的导出函数。
前文提到的dependency和everyone工具分别在查找dll和exe文件的依赖库、查找文件时能解决很多问题,不可或缺。
可以肯定的是,在查找依赖库的过程中,只要能顺利找到各级依赖库,复制到目标程序所在的路径,就一定能够正常运行。但有时候会找到很多不同版本的同名库文件,有可能要逐一试用才行。