对于CMake系列知识点来说,第三方库的使用是一个绕不开的知识点,废话不多说,直接开讲。
在此之前还是必须先简单了解一下基本知识点:库可以分为静态库与动态库。
- 静态库:静态库在程序编译链接时,将库中用到的代码直接链接(或者说复制)到最终的可执行文件中。这意味着,一旦你的程序链接了静态库,那么即使在没有库文件的系统上,你的程序也能正常运行,因为它已经包含了所有需要的代码。然而,这也会导致你的可执行文件比链接动态库的版本大,因为它包含了所有的库代码。
通常的文件后缀:
Windows | Linux和Android | macOS和iOS |
---|---|---|
*.lib | *.a | *.a或*.framework |
- 动态库:与静态库不同,动态库在程序编译链接时,并不会被复制到最终的可执行文件中。相反,当程序运行时,它会从系统中加载动态库。这意味着,如果你的程序链接了动态库,那么在运行程序的系统上,需要有一个相应的动态库文件。动态库的优点是它可以被多个程序共享,这可以减少磁盘空间和内存的使用。此外,如果动态库更新了,程序可以在不重新编译的情况下使用新版本的库。
通常的文件后缀:
Windows | Linux和Android | macOS和iOS |
---|---|---|
*.dll | *.so | *.dylib或*.framework |
【注:猴急的人可以跳过这一部分,太长了。】在了解了什么是静态库什么是动态库之后,我们还需要了解另外一个东西:C++运行时库,因为我本身是做Android应用层开发的,所以这里也可以提一提可能大家看到过的一个东西:libc++_shared.so
。给大家上个图方便回忆一下,比如微信的apk里就有它:
这个库就是C++在Android上包含了C++ 标准库的实现(包括IO操作、字符串处理、数学计算等功能),以及一些底层的服务,如内存管理和线程处理。此外,C++运行时库还包含了运行C++程序所必需的启动和退出代码。例如,在程序启动时,C++运行时库会负责初始化全局和静态变量,在程序退出时,C++运行时库会负责清理资源。。在Android里,它是由NDK提供的,具体位置如下图:
当然,它还有另一种形式—静态库,位置也来张图:
至于用哪个可以根据需求来调整。
可能看完前面的介绍现在还是有一些人不能理解这个运行时库到底是什么?那我可以提一下Java Runtime Environment(JRE),你现在大概知道它是什么了吧?你没看错,C++也是需要运行时库的,只是说这个运行时库不会很大,有的是直接静态导入到了exe里,所以你可能找不到它。
所以,在链接第三方库时(在这里系统库也算是第三方库),可以用静态或动态的方式来链接这个C++运行时库。比如:你在Windows上用MSVC来编译链接你的库时,你在cmake里可以设置它是用静态还是动态的方式来链接:
#设置为静态链接运行时库
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
或者
#设置为动态链接运行时库
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL$<$<CONFIG:Debug>:Debug>")
其实,MSVC是通过这几个命令行选项 /MT
,/MTd
,/MD
和 /MDd
来控制的,在cmake的这些设置都是一一对应而已:
- 静态多线程:
MultiThreaded
对应于/MT
- 静态多线程(带调试):
MultiThreadedDebug
对应于/MTd
- 动态多线程:
MultiThreadedDLL
对应于/MD
- 动态多线程(带调试):
MultiThreadedDebugDLL
对应于/MDd
当然,你什么也不设置,MSVC的默认行为是/MD
或/MDd
,即MultiThreadedDLL
或MultiThreadedDebugDLL
。
额。。。如果是其他编译器呢?
- 我们先来看静态链接运行时库:
GCC(包括MinGW):
target_link_options(myprogram PRIVATE -static-libstdc++ -static-libgcc)
#或者要想全部使用静态链接的话(但并不推荐,甚至可能有些操作系统会报错):
target_link_options(myprogram PRIVATE -static)
Clang:
target_link_options(myprogram PRIVATE -static-libc++ -static-libc++abi)
那如果是Android平台呢?
#可以,但不推荐,因为是全局的设置,甚至可能报错。
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libc++ -static-libc++abi")
#或者用android平台更简单的方式
set(ANDROID_STL c++_static)
如果是JNI的话也可以直接在gradle里这样配置:
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_static"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
- 再说动态链接运行时库:
因为默认情况下,所有平台都是默认的动态链接运行时库,所以只要你不明确的指定是静态链接运行时库,那么它就一定是动态链接运行时库。
那如果我是一个“铁脑壳”,非要指定呢?
MSVC:
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL$<$<CONFIG:Debug>:DebugDLL>")
GCC和Clang:
target_link_options(your_target PRIVATE -shared-libgcc -shared-libstdc++)
Android:
#不推荐:
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -shared-libgcc -shared-libstdc++")
#或
#推荐:
set(ANDROID_STL c++_shared)
如果是JNI的话也可以直接在gradle里这样配置:
android {
defaultConfig {
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
既然是这样,那这个C++运行时库在哪?分发的时候需要我手动添加吗?
- 在Android中,当你选择了动态链接运行时库时,
libc++_shared.so
会自动包含在你的APK中。 - 对于Windows的话,它其实是一个名叫
msvcp140.dll
的文件,这玩意儿应该由Microsoft Visual C++ Redistributable
安装的,我也没仔细考究它是否会在默认的Windows中存在。 - 对于Linux,不同的发行版好像还不一样,不过都包含了这个C++运行时库,具体的情况需要再研究。
- 对于macOS和iOS,系统也自带了,这个库好像是:libc++.1.dylib,Apple的Clang编译器,它会自动动态链接到它上面。
好,现在到这里解释了动态库/静态库与运行时库,我们现在可以自由配置链接它们了,那CMake里有哪些链接方式呢?
- 使用
find_package
命令:
对于一些常用库,CMake提供了对应的Find<PackageName>.cmake
模块或<PackageName>Config.cmake
模块,可以使用find_package
命令自动找到这些库并创建对应的导入目标。例如:
#如果你想静态链接就加上这句代码,否则就是动态链接。每个库的变量不一样,请自己查找。
set(OPENSSL_USE_STATIC_LIBS TRUE)
find_package(OpenSSL REQUIRED)
target_link_libraries(MyExecutable PRIVATE OpenSSL::SSL)
在这个例子中,OpenSSL::SSL
就是一个导入目标,它包含了链接OpenSSL库所需的所有信息,包括库的路径、头文件的路径以及其他编译选项。
需要注意的是,并不是所有库都提供了CMake的查找模块,也不是所有查找模块都提供了选择静态链接或动态链接的选项。
- 直接指定库文件的路径:
你可以手动找到库文件的路径,然后在target_link_libraries
命令中直接使用这个路径。例如:
#静态
target_link_libraries(MyExecutable PRIVATE "/path/to/mylibrary.lib")
#动态,它们之前的区别就是文件本身。
target_link_libraries(MyExecutable PRIVATE "/path/to/mylibrary.so")
在这个例子中,"/path/to/mylibrary.lib"
应该替换为你的库文件的实际路径。这种方式可以用于链接任何类型的库,只需要提供正确的库文件路径即可。
- 创建导入目标:你可以手动创建一个导入目标,然后在这个导入目标上设置库文件的路径以及其他属性。例如:
# 动态链接
add_library(MyLibrary SHARED IMPORTED)
set_target_properties(MyLibrary PROPERTIES
IMPORTED_LOCATION "/path/to/mylibrary.dll" # Windows
# 或者
# IMPORTED_LOCATION "/path/to/mylibrary.so" # Linux
INTERFACE_INCLUDE_DIRECTORIES "/path/to/mylibrary/headers"
)
# 静态链接
add_library(MyLibrary STATIC IMPORTED)
set_target_properties(MyLibrary PROPERTIES
IMPORTED_LOCATION "/path/to/mylibrary.lib" # Windows
# 或者
# IMPORTED_LOCATION "/path/to/mylibrary.a" # Linux
INTERFACE_INCLUDE_DIRECTORIES "/path/to/mylibrary/headers"
)
target_link_libraries(MyExecutable PRIVATE MyLibrary)
在这个例子中,MyLibrary
是你创建的一个导入目标,"/path/to/mylibrary.*"
和"/path/to/mylibrary/headers"
应该替换为你的库文件和头文件的实际路径。
至于add_library(MyLibrary SHARED IMPORTED)
里面的是SHARED
还是STATIC
并不能决定是静态还是动态,本质还是看具体的库文件。只是说加上这个,编译器会预处理一些东西,原则上是与真实的库文件保持相同就行。如果不一样,也有可能会编译通过,要看具体代码。
- 自定义查找模块:
如果CMake没有提供查找某个库的模块,或者提供的模块不能满足你的需求,你可以编写自定义的查找模块。自定义的查找模块通常会使用CMake提供的find_path
、find_library
、find_package_handle_standard_args
等命令来找到库文件和头文件的路径,并创建导入目标或者设置变量。至于具体怎么写,请参阅其他文章。
好像讲到这里还没说,静态库和动态库在链接时,为什么需要跟C++运行时库发生点关系?因为如果你链接的库是使用静态运行时库编译的,那么你自己的库用动态运行时的话就会报:error LNK2038错误(MSVC里是这个错,其他的自己试)。所以,自己的库要与链接的库的C++运行时库要相同。
暂时就这么多,本来还想多写点,发现有其他事要做了。。。