CMake Fundamentals Part 3

by | Jan 25, 2021 | CMake | 3 comments

In the previous part of the series we looked at the basics of defining and using libraries with CMake. In this part we explore the concepts of target properties and the PRIVATE, INTERFACE and PUBLIC keywords – a crucial pieces of knowledge to using CMake well. While we’re at it, we will once again extend the project we have gotten started on in part 2. Once again, there’s a lot of ground to cover, so let’s get right into it.

Properties

Broadly speaking CMake projects are customized with two major facilities – cache variables (which we will return to at some point) and properties. Whereas variables, be it normal or cached, have for the most part, global scope, properties are attached to an entity – they specify some aspect of it within the project. Properties may be specified for a wide range of scopes – from GLOBAL, through DIRECTORY and TARGET down do individual files. Today we’ll focus on TARGET properties – as these are what will help us with using targets to structure our projects well.

Properies may be operated on directly using the most general commands – set_property and get_property. These commands can set or fetch the value of any property in any scope. However, as the result of the generality they’re the most complicated to use. Most often it’s more convenient to use the more specialized alternatives, like set_target_properties and get_target_property. In fact, even these commands are usually too big of a hammer for the task. Instead, the most used commands operate on a very limited set of properties – usually just one or two. In fact, we’ve already used a command that operates on properties – target_link_libraries! If a command operates on a target, and does not create one, it most likely modifies its properties.

Project setup

In the previous part of the series we started working on a simple library that exposed a single add function. Expanding this example will help us illustrate the concept of properties. First of all, we will isolate the library code from the application code. For this purpose we create a subdirectory named add, move the library source files – calc_status.h, add.h, add.cpp into that directory, and create a CMakeLists.txt within it. We end up with the following directory structure:

.
├── add
│   ├── add.cpp
│   ├── add.h
│   ├── calc_status.h
│   └── CMakeLists.txt
├── CMakeLists.txt
└── main.cpp

All that needs to be done in the Add library CMakeLists.txt is to declare the library target:

add_library(Add
    calc_status.h
    add.h
    add.cpp
)

We now need to incorporate this directory into our project so that we may use it. The top-level CMakeLists.txt now looks as follows:

cmake_minimum_required(VERSION 3.19)
project(Fundamentals)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

add_subdirectory(add)

add_executable(Main main.cpp)
target_link_libraries(Main
    PUBLIC
        Add
)

add_subdirectory

To include a directory into a project we introduce a new command – add_subdirectory The add_subdirectory command does exactly what it says – it adds the specified directory to the project. The command expects that the directory contains a CMakeLists.txt file, and processes it immediately – all of the targets defined within the listing will be visible across the entire project. The add_subdirectory command allows us to keep the project-defining CMakeLists.txt files as close to the actual code as possible, this improves readability and maintainability, and actually helps with good design – a subdirectory in this case isolates the library as a separate component or a module. The Add target defined in the subdirectory is then linked into the application code. As a reminder, the main.cpp looks as follows.

#include <iostream>
#include <limits>

#include "add/add.h"
#include "add/calc_status.h"

void add(int a, int b)
{
    calc_status cs{};
    auto const result = add(a, b, cs);
    if (cs == calc_status::success)
    {
        std::cout << a << " + " << b << " = " << result << "\n";
    }
    else
    {
        std::cout << "Error calculating " << a << " + " << b << "\n";
    }
}

int main()
{
    const int a{42};
    const int b{102};

    add(a, b);
    add(a, std::numeric_limits<int>::max());
}

To include the Add library headers we now need to go down into the add directory.

Inspecting properties

Let’s now inspect how the target_link_libraries command affects the properties of the Main target. For that purpose we will create our first CMake function.

CMake functions serve the same purpose as in any other language – they facilitate code reuse. Function definitions are enclosed in a function() block. For our needs we will define a function that fetches the value of some target properties we’re interested in, and echoes them to the screen.


function(PrintTargetLibraries target msg)
    get_target_property(linkedLibs ${target} LINK_LIBRARIES)
    get_target_property(interfaceLibs ${target} INTERFACE_LINK_LIBRARIES)
    message(STATUS "${msg}")
    message(STATUS "${target} LINK_LIBRARIES: ${linkedLibs}")
    message(STATUS "${target} INTERFACE_LINK_LIBRARIES: ${interfaceLibs}")
endfunction()

The above listing defines a function named PrintTargetLibraries that accepts arguments target and msg. We see a new piece of syntax here – variables are dereferenced by enclosing them in ${}. The get_target_property command fetches the value of LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties and stores them in variables linkedLibs and interfaceLibs, respectively. The value is then printed to the screen using the message command. We will gloss over some of the details here as usual.

Our function can be called same as any other CMake command. We use it to print the value of the selected properties on the Main target, before, and after linking the Add library:

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
    PRIVATE
        Add
)
PrintTargetLibraries(Main "After linking Add:")

Let’s now configure the project to see the output:

$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: Add
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND

We see that before linking the Add library, the variables are set to linkedLibs-NOTFOUND and interfaceLibs-NOTFOUND values respectively. This is a common pattern in CMake – when fetching something that doesn’t exist, the destination variable is set to <variableName>-NOTFOUND. Here – these properties have not been set yet, and that how this is signalled by get_target_property. After linking the library the LINK_LIBRARIES is set to Add – as expected. The INTERFACE_LINK_LIBRARIES however is still empty. To understand why, we will need to discuss the difference between the LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties. We will do so, while also discussing the PRIVATE, INTERFACE and PUBLIC keywords.

PRIVATE, INTERFACE, PUBLIC

The PRIVATE, INTERFACE and PUBLIC keywords may bring to mind access specifiers familiar to anyone aquainted with object-oriented languages. This may seem applicable here at first, however at closer inspection it isn’t precisely right. Instead, I’d suggest to think in terms of transitivness of dependencies of a particular target. For exapmle, given target Foo, the semantic meaning of these keywords is:

PRIVATE – fully encapsulated dependencies of Foo – they are required to build Foo, but using Foo does not expose the user to the private dependencies

INTERFACE – dependencies required to use Foo – but NOT required to build Foo itself, Users of Foo will need also these dependencies

PUBLIC – transitive dependencies of Foo – they are required to build Foo, and users of Foo will also depend on these targets indirectly

This has been difficult to grasp intuitively for me at first. What has helped me greatly is to learn, that these keywords are implemented in terms of properties – all they do is specify which properties are modified by a given command. Going back to our target_link_libraries example – it operates on the LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties.
The LINK_LIBRARIES property specifies a list of libraries that need to be linked to the given target in order to build it. In this case the target Add is added to the list – CMake will handle dereferencing the target and linking the actual library at build time. This property can be thought of as a list of requirements necessary to build the given target.
The INTERFACE_LINK_LIBRARIES property specifies a list of libraries that need to be linked to the targets which use (link) the given target. In other words, these are transitive dependencies – if the use of library A also requires linking of library B we could ensure that this is done properly by setting this property to B on library A. This may seem somewhat abstract at the moment – we will see an example later that puts this into perspective.

Thus all these keywords do, is specify if one, or both of these properties should be affected by the target_link_libraries call. The exact mapping is presented in the table below:

Keyword Properties affected
PRIVATE LINK_LIBRARIES
INTERFACE INTERFACE_LINK_LIBRARIES
PUBLIC LINK_LIBRARIES, INTERFACE_LINK_LIBRARIES

 
Given our Add target library and Main target executable that uses it, let’s inspect how specifying different keywords would affect these properties. We’ve already seen the example of using PRIVATE. Let’s now try using INTERFACE:

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
    INTERFACE
        Add
)
PrintTargetLibraries(Main "After linking Add:")

Let’s now configure the project to see the output:

$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: Add

Note that attempting to actually build the project (rather than just configure it) would fail:

$ cmake --build build
/usr/bin/c++ -O3 -DNDEBUG CMakeFiles/Main.dir/main.cpp.o -o Main 
/usr/bin/ld: CMakeFiles/Main.dir/main.cpp.o: in function `add(int, int)':
main.cpp:(.text+0x30): undefined reference to `add(int, int, calc_status&)'

This is because calling target_link_libraries with INTERFACE won’t actually link the library into the target – and since Main directly uses code from the Add library, it needs to be linked.

The last keyword we need to inspect is PUBLIC. As described – the Add library should be added to both properties:

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
target_link_libraries(Main
    PUBLIC
        Add
)
PrintTargetLibraries(Main "After linking Add:")
$ cmake -B build -S .
-- Before linking Add:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Add:
-- Main LINK_LIBRARIES: Add
-- Main INTERFACE_LINK_LIBRARIES: Add

And it is. Since the library is now actually linked into the Main target, the project can be built successfully now:

$ cmake --build build
[ 75%] Building CXX object CMakeFiles/Main.dir/main.cpp.o
/usr/bin/c++ -o CMakeFiles/Main.dir/main.cpp.o -c /home/user/cmake_fundamentals/main.cpp
[100%] Linking CXX executable Main
/usr/bin/c++ -O3 -DNDEBUG CMakeFiles/Main.dir/main.cpp.o -o Main  add/libAdd.a 
[100%] Built target Main

Note that in this case using PUBLIC does not actually make any sense – since this is an executable target, that isn’t, won’t and can’t be linked into any other targets, therefore appending dependencies to INTERFACE_LINK_LIBRARIES has no practical use.

General-purpose property commands

At the start of this post I had mentioned that there are multiple, general-purpose commands for operating on targets. As a small aside, and curiosity let’s see how these commands could be used instead of taret_link_libraries to achieve the same goal.

First we’ll look at the more specialized set_target_properties. With this command it is relatively easy to modify both LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES in a single call:

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
set_target_properties(Main
    PROPERTIES
        LINK_LIBRARIES Add
        INTERFACE_LINK_LIBRARIES Add
)
PrintTargetLibraries(Main "After linking Add:")

The above call is equivalent to calling target_link_libraries with the PUBLIC specifier. Configuring and building the project would result in the same output. Note however, that here we set these properties, instead of appending to them – had they contained any values already, we’d clobber them. That’s why using target_link_libraries is both more convenient and readable.

And for completeness let’s now see how we could achieve the same thing using set_property – the most general command for property manipulation.

add_executable(Main main.cpp)
PrintTargetLibraries(Main "Before linking Add:")
set_property(TARGET Main
    APPEND
    PROPERTY
        LINK_LIBRARIES Add
)
set_property(TARGET Main
    APPEND
    PROPERTY
        INTERFACE_LINK_LIBRARIES Add
)
PrintTargetLibraries(Main "After linking Add:")

With set_property we need to specify explicitly that we’d like to manipulate properties on a TARGET. As an improvement, compared to set_target_properties we get to specify APPEND mode – the existing values won’t be overriden, but rather appended to. The trade-off here is that we can modify only a single property at a time, so to imitate a call to target_link_libraries with the PUBLIC specifier, two calls are necessary.

This was just for demonstration purposes – in real code prefer the right tool for the job, in this case – target_link_libraries.

Summary

In this part of the series we introduced a few new commands, but most importantly we got the chance to understand exactly what the PRIVATE, INTERFACE and PUBLIC keywords do and how they’re implemented. We learned enough about properties to be able to reason about what to expect from commonly used target-commands, and we’re now ready to explore the concept further.

You may have noticed that even though the Add library was isolated into a separate directory it was not fully encapsulated – we still used relative imports in the main.cpp, which could be seen as a leaky abstraction. In the next part of the series we address this issue and discuss modular design. We will also see practical examples of using INTERFACE and PUBLIC keywords, and we extract some guidelines for when to use each specifier.

Did you learn anything new from what we discussed in this post? Do you expect that any of it will be directly applicable to your own code and daily work?

 

References

3 Comments

  1. Alex

    This is very useful article. It reveals some crucial concepts of cmake.

    Reply
  2. Dave Smith

    Jeremi, this is an excellent guide. While “Professional CMake” by Craig Scott (@crascit on X) is the best CMake reference I’ve found, yours is the best CMake tutorial I’ve found. Thank you!

    Reply
    • Jeremi

      Tank you for the kind words!

      Reply

Submit a Comment

Your email address will not be published. Required fields are marked *

Share This