Kesco的编码世界

如何合并静态库

用C++写一个SDK给用户使用,一般来说都是采取打包好的动态库、静态库加头文件的方式提供。SDK内部实现往往是有不少第三方依赖,如果是静态库的形式,SDK和第三方库的编译产物都是多个独立的静态库,比如sdk.a、liba.a和libb.a等等,不利于项目集成。本文以Cmake构建方式为例,讲述如何合并多个静态库的方法。

合并静态库的工具

一般来说,C++工具链都会内置合并工具的功能,根据平台的不同,其工具依次为:

  1. Linux下,使用ar来合并静态库。
  2. macOS下,使用libtool来合并静态库。
  3. Windows下,使用lib.exe来合并静态库。

其合并命令行格式如下所示:

  1. Linux下的ar
# 旧版本ar命令不一定支持一步到位合并静态库,这时候可能需要先把.a文件拆回多个.o文件或者使用.mri文件辅助
ar -rcs target.a deps0.a deps1.a
  1. macOS下的libtool
libtool -static -o target.a deps0.a deps1.a
  1. Windows下的lib.exe
lib /NOLOGO /OUT:target.lib deps0.lib deps1.lib

使用Cmake脚本编译时自动合并静态库

如果每次编译都要手动敲命令来执行合并操作,实在太麻烦了,工作中往往使用脚本来自动处理。以我写的一个cmake函数脚本为例,每次编译时会自动生成合并静态库的target,读者可以参考使用。

该脚本已经托管到github上,其地址是【kesco/static-libraries-combiner】

function(combine_static_libraries base_lib target_lib_name)
  list(APPEND static_libs ${base_lib})

  function(get_lib_deps_recursively target)
    set(link_libs LINK_LIBRARIES)
    get_target_property(target_type ${target} TYPE)
    if (${target_type} STREQUAL "INTERFACE_LIBRARY")
      set(link_libs INTERFACE_LINK_LIBRARIES)
    endif()
    get_target_property(public_dependencies ${target} ${link_libs})
    foreach(dependency IN LISTS public_dependencies)
      if(TARGET ${dependency})
        get_target_property(alias ${dependency} ALIASED_TARGET)
        if (TARGET ${alias})
          set(dependency ${alias})
        endif()
        get_target_property(_type ${dependency} TYPE)
        if (${_type} STREQUAL "STATIC_LIBRARY")
          list(APPEND static_libs ${dependency})
        endif()

        get_property(library_already_added
          GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency})
        if (NOT library_already_added)
          set_property(GLOBAL PROPERTY _${base_lib}_static_bundle_${dependency} ON)
          get_lib_deps_recursively(${dependency})
        endif()
      endif()
    endforeach()
    set(static_libs ${static_libs} PARENT_SCOPE)
  endfunction()

  get_lib_deps_recursively(${base_lib})

  list(REMOVE_DUPLICATES static_libs)

  set(target_lib_path
    ${CMAKE_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${target_lib_name}${CMAKE_STATIC_LIBRARY_SUFFIX})

  if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$")
    file(WRITE ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
      "CREATE ${target_lib_path}\n" )

    foreach(tgt IN LISTS static_libs)
      file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in
        "ADDLIB $<TARGET_FILE:${tgt}>\n")
    endforeach()

    file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "SAVE\n")
    file(APPEND ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in "END\n")

    file(GENERATE
      OUTPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
      INPUT ${CMAKE_BINARY_DIR}/${target_lib_name}.ar.in)

    set(ar_tool ${CMAKE_AR})
    if (CMAKE_INTERPROCEDURAL_OPTIMIZATION)
      set(ar_tool ${CMAKE_CXX_COMPILER_AR})
    endif()

    add_custom_command(
      COMMAND ${ar_tool} -M < ${CMAKE_BINARY_DIR}/${target_lib_name}.ar
      OUTPUT ${target_lib_path}
      COMMENT "Packing ${target_lib_name}"
      VERBATIM)
  elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang")
    find_program(ar_tool libtool)

    foreach(tgt IN LISTS static_libs)
      list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
    endforeach()

    add_custom_command(
      COMMAND ${ar_tool} -static -o ${target_lib_path} ${static_lib_paths}
      OUTPUT ${target_lib_path}
      COMMENT "Packing ${target_lib_name}"
      VERBATIM)
  elseif(MSVC)
    find_program(ar_tool lib)

    foreach(tgt IN LISTS static_libs)
      list(APPEND static_lib_paths $<TARGET_FILE:${tgt}>)
    endforeach()

    add_custom_command(
      COMMAND ${ar_tool} /NOLOGO /OUT:${target_lib_path} ${static_lib_paths}
      OUTPUT ${target_lib_path}
      COMMENT "Packing ${target_lib_name}"
      VERBATIM)
  else()
    message(FATAL_ERROR "Unknown compiler!")
  endif()

  set(custom_target_name combine_universal_lib_for_${base_lib})
  add_custom_target(${custom_target_name} ALL DEPENDS ${target_lib_path})
  add_dependencies(${custom_target_name} ${base_lib})

  add_library(${target_lib_name} STATIC IMPORTED GLOBAL)
  set_target_properties(${target_lib_name}
    PROPERTIES
      IMPORTED_LOCATION ${target_lib_path}
      INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${base_lib},INTERFACE_INCLUDE_DIRECTORIES>)
  add_dependencies(${target_lib_name} ${custom_target_name})

endfunction()

使用方法也很简单,在CMakeLists.txt上简单引用声明即可。

# 引入该函数脚本
include(${CMAKE_SOURCE_DIR}/../combiner.cmake)

# 声明合并的静态库Target
combine_static_libraries(${TARGET_LIB} ${TARGET_LIB}_Bundle)

# 在cmake别的target上链接该合并静态库
target_link_libraries(${EXECUTABLE} ${TARGET_LIB}_Bundle)

对合并静态库进一步处理

给用户提供静态库产物,往往也希望隐藏下内部实现的符号位,避免用户反编译一眼看出代码底细。笔者推荐一个用Rust写的好工具armerge,在合并静态库的同时可以去除暴露的符号位,调用命令也很简单:

armerge --keep-symbols '^libfoo_' --output libfoo_merged.a libfoo.a libcrypto.a

想在Cmake上自动处理,只需要把上面的Cmake函数的add_custom_command部分命令调用改成armerge就可以了,这里就不重复叙述了。

#Linux #MacOS #Windows #C++ #CMake