CMake Fundamentals Part 4

by | Feb 8, 2021 | CMake | 2 comments

In the previous two parts of the series, I introduced quite a few new concepts. In this part instead of introducing yet more new features, I illustrate some practical applications of those already discussed. I will show examples of using PRIVATE, INTERFACE and PUBLIC specifiers well. While doing so I also return to the closely related concept of modular design, briefly mentioned in the previous post. This will further grow our example project, preparing us for future discussions.

Project structure

We will start by extending our project to facilitate the discussion. Let’s say that the Add library does not quite meet our needs any longer, and we’d like to build it up to a fully featured mathematical library. We remember that we have some old C code that handles subtracting somewhere, and decide to reuse it – we will create a subtract directory and place the following files within it.
subtract.h:

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

int subtract_i(int l, int r)
{
    return l - r;
}

#ifdef __cplusplus
}
#endif

subtract.c:


#include "subtract.h"

int subtract_i(int l, int r)
{
    return l - r;
}

CMakeLists.txt

.
├── add
├── maths
├── subtract
├── CMakeLists.txt
└── main.cpp

The code isn’t up to our standards so we’d like to wrap our Add and Subtract libraries to align the interfaces and ensure consistent error-handling across the entire library. Thus we create a Maths library, again with a dedicated maths directory. As we begin writing the Maths library, we face an issue. The directory tree currently looks as follows:

.
├── add
├── maths
├── subtract
├── CMakeLists.txt
└── main.cpp

So far we’ve been relying on relative paths to include appropriate headers. This didn’t seem so bad when dealing just with the Add library, but here, to include the Add and Subtract libraries from within the maths directory, we would need to go up a directory, like so:

#include "../add/add.h"
#include "../subtract/subtract.h"

Now it should be obvious that this is a sign of an issue. We’re relying on relative directory paths instead of on our build system to handle the dependencies. This can be seen as equivalent to breaking encapsulation or relying on implementation details in object-oriented code. Let’s fix this issue by modularising our project.

Targets as modules

Relying on the relative directory structure is bad for the same reasons relying on implementation details is. We are no longer free to change anything about the Add library without affecting the its consumers. Instead, we’d like to have a clearly defined interface that could be relied on, without depending on details like relative directory layout. That way it wouldn’t matter where the add directory is located in relation to the maths directory – it wouldn’t even need to be part of the same project, and could be provided by a package manager instead, as long as it exposed CMake targets that could be consumed by the Maths library. This is exactly what CMake allows for.

What is the interface of a library, or more precisely, a module? From the point of view of structuring and building a project, it’s the include paths, compiler flags required to build the code, and potential defines that control how the library works. All of these things can and should be, handled by the build system. I would argue that this is the correct level of abstraction to handle these details – it should not be done in source code, in form of relative #include‘s, or conditional #define‘s, but rather, in the build system.

We already know how to handle linking the library with CMake. Let’s now look at the remaining aspects of defining a target as a module. CMake allows us to directly specify the remaining mentioned aspects of a well-defined target – a module, using dedicated commands. This of course is implemented in terms of properties. The table below shows exactly which commands are used to configure which aspects of a module, and do so by modifying which properties.

Module aspect Command Properties
Include paths target_include_directories INCLUDE_DIRECTORIES, INTERFACE_INCLUDE_DIRECTORIES
Compiler flags target_compile_options COMPILE_OPTIONS, INTERFACE_COMPILE_OPTIONS
Defines target_compile_definitions COMPILE_DEFINITIONS, INTERFACE_COMPILE_DEFINITIONS

You should be able to see the pattern here. All of these commands accept the same PRIVATE, INTERFACE and PUBLIC keywords that target_link_libraries does, which affects the respective properties in exactly the same way.

For now we will focus only on the include directories – that’s all we currently need to define in order to make our library targets into well-behaved modules. This is actually fairly straightforward. In the Add library’s CMakeLists.txt add the following command:

target_include_directories(Add
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

We see a new variable here – CMAKE_CURRENT_SOURCE_DIR – it points to the current directory – as could probably be inferred from the name. More specifically this is the directory entered after most recent call to add_subdirectory. The command itself will append this directory to both INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES, which in turn will cause this directory to be appended as -I include directories to the compiler call – thus allowing to find #include‘ed headers relative to these directories. This allows us to include the Add library headers using absolute, rather than relative, includes:

#include <calc_status.h>
#include <add.h>

This works, and it’s the simplest possible solution. But is it exactly what we want? Looking at the #include directives we have no notion of what library this header comes from. In a smaller project this is likely fine, however, in larger codebases, or libraries that will be reused extensively, we might want to introduce something akin to a namespace, so that the #includes look as follows:

#include <add/calc_status.h>
#include <add/add.h>

It is now clear, that these headers come from the Add library. Achieving this is very simple – create another subdirectory named add within the add library directory, and move the headers there, so that the directory tree looks as follows:

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

The subdirectory may seem redundant, however, this is what directly allows us to reach our goal. It is actually not an ideal setup, but it’s sufficient for our current needs, so in order not to complicate matters further let’s keep it.

We need to update the add_library call (this would not be necessary if we chose not to list the headers in the declaration):

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

We also need to fix the #includes within the library itself:
add/add.cpp:

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

We apply the same treatment to the Subtract library

.
├── subtract
│   ├── CMakeLists.txt
│   ├── subtract
│   │   └── subtract.h
│   └── subtract.c

subtract/CMakeLists.txt:

add_library(Subtract
    subtract/subtract.h
    subtract.c
)
target_include_directories(Subtract
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

subtract/subtract.cpp:

#include "subtract/subtract.h"
...

We have modularized our dependencies – the Maths library can now rely strictly on the interfaces provided by CMake targets, and not on relative directory layout or other implicit properties. The Maths library can now include its dependencies using absolute paths:

#include <add/calc_status.h>
#include <add/add.h>
#include <subtract/subtract.h>

Now that we have learned the basics of modularization, let’s return to defining the Maths library.

Hiding and propagating dependencies

We already created the maths library directory. Let’s populate it with source code and build it up into a module – the same way we did with the Add and Subtract libraries. As a brief reminder – our task is to wrap the Add and Subtract libraries to ensure consistent interfaces. We also make a decision to reuse the error-handling strategy and error-codes from the Add library (which might be questionable, but it serves to illustrate a point). As a result the maths/code> header file exposes the following interface:

#pragma once

#include <add/calc_status.h>

namespace Maths
{
using Add::calc_status;

int add(int l, int r, calc_status& cs) noexcept;
int subtract(int l, int r, calc_status& cs) noexcept;

} // namespace math

Note that since we’re directly reusing types from the Add library – here the calc_status enum class, we need to include the add/calc_status.h header here. This exposes the users of our library to the Add dependency.

The maths.cpp implements the declared functions as follows:

#include "maths/maths.h"

#include <add/add.h>
#include <subtract/subtract.h>

#include <limits>

int Maths::add(int l, int r, calc_status& cs) noexcept
{
    return Add::add(l, r, cs);
}

int Maths::subtract(int l, int r, calc_status& cs) noexcept
{
    if ((l > 0) && (r < (std::numeric_limits<int>::min() + l)))
    {
        cs = calc_status::positive_overflow;
        return 0;
    }
    else if ((l < 0) && (r > (std::numeric_limits<int>::max() + l)))
    {
        cs = calc_status::negative_overflow;
        return 0;
    }
    cs = calc_status::success;
    return ::subtract_i(l, r);
}

The implementation directly reuses both the Add and Subtract libraries code. Since the Maths library delegates the work to its dependencies it will certainly need to link them. We have a choice to make regarding both dependencies – which transitivity specifier do we use? This can be reasoned about quite simply. At the highest level of abstraction we could ask ourselves the following question:

“Is the user of my library Foo exposed to its dependency Bar?

Or a little more concretly:

“Does my library Foo expose any types, or other symbols, declared by dependency Bar in its public interface?”

Or simplifying a little:

“Does my library Foo directly include headers from dependency Bar in its public headers?

If an answer to any of these questions is “Yes” than we use the PUBLIC transitivity specifier. Otherwise – if the use of a dependency is fully hidden from the user – dependency headers are included only in the implementation .cpp files, we use the PRIVATE specifier.

When in doubt – prefer PRIVATE, and only consider PUBLIC when necessary.

Now in our concrete example. The Maths library uses both the Add and Subtract libraries in its implementation .cpp files. A type defined in the Add library is also reused, and the add/calc_status.h header is included in the maths.h header. This means that the PUBLIC specifier needs to be used for the Add dependency. It is sufficient to use the PRIVATE specifier for the Subtract dependency since its use is fully encapsulated. The maths/CMakeLists.txt listing therefore contains the following code:

target_link_libraries(Maths
    PUBLIC
        Add
    PRIVATE
        Subtract
)

What about the INTERFACE specifier? This is a case for dependencies we’d like to propagate to the users, but don’t use ourself in our implementation code. The prepared example does not illustrate this, but imagine a dependency that the Maths library only #includes in its headers, but doesn’t use anywhere in the .cpp implementation files. This is one case where the INTERFACE specifier could be used. Another, and likely more common use case that will further solidify the understanding of properties will be discussed in a future blog post.

We’re almost there. The Maths library could be built now. However, it can not be considered a module yet – one more command is necessary. We should already be familiar with it:

target_include_directories(Maths
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}
)

And that’s it. The Maths library is now a module with a clearly defined interface, that correctly propagates its dependencies. We use the library we just developed in the same example previously used to demonstrate the Add library.
main.cpp:

#include <iostream>
#include <limits>

#include <maths/maths.h>

void use_add(int a, int b)
{
    Maths::calc_status cs{};
    auto const result = Maths::add(a, b, cs);
    if (cs == Maths::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};

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

The top-level CMakeLists.txt looks as follows:

cmake_minimum_required(VERSION 3.19)
project(Fundamentals)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_VERBOSE_MAKEFILE ON)

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()

add_subdirectory(add)
add_subdirectory(subtract)
add_subdirectory(maths)

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

Finally, let’s configure the example:

cmake --build build -j4
-- Before linking Main:
-- Main LINK_LIBRARIES: linkedLibs-NOTFOUND
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND
-- After linking Main:
-- Main LINK_LIBRARIES: Maths
-- Main INTERFACE_LINK_LIBRARIES: interfaceLibs-NOTFOUND

We see that only the Maths target is listed in the LINK_LIBRARIES property. What gives? Shouldn’t the Add target be listed as well, since the Maths specified it as a PUBLIC dependency? CMake evaluates the transitive dependencies lazily in the generation stage, that’s why public Maths dependencies are not listed here explicitly – but they will be linked as expected.

Let’s build the example:

$ cmake --build build
/usr/bin/cc  -I/home/user/cmake_fundamentals/subtract -o CMakeFiles/Subtract.dir/subtract.c.o -c /home/user/cmake_fundamentals/subtract/subtract.c
/usr/bin/ar qc libSubtract.a CMakeFiles/Subtract.dir/subtract.c.o
/usr/bin/ranlib libSubtract.a

/usr/bin/c++ -I/home/user/cmake_fundamentals/add -o CMakeFiles/Add.dir/add.cpp.o -c /home/user/cmake_fundamentals/add/add.cpp
/usr/bin/ar qc libAdd.a CMakeFiles/Add.dir/add.cpp.o
/usr/bin/ranlib libAdd.a

usr/bin/c++  -I/home/user/cmake_fundamentals/maths -I/home/user/cmake_fundamentals/add -I/home/user/cmake_fundamentals/subtract -o CMakeFiles/Maths.dir/maths.cpp.o -c /home/user/cmake_fundamentals/maths/maths.cpp
/usr/bin/ar qc libMaths.a CMakeFiles/Maths.dir/maths.cpp.o
/usr/bin/ranlib libMaths.a

/usr/bin/c++ -I/home/user/cmake_fundamentals/maths -I/home/user/cmake_fundamentals/add -o CMakeFiles/Main.dir/main.cpp.o -c /home/user/cmake_fundamentals/main.cpp

/usr/bin/c++ CMakeFiles/Main.dir/main.cpp.o -o Main  maths/libMaths.a add/libAdd.a subtract/libSubtract.a

Some relevant observations here:

  • The include paths have now been added to all of the compiler calls (-I…), due to our effort to modularize the code. We’re no longer reliant on relative paths, however the compilation commands are now significantly longer and less readable
  • Each of our library targets has been built into a static library, but the Maths library doesn’t acually add the Add and Subtract object code to the Maths archive. CMake doesn’t do so, since its aware of all of the dependencies – and since all of them are available it can refer to each individual archive to extract the object code to link the final Main target. This saves build time and space.

Summary

In this part of the CMake Fundamentals series, we modularized our example libraries and learned how to use transitivity specifiers to achieve that goal. This really just scratches the surface. There are many more considerations to be made when packaging, installing, and deploying library code comes into play. A different set of challenges arises when testing the code. And further more details need to be specified when building stable shared libraries is necessary. We will look at all of these important topics in the future. However, we’ve been rushing through many CMake features in these first four parts of the series in order to build up the example project to a reasonable level. Now that we have some context we slow down the pace a little to look at some important concepts in greater detail in a more atomic, bite-sized format.
 

References

2 Comments

  1. Pawel

    Great content!

    Reply
  2. LT

    Hi Jeremi,
    Thank you for your valuable posts. There are some points regarding project structure that I am not clear about, and I hope you can give some insights.

    Firstly, I want to extend your calculator project by adding a source file, namely ‘average.cpp’, which has #include . Now, in another project, I have to install another math library that has a path like this ‘include/maths/maths.h’. How can I resolve the name collision without renaming the libraries?

    Secondly, in the maths directory of the calculator project, I want to put some header-only files, how can I integrate them into the project?

    Best regards, LT

    Reply

Submit a Comment

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

Share This