Undefined reference in transitive library linking

I have a large library B that performs some operations on a configuration object MY_CONFIG. This library is linked against an even larger library C that provides the main functionality. In the end, C is linked against the executable main. The configuration object MY_CONFIG is unique for each computing unit. Since the goal is to produce one C library for all systems, I am currently trying to produce an additional library A that only contains the defined configuration object.

In other words, all computing units eventually receive the same C, but different A libraries.

The problem is, however, that I get an undefined reference linker error when trying to link main against C and A.

Let's illustrate the example with some code:

A.cpp

#include "B.hpp"

Config MY_CONFIG = {
    .id=2
};

B.hpp

int get_config_id ();

typedef struct Config {
    int id;
} Config;

B.cpp

#include "B.hpp"

extern Config MY_CONFIG;

int get_config_id () {
    Config work = MY_CONFIG;
    return work.id;
}

C.hpp

void print_config();

C.cpp

#include "B.hpp"
#include <iostream>

void print_config() {
    std::cout << "Config ID: " << get_config_id() << std::endl;
}

main.cpp

#include <iostream>
#include "C.hpp"

int main() {
    
    print_config();
    
    return 0;
}

CMakeLists.txt

project(config)
cmake_minimum_required(VERSION 3.16)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pthread")


add_library(A STATIC A/A.cpp)
target_include_directories(A PUBLIC B)

add_library(B STATIC B/B.cpp)
target_include_directories(B PUBLIC B)

add_library(C STATIC C/C.cpp)
target_include_directories(C PUBLIC C)
target_link_libraries(C B)


add_executable(main main.cpp)

# Apparently, this is not possible. But why?
target_link_libraries(main C A)

As you can see, I use the extern keyword for MY_CONFIG in B.cpp. My intuition was that it is resolved once building the executable main. Since the definition of MY_CONFIG is in A.cpp, I also link main against A. The following error shows up during compilation:

/usr/bin/ld: libB.a(B.cpp.o): in function `get_config_id()':
B.cpp:(.text+0xa): undefined reference to `MY_CONFIG'
collect2: error: ld returned 1 exit status

If I linked A against C, then it works perfectly fine. But this is something that I want to avoid because I'd need to deliver individual C's for all computing systems. An important side note: I can only work with static libraries (no shared objects).

Is my intention generally not possible or what do I miss?


Solution 1:

CMake has no issue with circular dependencies between static libraries. Just declare the actual dependencies and CMake will take care of the rest. Here is my revised CMakeLists.txt:

cmake_minimum_required(VERSION 3.22)
project(config)

set(CMAKE_CXX_STANDARD 11)

add_library(A STATIC A/A.cpp)
target_include_directories(A PUBLIC "${CMAKE_CURRENT_LIST_DIR}/A")
target_link_libraries(A PRIVATE B)

add_library(B STATIC B/B.cpp)
target_include_directories(B PUBLIC "${CMAKE_CURRENT_LIST_DIR}/B")
target_link_libraries(B PRIVATE A)

add_library(C STATIC C/C.cpp)
target_include_directories(C PUBLIC "${CMAKE_CURRENT_LIST_DIR}/C")
target_link_libraries(C PRIVATE B)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE C)

Using your exact sources, I can build and run like so:

$ cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release
...
$ cmake --build build --verbose
[1/8] /usr/bin/c++  -I/path/to/B -I/path/to/A -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/B.dir/B/B.cpp.o -MF CMakeFiles/B.dir/B/B.cpp.o.d -o CMakeFiles/B.dir/B/B.cpp.o -c /path/to/B/B.cpp
[2/8] /usr/bin/c++  -I/path/to/A -I/path/to/B -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/A.dir/A/A.cpp.o -MF CMakeFiles/A.dir/A/A.cpp.o.d -o CMakeFiles/A.dir/A/A.cpp.o -c /path/to/A/A.cpp
[3/8] : && /usr/bin/cmake -E rm -f libB.a && /usr/bin/ar qc libB.a  CMakeFiles/B.dir/B/B.cpp.o && /usr/bin/ranlib libB.a && :
[4/8] : && /usr/bin/cmake -E rm -f libA.a && /usr/bin/ar qc libA.a  CMakeFiles/A.dir/A/A.cpp.o && /usr/bin/ranlib libA.a && :
[5/8] /usr/bin/c++  -I/path/to/C -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /path/to/main.cpp
[6/8] /usr/bin/c++  -I/path/to/C -I/path/to/B -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/C.dir/C/C.cpp.o -MF CMakeFiles/C.dir/C/C.cpp.o.d -o CMakeFiles/C.dir/C/C.cpp.o -c /path/to/C/C.cpp
[7/8] : && /usr/bin/cmake -E rm -f libC.a && /usr/bin/ar qc libC.a  CMakeFiles/C.dir/C/C.cpp.o && /usr/bin/ranlib libC.a && :
[8/8] : && /usr/bin/c++ -O3 -DNDEBUG  CMakeFiles/main.dir/main.cpp.o -o main  libC.a  libB.a  libA.a  libB.a  libA.a && :
$ ./build/main
Config ID: 2

As you can see, the final link line produces C B A B A. The latter two don't hurt anything since they'll never actually be searched.


If you want to break the cycle, you can break off the part of B that A needs into another library and make both of them depend on it.

Now I have the following files:

// A/A.cpp
#include "common.hpp"

Config MY_CONFIG = {
    .id=2
};
// common/common.hpp

typedef struct Config {
    int id;
} Config;
// B/B.cpp

#include "B.hpp"
#include "common.hpp"

extern Config MY_CONFIG;

int get_config_id () {
    Config work = MY_CONFIG;
    return work.id;
}
// B/B.hpp

int get_config_id ();

The build is then adjusted like so:

cmake_minimum_required(VERSION 3.22)
project(config)

set(CMAKE_CXX_STANDARD 11)

add_library(common INTERFACE)
target_include_directories(common INTERFACE "${CMAKE_CURRENT_LIST_DIR}/common")

add_library(A STATIC A/A.cpp)
target_include_directories(A PUBLIC "${CMAKE_CURRENT_LIST_DIR}/A")
target_link_libraries(A PRIVATE common)

add_library(B STATIC B/B.cpp)
target_include_directories(B PUBLIC "${CMAKE_CURRENT_LIST_DIR}/B")
target_link_libraries(B PRIVATE common A)

add_library(C STATIC C/C.cpp)
target_include_directories(C PUBLIC "${CMAKE_CURRENT_LIST_DIR}/C")
target_link_libraries(C PRIVATE B)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE C)

and when we build, we see that the link order is correct and the duplicated libraries have disappeared:

$ cmake --build build --verbose
[1/8] /usr/bin/c++  -I/path/to/A -I/path/to/common -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/A.dir/A/A.cpp.o -MF CMakeFiles/A.dir/A/A.cpp.o.d -o CMakeFiles/A.dir/A/A.cpp.o -c /path/to/A/A.cpp
[2/8] /usr/bin/c++  -I/path/to/B -I/path/to/common -I/path/to/A -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/B.dir/B/B.cpp.o -MF CMakeFiles/B.dir/B/B.cpp.o.d -o CMakeFiles/B.dir/B/B.cpp.o -c /path/to/B/B.cpp
[3/8] : && /usr/bin/cmake -E rm -f libA.a && /usr/bin/ar qc libA.a  CMakeFiles/A.dir/A/A.cpp.o && /usr/bin/ranlib libA.a && :
[4/8] : && /usr/bin/cmake -E rm -f libB.a && /usr/bin/ar qc libB.a  CMakeFiles/B.dir/B/B.cpp.o && /usr/bin/ranlib libB.a && :
[5/8] /usr/bin/c++  -I/path/to/C -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/main.dir/main.cpp.o -MF CMakeFiles/main.dir/main.cpp.o.d -o CMakeFiles/main.dir/main.cpp.o -c /path/to/main.cpp
[6/8] /usr/bin/c++  -I/path/to/C -I/path/to/B -O3 -DNDEBUG -std=gnu++11 -MD -MT CMakeFiles/C.dir/C/C.cpp.o -MF CMakeFiles/C.dir/C/C.cpp.o.d -o CMakeFiles/C.dir/C/C.cpp.o -c /path/to/C/C.cpp
[7/8] : && /usr/bin/cmake -E rm -f libC.a && /usr/bin/ar qc libC.a  CMakeFiles/C.dir/C/C.cpp.o && /usr/bin/ranlib libC.a && :
[8/8] : && /usr/bin/c++ -O3 -DNDEBUG  CMakeFiles/main.dir/main.cpp.o -o main  libC.a  libB.a  libA.a && :

Solution 2:

Instead of

target_link_libraries(main C A)

you could use

target_link_libraries(main C B A)

which will produce correct order between the libraries in the linker's command line. So you will overcome the "undefined reference" error.

Explanations

The library B uses symbol (specifically, a variable) MY_CONFIG, which is defined in the library A. So, for link the resulted executable successfully, you need either:

  1. Ensure, that A.a comes after B.a in the linker's command line.
  2. Use options, which eliminates ordering-sensitivity for the linker.

You could follow the second way by using -Wl,--whole-archive option. See e.g. that question.

For following the first way, that is ensuring the order of the libraries to link, it should be taken into account that CMake provides two ways for enforce ordering:

  1. Linkage between the libraries:

    target_link_libraries(B A)
    

    This is most convenient and scalable way. It ensures that in the linker's command line A.a will come after the last B.a.

    But this linkage doesn't fit for your purpose.

  2. Order of libraries in target_link_libraries. E.g.

    target_link_libraries(main <...> B A <...>)
    

    will ensure, that at least one B will be followed with A.

    This way has lesser guarantee than the first one. E.g. that order doesn't prevent CMake from producing the linker command line <...> B.a A.a B.a <...>, which will again cause the undefined reference error. But CMake won't duplicate libraries in the command line without needs.

E.g., instead of

target_link_libraries(main C A)

you could use

target_link_libraries(main C B A)

So the linker command line will contain the libraries in exactly that order:

ld <...> C.a B.a A.a

Note, in the given command line B.a follows C.a, which correlates with another linkage you have in your CMakeLists.txt:

target_link_libraries(C B)

so CMake won't add another B.a into it.


The linkage

target_link_libraries(C B)

which you currently have in your code only ensures that B.a will come somewhere after C.a in the linker command line.

With additional

target_link_libraries(main C A)

you could expect that CMake produces correct command line

ld <...> C.a B.a A.a

which satisfies both linkages.

But CMake is perfectly allowed to produce another command line:

ld <...> C.a A.a B.a

where B.a does not immediately follow C.a.

Exactly that command line causes the "undefined reference" error in your case.