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.
August 10, 2016 at 7:50 pm |
> 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
August 10, 2016 at 8:54 pm |
> 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,
August 10, 2016 at 9:12 pm
> Can you say more about the CMake anti-pattern of SHARED and STATIC?
I will write some examples tomorrow and will leave a link here. In short: it’s not optimal in build time, doesn’t scale when used as third party, lead to PIC conflicts on Linux and quite surprising for user who expect to see shared library when `CMAKE_SHARED_LIBS=ON` and static library when `CMAKE_SHARED_LIBS=OFF`.
August 12, 2016 at 3:22 pm
> Can you say more about the CMake anti-pattern of SHARED and STATIC?
http://cgold.readthedocs.io/en/latest/tutorials/libraries/static-shared.html
August 11, 2016 at 1:57 pm |
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.
August 12, 2016 at 3:29 pm
> 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.
August 14, 2016 at 11:22 am |
[…] Opt-in header-only libraries with CMake […]
June 21, 2018 at 8:33 am |
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
)
September 5, 2020 at 6:23 pm |
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!
August 29, 2021 at 6:07 pm |
http://Ismp.cpatu.embrapa.br/
Opt-in header-only libraries with CMake | Steveire's Blog