用静态链接Qt库的方式使用Qt(下)

上篇文章:用静态链接Qt库的方式使用Qt(上)

上篇文章讲了如何编译出静态库形式的Qt,这篇文章讲怎样把编译好的Qt静态库链接到程序上。这里主要说的是寻找引用Qt库正确途径的过程,嫌太长只想知道方法可以直接看结尾处的结论。

尝试把Qt静态库链接到程序上

既然已经把Qt静态库编译好了,那么就把它链接到我们的CMake项目吧!

Qt本身确实是支持用CMake构建的项目的,还提供了CMake项目使用Qt库可用的文档:https://doc.qt.io/qt-5/cmake-manual.html。自己的项目使用Qt动态库的时候,Qt提供支持的CMake脚本放在lib/cmake这个文件夹下。configure的时候把Qt5_DIR的值设成lib/cmake/Qt5,Qt提供的CMake脚本就能帮你完成后续的寻找Qt库依赖的工作。编译发布包的时候,只要再运行一下windeployqt.exe,这个程序就能把你的项目需要的所有Qt DLL复制到发布包所在的目录下。对应的CMake脚本语句写起来也非常简单,这里就不详细说了。

静态库形式的Qt库是不是也可以这样简单地就完事呢?来到静态库Qt的安装目录,切到lib目录下就能看到编译成静态库的Qt库,debug版和Release版的都有,debug版的库文件名都是以d结尾的。这个目录下面也有CMake子目录,里面存了和动态库Qt一致的CMake脚本,看起来链接静态库Qt只要照葫芦画瓢就可以。再切回自己的项目,把cmake缓存清一下,再把Qt5_DIR的值指向静态库Qt下同样的目录。CMake程序很顺利地把项目configure完了,VS解决方案也生成出来了。

那么下一步就是编译。生成程序的过程也是非常地顺利,直到构建过程到了链接生成二进制文件的这一步:

Qt5Core.lib(qcoreapplication_win.obj) : error LNK2001: 无法解析的外部符号 _VerQueryValueW@16
7>Qt5Widgets.lib(qwindowsxpstyle.obj) : error LNK2019: 无法解析的外部符号 __imp__OpenThemeData@8,该符号在函数 "public: static void * __cdecl QWindowsXPStylePrivate::createTheme(int,struct HWND__ *)" (?createTheme@QWindowsXPStylePrivate@@SAPAXHPAUHWND__@@@Z) 中被引用
7>Qt5Widgets.lib(qwindowsvistastyle.obj) : error LNK2001: 无法解析的外部符号 __imp__OpenThemeData@8
7>Qt5Widgets.lib(qwindowsxpstyle.obj) : error LNK2019: 无法解析的外部符号 __imp__CloseThemeData@4,该符号在函数 "public: void __thiscall QWindowsXPStylePrivate::cleanupHandleMap(void)" (?cleanupHandleMap@QWindowsXPStylePrivate@@QAEXXZ) 中被引用

......

7>Qt5Gui.lib(qpnghandler.obj) : error LNK2019: 无法解析的外部符号 _png_get_text,该符号在函数 "public: void __thiscall QPngHandlerPrivate::readPngTexts(struct png_info_def *)" (?readPngTexts@QPngHandlerPrivate@@QAEXPAUpng_info_def@@@Z) 中被引用
7>Qt5Gui.lib(qpnghandler.obj) : error LNK2019: 无法解析的外部符号 _png_set_text,该符号在函数 "void __cdecl set_text(class QImage const &,struct png_struct_def *,struct png_info_def *,class QString const &)" (?set_text@@YAXABVQImage@@PAUpng_struct_def@@PAUpng_info_def@@ABVQString@@@Z) 中被引用
7>Qt5Gui.lib(qpnghandler.obj) : error LNK2019: 无法解析的外部符号 _png_get_tRNS,该符号在函数 "void __cdecl setup_qt(class QImage &,struct png_struct_def *,struct png_info_def *,class QSize,bool *,float,float)" (?setup_qt@@YAXAAVQImage@@PAUpng_struct_def@@PAUpng_info_def@@VQSize@@PA_NMM@Z) 中被引用
7>Qt5Gui.lib(qpnghandler.obj) : error LNK2019: 无法解析的外部符号 _png_set_tRNS,该符号在函数 "public: bool __thiscall QPNGImageWriter::writeImage(class QImage const &,int,class QString const &,int,int)" (?writeImage@QPNGImageWriter@@QAE_NABVQImage@@HABVQString@@HH@Z) 中被引用
7>Qt5Gui.lib(qpnghandler.obj) : error LNK2019: 无法解析的外部符号 _png_set_option,该符号在函数 "public: bool __thiscall QPngHandlerPrivate::readPngHeader(void)" (?readPngHeader@QPngHandlerPrivate@@QAE_NXZ) 中被引用
...... fatal error LNK1120: 151 个无法解析的外部命令

卧槽,怎么突然多出来这么多无法解析的外部符号!

解决 “无法解析的外部符号”

仔细看这些无法找到实现的函数符号,可以看到其中包含的函数名有不同的特征:

名称类似__imp__function@16_func@8的函数

这类函数的函数名的特点大概有:函数名中的短语之间没有分隔;函数的前缀都包含下划线,后缀都带@字符加一串数字。前者是Win32 API提供的函数名典型的命名方法,几乎没有例外。后者表明在x86环境下,函数使用了__stdcall的调用约定(详情请去google找name mangling这个关键字)。所以这类函数基本上都是Win32 API提供的函数。用函数名做关键字,在google上找提供这个函数的Windows DLL,之后在target_link_libraries里加上对应的lib文件,这种类型的函数的链接问题就都能解决。

_pcre_hb_png等等开头的函数

这些都是Qt项目本身使用的第三方库提供的函数。Qt都使用了哪些第三方库,在这里可以找到:https://doc.qt.io/qt-5/configure-options.html#third-party-libraries。Qt为了方便开发,在项目源代码文件中包含了这些库的源码,编译的时候也默认编译使用自己项目里的第三方库源代码。那么问题来了,为什么动态库Qt肯定也用了这些第三方库,我在windeployqt.exe复制的DLL里面却找不到这些库的存在?既然用到了这些库为什么静态库Qt就不能把这些库生成的二进制代码直接包含进类似Qt5Gui.lib这样的静态库里面?

我的粗略的理解是这样的:Qt编译成DLL时,最终生成的是一个完整的PE可执行文件,PE文件本身就要求依赖的代码要么在PE导入表中声明,要么代码本身就存在PE文件内部,既然没在导入表中找到第三方库的DLL,那么这些第三方库在链接生成PE文件时就必然已经包含进DLL中。编译成静态库时,lib文件是一系列由源代码编译生成的中间文件组成的集合,自然没有PE文件的限制。而由于项目管理的限制,第三方库这部分代码又不能直接整合进类似Qt5Gui.lib的静态库里面,要这样做就得在每个用了这个第三方库的Qt项目的makefile里编译第三方库的源文件,这样链接的时候必然会出现无数函数符号冲突的错误。因此第三方库的代码没法包含进Qt静态库中。

因此必须得想办法把这些第三方库找回来。拿pcre库为例,在静态库Qt安装目录下找pcre的字眼,可以在lib目录下找到两个静态库qtpcre2.libqtpcre2d.lib,仔细看,其余的第三方库lib都能这样找到,例如qtlibpng.lib。那么就把这些第三方库也包含进里面,最后链接的第三方库的列表大概是这样的:

set(QT5_LIB_DIR "${Qt5_DIR}/../..")
set(Qt5_STATIC_LINK_LIBRARIES
  debug ${Qt5_LIB_DIR}/qtpcre2d.lib optimized ${Qt5_LIB_DIR}/qtpcre2.lib
  debug ${Qt5_LIB_DIR}/qtharfbuzzd.lib optimized ${Qt5_LIB_DIR}/qtharfbuzz.lib
  debug ${Qt5_LIB_DIR}/qtlibpngd.lib optimized ${Qt5_LIB_DIR}/qtlibpng.lib
  debug ${Qt5_LIB_DIR}/qtfreetyped.lib optimized ${Qt5_LIB_DIR}/qtfreetype.lib
)

解决Qt程序所需的插件依赖

解决完Windows API和Qt第三方库的链接问题后,再重新构建项目,这次构建程序居然成功了!既然构建成功了那就尝试运行下构建出来的程序,程序的主界面没有出来反而弹出了一个报错对话框,向你泼了一盆冷水:

对话框提示我们程序里缺了让Qt程序正常运行的插件:”windows”。这个”windows”插件是什么鬼?把项目调回到链接Qt 动态库的配置上,看下windeployqt.exe复制过来DLL,其中有个qwindows.dll,在platform目录下。这个DLL在Qt安装目录的位置是在plugins/platforms/qwindows.dll,它被放在了plugins的目录下,所以这个qwindows.dll应该就是那个名叫”windows”的Qt插件。

不过Qt的文档说了,链接Qt的静态库必然会牺牲掉Qt框架按需动态加载插件的特性,这时的程序必须在代码里声明自己要用到的所有Qt插件。声明插件的方式是在C++代码里加上类似Q_IMPORT_PLUGIN(pluginName)这样的语句。所以这个qwindows.dll对应的静态库是什么?在Qt静态库的安装目录下可以找到plugins/platforms/qwindows.lib这个文件,这个应该是对应的静态库文件。于是先尝试这样做:

  • 在C++代码里加上以下语句引入”windows”插件:
    #include <QtPlugin>
    
    #ifdef QT_STATIC
    Q_IMPORT_PLUGIN(windows)
    #endif
    
  • 在CMake脚本里加上以下语句链接qwindows.lib:
    set(Qt5_STATIC_PLUGIN_LIBRARIES
    debug ${Qt5_ROOT_DIR}/plugins/platforms/qwindowsd.lib optimized ${Qt5_ROOT_DIR}/plugins/platforms/qwindows.lib
    )

尝试编译,然后编译失败了:

error LNK2019: 无法解析的外部符号 "struct QStaticPlugin const __cdecl qt_static_plugin_windows(void)" (?qt_static_plugin_windows@@YA?BUQStaticPlugin@@XZ),该符号在函数 "void __cdecl `dynamic initializer for 'staticwindowsInstance''(void)" (??__EstaticwindowsInstance@@YAXXZ) 中被引用

所以该从哪里获得qwindows.dll插件在C++代码里的正确名称?这个问题百思不得其解,直到我打开了Qt库bin目录下的qtplugininfo.exe。原来这个程序就是用来把Qt插件的基本数据dump出来的。按照这个程序的帮助输入了下面的命令,终于知道了插件的名字原来叫QWindowsIntegrationPlugin

qtplugininfo C:\Qt\Qt5.9.8\5.9.8\msvc2015\plugins\platforms\qwindows.dll -p classname

把前面的”windows”改成QWindowsIntegrationPlugin,继续尝试编译,怎么还是报找不到外部符号的错?

1>qwindows.lib(qwindowsintegration.obj) : error LNK2019: 无法解析的外部符号 "public: __thiscall QWindowsFontDatabase::QWindowsFontDatabase(void)" (??0QWindowsFontDatabase@@QAE@XZ),该符号在函数 "public: virtual class QPlatformFontDatabase * __thiscall QWindowsIntegration::fontDatabase(void)const " (?fontDatabase@QWindowsIntegration@@UBEPAVQPlatformFontDatabase@@XZ) 中被引用
...
1>qwindows.lib(iaccessible2.obj) : error LNK2019: 无法解析的外部符号 "bool __cdecl QAccessibleBridgeUtils::performEffectiveAction(class QAccessibleInterface *,class QString const &)" (?performEffectiveAction@QAccessibleBridgeUtils@@YA_NPAVQAccessibleInterface@@ABVQString@@@Z),该符号在函数 "public: virtual long __stdcall QWindowsIA2Accessible::doAction(long)" (?doAction@QWindowsIA2Accessible@@UAGJJ@Z) 

这些名叫QWindowsFontDatabaseQAccessibleBridgeUtils等等的C++类又是怎么回事?肯定是又有一些额外生成的Qt内部的静态库依赖需要我们手动包含进程序里面。继续在Qt静态库目录里面找,发现了两个lib文件,名叫Qt5FontDatabaseSupport.libQt5AccessbilitySupport.lib。把这俩静态库也加到链接列表里面去,果然错误少了很多。lib目录下面还有别的这样结尾的库文件,索性把它们都加到target_link_libraries列表里面,反正没有引用的话最终程序也不会包含多出来的静态库。最终的CMake脚本是这样的:

  file(GLOB 
    ${QT5_LIB_DIR}/*Supportd.lib
  )
  file(GLOB Qt5_STATIC_LIBRARIES_SUPPORT_RELEASE
    ${QT5_LIB_DIR}/*Support.lib
  )
  foreach(support_lib_debug IN LISTS Qt5_STATIC_LIBRARIES_SUPPORT_DEBUG)
    list(APPEND Qt5_STATIC_LINK_LIBRARIES
    debug ${support_lib_debug}
  )
  endforeach()

  foreach(support_lib_release IN LISTS Qt5_STATIC_LIBRARIES_SUPPORT_RELEASE)
    list(APPEND Qt5_STATIC_LINK_LIBRARIES
      optimized ${support_lib_release}
    )
  endforeach()

经过这一番折腾之后,我们的程序主界面终于出来了,可是还有一些不对劲的地方,比如窗口的图标不好用。再重新回顾下动态链接Qt时windeployqt.exe复制的dll,里面还有个DLL文件叫qico.dll。如法炮制把qico.dll对应的插件静态链接到程序上,再以此类推把其余缺少的插件补上。
最后,我们使用Qt静态库的程序终于能像使用Qt动态库的程序那样能正常运行了。因为Qt库的代码与主程序整合到了一起,所以主程序的大小会增大到15MB以上。不过与NW.js或者Electron比起来,这占的空间简直是太小了,而且整个程序全都是编译成汇编的原生代码,在性能上还是有天然优势的嘿嘿嘿……

总结

总结下静态链接Qt库遇到的问题和注意事项。

要把Qt库静态链接到自己的项目上,除了照常链接Qt库之外,还需要把Qt依赖的Windows库、Qt依赖的第三方库、程序依赖的Qt插件都嵌进程序里:
– Windows库很好解决,碰到“无法解析的外部符号”错误,只要去查google和msdn就能知道要需要链接上哪个Windows库。不过Qt库的依赖未知,所以只能碰到一个解决一个。
– Qt依赖的第三方库——harfbuzz、libpng等等,存储的位置也在lib目录下,名字的特点是前面都带小写的qt
– 要保证程序依赖的Qt插件都能正常运行,需要把Qt主项目包含的支持库也链接到主程序上,这些库名字的特点是文件名结尾都是*Support.lib
– 要找全项目所需的Qt插件,动态链接Qt库的配置下,windeployqt.exe复制过来的DLL列表是很重要的依据。要查询插件DLL的文件名,有一种可靠的方式是使用Qt带的qtplugininfo工具。要静态链接某个插件,把该工具获取到的插件名在C++代码里用Q_IMPORT_PLUGIN声明一下,之后把Qt插件对应的静态库版本链接到程序上,这个插件就能正确加载了。

最后,项目里用到Qt库还是直接引用Qt DLL省心省力,windeployqt提供一站式解决方案。正确静态链接Qt库要花的工夫比无脑复制DLL多得多。除非为了部署方便,想把程序做成单个可执行文件,否则在项目里引用Qt DLL无论如何都是首选,这样做还没有法律风险之虞。