Cristian Adam

Bundling together static libraries with CMake

In this article I’m going to talk about building a C++ library with CMake, but it won’t be a CMake tutorial.

Let’s say you have a C++ library which depends upon a few open source libraries, which have a CMake project structure, but not necessarily done by the book (which means that they get only get built, and not deployed / installed)

Your library will include tests (unit-tests / integration tests), and the deployment can be just packing the headers and the binaries together in a tar.gz file.

This is not necessarily by the book, but it will do the job, and it could fit into any build system that the client has.

A book that one can use to do CMake right is Profesional CMake. Awesome CMake also has a great list of resources regarding CMake.

Coming back to the C++ library, which decisions do we take to build it? Shared library, static library, both?

Shared library

The most common decision is to build as a shared library (BUILD_SHARED_LIBS set to TRUE in the CMake script).

The open source dependencies could be also shared libraries, or static libraries. If they are shared libraries you need to take care of deployment. Sometimes you might be forced to compile them as shared libraries, due to licensing for example.

It’s all good, until you have to deal with operating systems like QNX, which has a problem with shared libraries that have lots of symbols. The problem is that it takes longer to load them.

The default GCC and Clang compilers will compile all symbols (functions, classes, global variables) with default visibility. The Visual C++ compiler does the opposite, it hides all the symbols.

You might be familiar with macros like MY_LIB_API which might look like this:

#if defined(_WIN32) || defined(__CYGWIN__)
  #if defined(BUILD_SHARED_LIBS)
    #if defined(MY_LIB_EXPORTS)
      #define MY_LIB_API __declspec(dllexport)
    #else
      #define MY_LIB_API __declspec(dllimport)
    #endif
  #endif
#elif defined(MY_LIB_EXPORTS)
  #define MY_LIB_API __attribute__((visibility("default")))
#endif

#if !defined(MY_LIB_API)
  #define MY_LIB_API
#endif

And then in your CMake script code you have:

set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN 1)

This will ensure that your shared library will contain only the MY_LIB_API symbols. This also means that you won’t have any problems with visible symbols from any open source libraries that you linked statically. Hopefully you can control how that open source libraries decide how to export their symbols.

The generated shared object will also be smaller in size. It depends upon the number of symbols though.

CMake has the GenerateExportHeader which can help with this matter.

But now you will notice that your tests will fail to build, since the symbols they require are not there anymore. So what now?

Shared and static library

We need to have a shared library with only the MY_LIB_API symbols exported, but also have tests working.

The problem with visibility flags is that it will affect the compiler command line, CMAKE_CXX_VISIBILITY_PRESET, and CMAKE_VISIBILITY_INLINES_HIDDEN will result in having -fvisibility=hidden and -fvisibility-inlines-hidden added to the compiler command line.

So we compile a shared library with all symbols, and one with only the MY_LIB_API symbols. But this means compiling twice, which is a bit wasteful.

We could compile a static library with hidden symbols, then create a shared library based on this static library, and link the tests to the static library. The tests will link because the symbols are there in the static library, marked hidden, but still accessible to the linker.

You will have to take care of the POSITION_INDEPENDENT_CODE CMake property, which is not set for static libraries.

This solves it. Everything works. But what if you want to make the QNX case even faster? (by removing the shared library all together!)

Static library

We could just build only the static library, with hidden visibility and ship that. But this also means everything (including client code) needs to be compiled with the same compiler / toolchain.

The problem lies with the open source library dependencies. They also need to be shipped along side with your library, and then the client code needs to link them too.

If you export your CMake targets, you can have the dependencies “linked” to your target, and the client code will only have to specify one target. But this requires proper CMake exports! smile

Bundled static library

What if you could bundle the open source dependencies in the static library?

Stackoverflow has this article: Using cmake to build a static library of static libraries, which boils down to:

ar -M <<EOM
    CREATE libALL.a
    ADDLIB libA.a
    ADDLIB libB.a
    SAVE
    END
EOM

You need to run a script which does this, but wouldn’t it be nice if we had a CMake function which enumerates the dependencies and bundles them into one library?

Here it is:

function(bundle_static_library tgt_name bundled_tgt_name)
  list(APPEND static_libs ${tgt_name})

  function(_recursively_collect_dependencies input_target)
    set(_input_link_libraries LINK_LIBRARIES)
    get_target_property(_input_type ${input_target} TYPE)
    if (${_input_type} STREQUAL "INTERFACE_LIBRARY")
      set(_input_link_libraries INTERFACE_LINK_LIBRARIES)
    endif()
    get_target_property(public_dependencies ${input_target} ${_input_link_libraries})
    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 _${tgt_name}_static_bundle_${dependency})
        if (NOT library_already_added)
          set_property(GLOBAL PROPERTY _${tgt_name}_static_bundle_${dependency} ON)
          _recursively_collect_dependencies(${dependency})
        endif()
      endif()
    endforeach()
    set(static_libs ${static_libs} PARENT_SCOPE)
  endfunction()

  _recursively_collect_dependencies(${tgt_name})

  list(REMOVE_DUPLICATES static_libs)

  set(bundled_tgt_full_name 
    ${CMAKE_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${bundled_tgt_name}${CMAKE_STATIC_LIBRARY_SUFFIX})

  if (CMAKE_CXX_COMPILER_ID MATCHES "^(Clang|GNU)$")
    file(WRITE ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in
      "CREATE ${bundled_tgt_full_name}\n" )
        
    foreach(tgt IN LISTS static_libs)
      file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in
        "ADDLIB $<TARGET_FILE:${tgt}>\n")
    endforeach()
    
    file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in "SAVE\n")
    file(APPEND ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar.in "END\n")

    file(GENERATE
      OUTPUT ${CMAKE_BINARY_DIR}/${bundled_tgt_name}.ar
      INPUT ${CMAKE_BINARY_DIR}/${bundled_tgt_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}/${bundled_tgt_name}.ar
      OUTPUT ${bundled_tgt_full_name}
      COMMENT "Bundling ${bundled_tgt_name}"
      VERBATIM)
  elseif(MSVC)
    find_program(lib_tool lib)

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

    add_custom_command(
      COMMAND ${lib_tool} /NOLOGO /OUT:${bundled_tgt_full_name} ${static_libs_full_names}
      OUTPUT ${bundled_tgt_full_name}
      COMMENT "Bundling ${bundled_tgt_name}"
      VERBATIM)
  else()
    message(FATAL_ERROR "Unknown bundle scenario!")
  endif()

  add_custom_target(bundling_target ALL DEPENDS ${bundled_tgt_full_name})
  add_dependencies(bundling_target ${tgt_name})

  add_library(${bundled_tgt_name} STATIC IMPORTED)
  set_target_properties(${bundled_tgt_name} 
    PROPERTIES 
      IMPORTED_LOCATION ${bundled_tgt_full_name}
      INTERFACE_INCLUDE_DIRECTORIES $<TARGET_PROPERTY:${tgt_name},INTERFACE_INCLUDE_DIRECTORIES>)
  add_dependencies(${bundled_tgt_name} bundling_target)

endfunction()

The usage of this function is as simple as:

  add_library(awesome_lib STATIC ...);
  
  bundle_static_library(awesome_lib awesome_lib_bundled)

Another benefit of a static library is that you could provide a build with Interprocedural Optimization / Link Time Optimization (IPO/LTO) enabled, and then the client code will generate smaller, faster binaries.

CMake has support for IPO/LTO, see CheckIPOSupported, and CMP0069.

Comments