Opt-in header-only libraries with CMake

Using a C++ library, particularly a 3rd party one, can be complicated affair. Library binaries compiled on Windows/OSX/Linux can not simply be copied over to another platform and used there. Linking works differently, compilers bundle different code into binaries on each platform etc.

This is not an insurmountable problem. Libraries like Qt distribute dynamically compiled binaries for major platforms and other libraries have comparable solutions.

There is a category of libraries which considers the portable binaries issue to be a terminal one. Boost is a widespread source of many ‘header only’ libraries, which don’t require a user to link to a particular platform-compatible library binary. There are also many other examples of such ‘header only’ libraries.

Recently there was a blog post describing an example library which can be built as a shared library, or as a static library, or used directly as a ‘header only’ library which doesn’t require the user to link against anything to use the library. The claim is that it is useful for libraries to provide users the option of using a library as a ‘header only’ library and adding preprocessor magic to make that possible.

However, there is yet a fourth option, and that is for the consumer to compile the source files of the library themselves. This has the
advantage that the .cpp file is not #included into every compilation unit, but still avoids the platform-specific library binary.

I decided to write a CMake buildsystem which would achieve all of that for a library. I don’t have an opinion on whether good idea in general for libraries to do things like this, but if people want to do it, it should be easy as possible.

Additionally, of course, the CMake GenerateExportHeader module should be used, but I didn’t want to change the source from Vittorio so much.

The CMake code below compiles the library in several ways and installs it to a prefix which is suitable for packaging:

cmake_minimum_required(VERSION 3.3)

project(example_lib)

# define the library

set(library_srcs
    example_lib/library/module0/module0.cpp
    example_lib/library/module1/module1.cpp
)

add_library(library_static STATIC ${library_srcs})
add_library(library_shared SHARED ${library_srcs})

add_library(library_iface INTERFACE)
target_compile_definitions(library_iface
    INTERFACE LIBRARY_HEADER_ONLY
)

set(installed_srcs
    include/example_lib/library/module0/module0.cpp
    include/example_lib/library/module1/module1.cpp
)
add_library(library_srcs INTERFACE)
target_sources(library_srcs INTERFACE
    $<INSTALL_INTERFACE:${installed_srcs}>
)

# install and export the library

install(DIRECTORY
    example_lib/library
  DESTINATION
    include/example_lib
)
install(FILES
    example_lib/library.hpp
    example_lib/api.hpp
  DESTINATION
    include/example_lib
)
install(TARGETS
    library_static
    library_shared
    library_iface
    library_srcs
  EXPORT library_targets
  RUNTIME DESTINATION bin
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  INCLUDES DESTINATION include
)
install(EXPORT library_targets
  NAMESPACE example_lib::
  DESTINATION lib/cmake/example_lib
)

install(FILES example_lib-config.cmake
  DESTINATION lib/cmake/example_lib
)

This blog post is not a CMake introduction, so to see what all of those commands are about start with the cmake-buildsystem and cmake-packages documentation.

There are 4 add_library calls. The first two serve the purpose of building the library as a shared library and then as a static library.

The next two are INTERFACE libraries, a concept I introduced in CMake 3.0 when it looked like Boost might use CMake. The INTERFACE target can be used to specify header-only libraries because they specify usage requirements for consumers to use, such as include directories and compile definitions.

The library_iface library functions as described in the blog post from Vittorio, in that users of that library will be built with LIBRARY_HEADER_ONLY and will therefore #include the .cpp files.

The library_srcs library causes the consumer to compile the .cpp files separately.

A consumer of a library like this would then look like:

cmake_minimum_required(VERSION 3.3)

project(example_user)

find_package(example_lib REQUIRED)

add_executable(myexe
    src/src0.cpp
    src/src1.cpp
    src/main.cpp
)

## uncomment only one of these!
# target_link_libraries(myexe 
#     example_lib::library_static)
# target_link_libraries(myexe
#     example_lib::library_shared)
# target_link_libraries(myexe
#     example_lib::library_iface)
target_link_libraries(myexe
     example_lib::library_srcs)

So, it is up to the consumer how they consume the library, and they determine that by using target_link_libraries to specify which one they depend on.

10 Responses to “Opt-in header-only libraries with CMake”

  1. ruslo Says:

    > Library binaries compiled on Windows/OSX/Linux can not simply be copied over to another platform and used there. Linking works differently, compilers bundle different code into binaries on each platform etc.

    This is a job for package manager. Trying to solve it another way will lead to ugly hacks or/and affect performance.

    Unlike library_iface approach library_srcs **doesn’t inline code**. So effectively you got worse from both worlds: like library_static it doesn’t inline code from `*.cpp` files, like library_iface it doesn’t compile code once for `*.cpp` sources (you have to compile them for each project where it used).

    Also I hope that `add_library(… STATIC)` + `add_library(… SHARED)` code here is just for the simplifying purposes, this is cleanly CMake antipattern.

    What would be nice to have is a Single Compilation Unit optimization support from-the-box in CMake working in a similar way. So when you have `add_library(foo ${foo_sources})` user can:

    * CMAKE_SHARED_LIBS=ON – install shared library
    * CMAKE_SHARED_LIBS=OFF – install static library (default)
    * CMAKE_SCU=ON – install sources

    When sources installed there is no compilation of `*.cpp` files happens until we met executable. For executable CMake should create file with all `*.cpp` included:

    #include
    #include

    This is kind of emulated Link Time Optimization. But unlike LTO we have next benefits:

    * We can use such optimization for compilers that doesn’t have `-flto` support
    * We can use Clang Analyzer more efficiently (I guess)
    * `-flto` may have bugs/limitation, SCU do it perfectly
    * SCU is faster because all headers processed once. I’m not sure, but I think `-flto` process them for each `*.cpp` source like regular compile process

    • steveire Says:

      > This is a job for package manager.

      Yes, I agree. But still, others don’t see it that way.

      Can you say more about the CMake anti-pattern of SHARED and STATIC? I’ve never attempted to create both types of libraries from a single build before, nor have I seen anyone else try to do that with CMake.

      Thanks,

    • Gregor Jasny Says:

      Sometimes you want to compile some source files with exactly the same compiler version and flags as your library or executable. For example LLVMs libfuzzer strongly depends on compiler internal stuff. On the other hand you do not want to give up the possibility to package, version, and handle it like any other dependency.

      One thing that I’m missing is to specify PRIVATE compile definitions or options to sneak in a “-Wno-error” for compiling the “foreign” source files or something else I just want to have privately applied when compiling the sources.

      • ruslo Says:

        > Sometimes you want to compile some source files with exactly the same compiler version and flags as your library or executable

        What are you trying to say? That package managers must provide that customization? I thinks it’s true, totally agree.

  2. Links 14/8/2016: ‘Goodbye Windows – Hello Ubuntu’, Linux Mint 18 Xfce Overview | Techrights Says:

    […] Opt-in header-only libraries with CMake […]

  3. Dmitry Igrishin Says:

    Seems, that lines 58-60 are wrong and should be replaced with the FILE option of the install(EXPORT) command:

    install(EXPORT library_targets
    NAMESPACE example_lib::
    DESTINATION lib/cmake/example_lib
    FILE example_lib-config.cmake
    )

    • Philippe Coté Says:

      I spent so much time trying to figure out what was wrong with the CMakeLists as `make install` simply didn’t work. Files were not being installed where they should have. Thanks for pointing out the proper fix!

  4. Http://Ismp.cpatu.embrapa.br/ Says:

    http://Ismp.cpatu.embrapa.br/

    Opt-in header-only libraries with CMake | Steveire's Blog

Leave a comment