Skip to Content

How to Avoid Undefined Symbols Errors by Propagating Function Definitions in CMake Target Library to Target that Links to It

  • The article explains how to propagate the function definitions from one CMake target library to another target that links to it, which can help avoid undefined symbols errors at link time.
  • The article presents two options for doing this: using PUBLIC or INTERFACE keyword in target_link_libraries, or using target_link_libraries on the final target.

CMake is a powerful and popular tool for managing the build process of software projects. It allows developers to specify the dependencies, options, and commands for compiling and linking their code in a platform-independent way. However, CMake can also be tricky to use, especially when dealing with complex scenarios involving multiple targets and libraries.

In this article, we will explore one such scenario: how to propagate the function definitions in one CMake target library to the target that links to it. This is useful when you have a library that defines some functions that are used by another library or executable, and you want to avoid getting undefined symbols errors at link time.

How to Avoid Undefined Symbols Errors by Propagating Function Definitions in CMake Target Library to Target that Links to It

The Problem

Let’s say you have a project structure like this:

project/
  lib/
    CMakeLists.txt
    lib.cpp
    lib.h
    sublib/
      CMakeLists.txt
      sublib.h
      sublib.cpp
  test/
    CMakeLists.txt
    test.cpp

The lib directory contains a library target called lib, which depends on another library target called sublib defined in the sublib directory. The test directory contains an executable target called test, which depends on lib.

The lib.cpp file contains a function that calls another function defined in sublib.cpp:

// lib.cpp
#include "lib.h"
#include "sublib.h"

void Lib::process() {
  Sublib::process();
}

The test.cpp file contains a function that calls the function defined in lib.cpp:

// test.cpp
#include "lib.h"

int main() {
  Lib::process();
  return 0;
}

The CMakeLists.txt files are as follows:

# project/lib/CMakeLists.txt
add_subdirectory(sublib)
add_library(lib lib.cpp)
target_link_libraries(lib sublib)
# project/lib/sublib/CMakeLists.txt
add_library(sublib sublib.cpp)
target_link_libraries(sublib some_other_libs_like_thrift)
# project/test/CMakeLists.txt
add_executable(test test.cpp)
add_test(test test)
target_link_libraries(test lib)

You might expect this to work fine, since you have specified the dependencies between the targets using target_link_libraries. However, when you try to build the project, you get errors like this:

Undefined symbols for architecture x86_64:
  "Sublib::process()", referenced from:
      Lib::process() in lib.cpp.o
ld: symbol(s) not found for architecture x86_64

What is going on here? Why is the linker not able to find the definition of Sublib::process()?

The Solution

The problem is that CMake does not automatically propagate the function definitions from one target library to another target that links to it. In other words, when you link test to lib, CMake does not tell the linker to also look for symbols in sublib or any other libraries that lib depends on.

To fix this, you need to explicitly tell CMake to propagate the function definitions from one target library to another target that links to it. There are two ways to do this:

Option 1: Use PUBLIC or INTERFACE keyword in target_link_libraries

One way to propagate the function definitions from one target library to another target that links to it is to use the PUBLIC or INTERFACE keyword in the target_link_libraries command. These keywords specify the scope of the link dependencies for a target.

According to the CMake documentation, the meaning of these keywords are as follows:

  • PRIVATE: Dependencies specified as PRIVATE are linked only into the consuming target itself. They are not propagated as usage requirements for other targets.
  • PUBLIC: Dependencies specified as PUBLIC are linked into both the consuming target itself and any targets that link to it. They are propagated as usage requirements for other targets.
  • INTERFACE: Dependencies specified as INTERFACE are not linked into the consuming target itself, but they are propagated as usage requirements for any targets that link to it.

In our example, we want to propagate the function definitions from sublib to both lib and test. Therefore, we need to use either PUBLIC or INTERFACE keyword when linking sublib to lib. For example:

# project/lib/CMakeLists.txt
add_subdirectory(sublib)
add_library(lib lib.cpp)
target_link_libraries(lib PUBLIC sublib) # use PUBLIC or INTERFACE here

This will tell CMake to add -lsublib (or equivalent) to both the link command for lib and the link command for any target that links to lib, such as test. This way, the linker will be able to find the definition of Sublib::process().

Note that using PUBLIC or INTERFACE keyword also affects the include directories of the targets. If you use PUBLIC or INTERFACE keyword, CMake will also propagate the include directories of sublib to both lib and test. This means that you don’t need to use target_include_directories to specify the include directories of sublib for lib or test. However, if you use PRIVATE keyword, CMake will not propagate the include directories of sublib to any target, and you will need to use target_include_directories to specify them manually.

Option 2: Use target_link_libraries on the final target

Another way to propagate the function definitions from one target library to another target that links to it is to use target_link_libraries on the final target that needs them. In other words, instead of linking sublib to lib, you can link both sublib and lib to test. For example:

# project/test/CMakeLists.txt
add_executable(test test.cpp)
add_test(test test)
target_link_libraries(test lib sublib) # link both lib and sublib here

This will tell CMake to add -llib -lsublib (or equivalent) to the link command for test. This way, the linker will be able to find the definition of Sublib::process().

Note that this option does not affect the include directories of the targets. You still need to use target_include_directories to specify the include directories of sublib for lib or test.

Frequently Asked Questions

Question: What is the difference between PRIVATE, PUBLIC, and INTERFACE keywords in CMake?

Answer: These keywords specify the scope of the dependencies for a target. PRIVATE dependencies are only used by the target itself, and are not propagated to other targets. PUBLIC dependencies are used by both the target itself and any targets that link to it, and are propagated as usage requirements for other targets. INTERFACE dependencies are not used by the target itself, but are propagated as usage requirements for any targets that link to it.

Question: How can I check what libraries are linked to a target in CMake?

Answer: You can use the get_target_property command to get the value of the LINK_LIBRARIES property of a target. This property contains a list of libraries that are linked to the target. For example:

get_target_property(LIBS test LINK_LIBRARIES)
message(STATUS "Libraries linked to test: ${LIBS}")

This will print something like:

-- Libraries linked to test: lib;sublib

Question: How can I avoid hard-coding library names or paths in CMake?

Answer: You can use find_package or find_library commands to locate external libraries on your system. These commands will try to find the libraries using standard paths and environment variables, and will set some variables or targets that you can use in your CMake code. For example:

find_package(Boost REQUIRED COMPONENTS filesystem)
target_link_libraries(test Boost::filesystem) # use imported target

or

find_library(THIRDPARTY_LIB thirdparty)
target_link_libraries(test ${THIRDPARTY_LIB}) # use variable

Conclusion

In this article, we have learned how to propagate the function definitions from one CMake target library to another target that links to it. We have seen two options: using PUBLIC or INTERFACE keyword in target_link_libraries, or using target_link_libraries on the final target. Both options have their pros and cons, and you should choose the one that suits your needs best.

We hope this article has been helpful for you. If you have any questions or feedback, please feel free to leave a comment below.

Disclaimer: The purpose of this article is to provide an informative and comprehensive explanation of the problem and solution, not to copy or plagiarize from other sources. The author has tried to cite all relevant sources and give credit where due, but if you find any errors or omissions, please let us know in the comments.