传递库链接中的未定义引用

Undefined reference in transitive library linking

我有一个大型库 B,它对配置对象 MY_CONFIG 执行一些操作。该库 link 是针对提供主要功能的更大的库 C 编写的。最后,C 针对可执行文件 main 进行了 linked。 配置对象 MY_CONFIG 对于每个计算单元都是唯一的。由于目标是为所有系统生成一个 C 库,我目前正在尝试生成一个仅包含定义的配置对象的附加库 A

换句话说,所有计算单元最终得到相同的C,但不同的A库。

然而,问题是,当我尝试 link main 对抗 CA 时,我得到一个 undefined reference linker 错误。 =35=]

让我们用一些代码来说明这个例子:

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)

如您所见,我在 B.cpp 中为 MY_CONFIG 使用了 extern 关键字。我的直觉是,一旦构建了可执行文件 main,它就解决了。由于MY_CONFIG的定义在A.cpp中,我也link主要针对A。编译时出现如下错误:

/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

如果我link用A 来对抗C,那么它工作得很好。但这是我想避免的事情,因为我需要为所有计算系统提供单独的 C。一个重要的旁注:我只能使用静态库(没有共享对象)。

我的意图通常是不可能的还是我错过了什么?

而不是

target_link_libraries(main C A)

你可以使用

target_link_libraries(main C B A)

这将在 linker 的命令行中的库之间产生正确的顺序。因此,您将克服“未定义引用”错误。

解释

B 使用符号(具体来说,变量)MY_CONFIG,它在库 A 中定义。因此,为了 link 成功生成可执行文件,您需要:

  1. 确保 A.a 在 link 用户的命令行中位于 B.a 之后。
  2. 使用选项,消除 linker 的排序敏感性。

您可以使用 -Wl,--whole-archive 选项按照第二种方式进行操作。参见例如.

为了遵循第一种方式,即确保库的 order 为 link,应该考虑到 CMake 提供了两种强制排序方式:

  1. 链接 库之间:

    target_link_libraries(B A)
    

    这是最方便和可扩展的方式。它确保在 linker 的命令行中 A.a 将在 last B.a.

    之后

    但是这个link年龄不适合你的目的。

  2. target_link_libraries 中的库顺序。例如

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

    将确保 至少一个 B 后跟 A

    这种方式比第一种方式 保证 少。例如。该命令不会阻止 CMake 生成 linker 命令行 <...> B.a A.a B.a <...>,这将再次导致未定义的引用错误。但是 CMake 不会在没有需要的情况下在命令行中复制库。

例如,而不是

target_link_libraries(main C A)

你可以使用

target_link_libraries(main C B A)

因此 linker 命令行将完全按照以下顺序包含库:

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

请注意,在给定的命令行中 B.a 跟在 C.a 之后,这与您 CMakeLists.txt 中的另一个 link 年龄相关:

target_link_libraries(C B)

所以 CMake 不会再向其中添加 B.a


link年龄

target_link_libraries(C B)

你目前在你的代码中只有确保B.a会在linker命令行中某个地方之后C.a

有额外的

target_link_libraries(main C A)

您可以预期 CMake 会生成正确的命令行

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

满足link年龄。

但是完全允许 ​​CMake 生成另一个命令行:

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

其中 B.a 没有紧跟在 C.a 之后。

在您的案例中,正是该命令行导致了“未定义引用”错误。

CMake 对静态库之间的循环依赖没有问题。只需声明实际的依赖关系,CMake 就会处理其余的事情。这是我修改后的 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)

使用您的确切来源,我可以像这样 运行 构建:

$ 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

如您所见,最后的 link 行生成 C B A B A。后两者不会造成任何伤害,因为它们实际上永远不会被搜索。


如果想打破这个循环,可以把B中A需要的那部分拆成另一个库,让两者都依赖。

现在我有以下文件:

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

然后像这样调整构建:

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)

并且当我们构建时,我们看到 link 顺序是正确的并且重复的库已经消失:

$ 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 && :