Getting Started with CMake - 4 - PUBLIC/PRIVATE/INTERFACE
When using CMake to develop large C/C++ projects, it’s common for us to split the code base into separate libraries, and later combine those libs into a final executable, also it’s common to use 3rdparty dependencies. By using CMake, we can config those libraries and add them via commands like target_include_directories, target_link_libraries.
And if we check the document for these commands, we can see the prevalent usage of the keyword INTERFACE/PUBLIC/PRIVATE
. For example,
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
What does the keyword INTERFACE/PUBLIC/PRIVATE
do in such commands? In this article, we are going to find out.
Where should I use these?
The INTERFACE/PUBLIC/PRIVATE
(keywords for short) is most commonly found in the following commands (not an exhaustive list)
- target_include_directories
- target_link_libraries
- target_link_options
- target_compile_definitions
- target_compile_options
These commands are used to compile/link a target(be it an executable or library, assume we are building a library targetBase
). And the keywords play an important role in how this targetBase
can be used by others. In other words, the keywords do 2 jobs
- Tell CMake how to build
targetBase
- Tell other targets that use
targetBase
(viatarget_link_libraries
), how to handle the include directories/compile options/etc.
Keyword effects
Let’s focus on one particular command target_include_directories
, and see how different keywords affect the 2 jobs we mentioned above.
Keywords behave the same in other commands, so understand this one and you will understand the rest.
Here’s what each keyword does, we will examine their effect and talk about how it works under the hood later. In the context of target_include_directories
, "Added" and "Not added" refer to whether the given directories are added to the #include
search path or not.
Keyword | Effect to current target | Effect to other targets that use this target |
---|---|---|
PUBLIC | Added | Added |
PRIVATE | Added | Not added |
INTERFACE | Not added | Added |
Example project
To better illustrate the effects of keywords, let’s create an example project, consist of 1 executable, built from 1 library.
Library
In this example library, let’s create a simple int add(int, int)
method
libA/libA.h
#pragma once
int add(int, int);
libA/libA.c
#include <libA.h> // intentionally use <> here
int add(int a, int b) {
return a + b;
}
libA/CMakeLists.txt
add_library(libA STATIC libA.c)
target_include_directories(libA PUBLIC "./")
Executable
In the final executable, simply call the libA’s add method.
main.c
#include <stdio.h>
#include <libA.h>
int main() {
printf("%d\n", add(1, 1));
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(demo)
add_subdirectory(libA)
add_executable(demo main.c)
target_link_libraries(demo PRIVATE libA)
Now, try it yourself and try to build the project with different keywords, there are 3x3=9 combinations of keywords. Most of them would fail to compile.
Why only PUBLIC for libA and PUBLIC/PRIVATE for demo works?
If you play around a bit, you will notice the program will only compile given
target_include_directories(libA PUBLIC …
target_link_libraries(demo PUBLIC/PRIVATE libA)
Now, refer to the table of keywords effects above, and see if you can figure out why.
Why does libA need PUBLIC?
To build libA, we need to add /.
(current directory) to #include
search path. And if any other target wishes to use libA, they need to know this search path to look for libA.h
as well.
Looking at our effects table above, only PUBLIC
keyword does both.
We explicitly use
#include <libA.h>
inlibA.c
so it will only compile if search path is correctly configured.
If we use#include "libA.h"
, obviously it will always find the header file regardless of keywords. This is the most common usage in the real world, but for the purpose of learning I intentionally used the first one.
Why can’t demo use INTERFACE?
To build demo
executable, we need to know the search path for libA.h
as we use #include <libA.h>
in main.c
.
Looking at our effects table, INTERFACE
keyword will not add related info when building the current target, so demo
target doesn’t know where to find libA.h
.
How does it work under the hood?
From the effect table, you could probably guess that each keyword affects 2 variables: one for building the current target, the other for building other targets that use the current target.
That’s true, let’s see how CMake implements such behavior. Again, let’s focus on target_include_directories
.
It turns out there are 2 variables (actually, property of target, to be more precise) of our interest here:
INCLUDE_DIRECTORIES
INTERFACE_INCLUDE_DIRECTORIES
Let’s update the effect table with 2 properties:
Keyword | INCLUDE_DIRECTORIES | INTERFACE_INCLUDE_DIRECTORIES |
---|---|---|
PUBLIC | Added | Added |
PRIVATE | Added | Not added |
INTERFACE | Not added | Added |
When building a target, the final include directories (search paths) will be constructed by combining:
- The
INCLUDE_DIRECTORIES
of current target - The combined
INTERFACE_INCLUDE_DIRECTORIES
of every target this target links with.
For our demo project, that means the final include directories is combined from:
- We didn't use
target_include_directories
on targetdemo
, so emptyINCLUDE_DIRECTORIES
- We link with
libA
, it gotINTERFACE_INCLUDE_DIRECTORIES="./"
Combining 1 and 2, we got a search path of /xxx/playground/libA/./
when compiling demo
.
How to check these 2 properties?
We can print out these properties, just to be sure.
libA/CMakeLists.txt
add_library(libA STATIC libA.c)
target_include_directories(libA PUBLIC "./")
# Get the target libA's property INCLUDE_DIRECTORIES, add assign to variable libA_INCLUDE_DIRS
get_target_property(libA_INCLUDE_DIRS libA INCLUDE_DIRECTORIES) # or INTERFACE_INCLUDE_DIRECTORIES
message("libA_INCLUDE_DIRS: ${libA_INCLUDE_DIRS}")
Output
libA_INCLUDE_DIRS: /xxx/playground/libA/./
Empty INCLUDE_DIRECTORIES for target demo?
If you try to get INCLUDE_DIRECTORIES
or INTERFACE_INCLUDE_DIRECTORIES
for the executable target demo
, you will see these 2 properties not found.
# You would get nothing
get_target_property(demo_INCLUDE_DIRS demo INCLUDE_DIRECTORIES)
You would probably expect them to be /xxx/playground/libs/libA/./
.
Remember, INCLUDE_DIRECTORIES
and INTERFACE_INCLUDE_DIRECTORIES
are properties local to the specific target. They are modified only by the developer's target_include_directories
command. Later during config time, CMake will read these properties and generate the final search path, but that value will be stored elsewhere, and we shouldn't worry about it.
Reference
https://kubasejdak.com/modern-cmake-is-like-inheritance
https://leimao.github.io/blog/CMake-Public-Private-Interface/