CMake 静态库合并问题

发布时间 2023-12-22 00:28:20作者: Gentleaves

一般我们使用 cmake 生成静态库或者动态库时,它们生成的库都仅包含工程内部的文件,当引用了外部的静态库时,均是以链接形式存在在 cmake 文件中。当我们发布(也就是 install)一个静态库时,这个静态库并不是独立的,如果你想使用它,仍需要查找齐全它所依赖的库。

简单一点说就是,我们编写的静态库 A,依赖了静态库 B、C,直接发布的话,在使用该库时会提示没有 B、C 库的符号。为了方便用户使用我们的库,当有必要时,就需要直接将 ABC 库合并起来,并发布成一个无依赖的库 D,这个过程称为合并静态库。

原理

合并静态库需要使用特殊的工具,msvc 提供 lib.exe,gcc 提供 ar,Macos 提供 libtool。下面以 ar 的使用过程为例。

首先一个我们需要关于静态库的基本知识, 静态库是源文件编译后生成的 .obj/.o 文件的集合。

使用如下命令可以将静态库拆分开,可以看到,静态库包含一个 obj 文件

> ar x ./libbirl4th_basic_lib.a
> ls
controller.cpp.obj  libbirl4th_basic_lib.a

将 obj 文件合并为一个静态库

λ ar rcs merge.a *.obj
λ ls
controller.cpp.obj  libbirl4th_basic_lib.a  merge.a

命令含义:

  1. x:拆解静态库文件为其包含的内容
  2. r:替换或添加文件到归档文件中。
  3. c:创建归档文件,如果它不存在。
  4. s:相当于对结果执行一次 ranlib,为静态库的内容添加索引,提高访问效率
  5. T or --thin:对归档文件进行瘦身、压缩。

例如我们有一些静态库文件,那么可以采用如下命令行编译,即可将 ABC 静态库打包到 D 中。

ar crs D.a A.a B.a C.a

使用 T or --thin 可对生成的库文档进一步瘦身:

ar crs --thin D.a A.a B.a C.a

为了验证瘦身带来的效果,我这里打包一些静态库,并分别生成 merge_s.a merge_sT.a 两个库文件,可以看到,使能了 T 之后的文件小了 10 倍还多。

ar crsT ./merge_sT.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/FMT/lib/libfmt.a `
D:/BIRL_LAB/ForthModularRobot/Project/birl4th_basic/build/Host-GCC-12.2.0-Release/libbirl4th_basic_lib.a `
D:/BIRL_LAB/ForthModularRobot/Project/birl4th_basic/build/Host-GCC-12.2.0-Release/_birl4th/lib/libbirl4th_lib_s.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/spdlog/lib/libspdlog.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/orocos_kdl/lib/liborocos-kdl.a

ar crs ./merge_s.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/FMT/lib/libfmt.a `
D:/BIRL_LAB/ForthModularRobot/Project/birl4th_basic/build/Host-GCC-12.2.0-Release/libbirl4th_basic_lib.a `
D:/BIRL_LAB/ForthModularRobot/Project/birl4th_basic/build/Host-GCC-12.2.0-Release/_birl4th/lib/libbirl4th_lib_s.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/spdlog/lib/libspdlog.a `
C:/MyWorkSoft/CPlusPlusLib/lib_thirdparty/orocos_kdl/lib/liborocos-kdl.a

ls -alh merge_s.a merge_sT.a
-rw-r--r-- 1 91225 197609 6.6M Dec 21 21:50 merge_s.a
-rw-r--r-- 1 91225 197609 401K Dec 21 21:50 merge_sT.a

需要注意的是,T 选项还带来了展开文件内容的效果,如下,经过实际编译测试,静态库必须仅包含 obj 文件才能链接其它程序一起编译,存在嵌套情况的话会找不到符号。因此,为了使合并后的静态库可以链接、编译,必须使用 crsT

λ ar -t ./merge_s.a
libfmt.a
libbirl4th_basic_lib.a
libbirl4th_lib_s.a
libspdlog.a
liborocos-kdl.a

λ ar -t ./merge_sT.a
format.cc.obj
os.cc.obj
controller.cpp.obj
io.cpp.obj
logger.cpp.obj
times.cpp.obj
utils.cpp.obj
controller_device.cpp.obj
device_sim.cpp.obj
pid.cpp.obj
device.cpp.obj
filter.cpp.obj
filter_c.c.obj
gravity_compensate.cpp.obj
joint_force_observer.cpp.o
.......

实践

CMake 中是不支持合并静态库的,想要合并静态库只能借助于 add_custom_command add_custom_target 用外部命令来实现。

# 待合并的 static 库
list(APPEND lib_merged 
  orocos-kdl
  birl4th_lib::birl4th_lib
  fmt::fmt
  spdlog::spdlog
  )

# 获得静态库位置
list(APPEND lib_locations "")
foreach(lib ${lib_merged})
  set(lib_location $<TARGET_FILE:${lib}>)
  list(APPEND lib_locations ${lib_location})
endforeach(lib)

# 如果rebuild,删掉之前生成的文件
add_custom_target(rm_libmerge.a
  COMMAND ${CMAKE_COMMAND} -E remove -f ${CMAKE_CURRENT_BINARY_DIR}/libmerge.a
)

# 调用命令行重新生成
add_custom_command(OUTPUT libmerge.a
  COMMAND  ${CMAKE_AR}  crsT ${CMAKE_CURRENT_BINARY_DIR}/libmerge.a ${lib_locations}
  DEPENDS rm_libmerge.a
)

可以看到,一共分三个步骤:

  1. 列出所有需要合并的静态库。
  2. 使用 $<TARGET_FILE:${lib}> 获取静态库位置。
  3. 调用 ar crsT 合并静态库,并展开嵌套。需要注意的是,这里添加了一个依赖,使得每次 build 时都会重新合并静态库,保证库是最新的。

需要注意的是,使用 ${CMAKE_AR} 确保使用工具链对应的 ar

有了静态库后,就可以包装一下,然后进行调用了。

# 设置生成的静态库的输出名称
add_library(merge STATIC IMPORTED )
set_target_properties(merge PROPERTIES
  OUTPUT_NAME libmerged
  IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/libmerge.a
  DEPENDS libmerge.a
)
target_include_directories(merge INTERFACE some_include)

# 调用时直接link即可。
add_executable(${target} "${target}.cpp")
target_link_libraries(${target} merge)

安装、导出

如果你想安装或者导出该库,那么需要再包装一下才行,这样包装后的库就像一个普通的静态库一样了。

set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> crsT <TARGET> <LINK_FLAGS> <OBJECTS>")
message(STATUS "ARCHIVE: ${CMAKE_CXX_ARCHIVE_CREATE}")

add_library(birl4th_basic STATIC $<TARGET_OBJECTS:merge> )
set_target_properties(birl4th_basic PROPERTIES LINKER_LANGUAGE CXX)

# 
target_include_directories(birl4th_basic PUBLIC 
  $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>      # 
  $<INSTALL_INTERFACE:${birl4th_basic_install_include_dir}>)

add_library(birl4th_basic::birl4th_basic ALIAS birl4th_basic)

set_target_properties(birl4th_basic
  PROPERTIES
    POSITION_INDEPENDENT_CODE 1
    # WINDOWS_EXPORT_ALL_SYMBOLS ON
    ARCHIVE_OUTPUT_NAME "birl4th_basic"
    DEBUG_POSTFIX "_rd"
    RELEASE_POSTFIX "_s"
    PUBLIC_HEADER "${HEADERS}"
  )

需要注意一下几个问题:

  1. 二次包装会导致静态库发生嵌套,导致链接时找不到符号,因此需要想办法让它在链接时自动使用 ar--thin 选项,因此配置 CMAKE_CXX_ARCHIVE_CREATE,可惜的是这个是全局选项,如果有针对目标的配置就好了。
  2. 因为 merge 库已经丢失了语言信息,因此需要重新指定语言:set_target_properties(birl4th_basic PROPERTIES LINKER_LANGUAGE CXX)

参考

  1. CMake 应用:合并静态库的最佳实践
  2. CMake 合并静态库 | C & C++
  3. ar