CMake:静态库的传递依赖链接“就地”而不是附加

Net*_*oBF 5 c++ pimpl-idiom cmake

标题可能有点太短,不够清晰。

我们有一个复杂的 C/C++ 项目,它是在许多单独的目标中作为静态库构建和链接的。所以我的问题是 target_link_libraries 进行传递链接,但顺序错误。我需要另一个订单,因为我们使用 PIMPL 并且实现没有任何要包含的标头。它们只包含定义 PIMPL 标头并声明 Impl 对象的库的标头。但为了正确链接,它们需要链接在为 Pimpl 提供标头的 lib 之后。这不会发生。

仅当我们使用用于单元测试框架的 gcc 编译器和链接器进行构建时,才会出现这种情况。对于我们的目标项目,我们使用 TI clang-toolchain,它提供了链接器选项 --reread_libs,可以自动解决此问题。据我所知,这在 gcc 中不可用。

现在更详细地介绍一下 Cmake。该项目太大,无法做一个最小的例子,但我会尝试描述问题并将其缩小为问题:

所以我们在一个模块中有多个静态库:

# static libs inside the platform
add_library(memutils memutils/ForwardDeclaredStorage.hpp)
target_include_directories(memutils INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/memutils)

set_target_properties(memutils PROPERTIES LINKER_LANGUAGE CXX)

# contains the Pimpl-Headers to use, here task and mutex as extract
add_library(osal osal/mutex.hpp osal/task.hpp osal/TaskManager.hpp osal/TaskManager.cpp) 
target_include_directories(osal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/osal)

target_link_libraries(osal PUBLIC memutils)

add_library(system sys/system.hpp sys/system.cpp)

target_link_libraries(system PUBLIC osal)

target_include_directories(system PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sys)
add_library(platformlib INTERFACE)
# example with INTERFACE keyword. Adding a dummy source and using PUBLIC does not change anything
target_link_libraries(platformlib INTERFACE osal system memutils)

# some extensions
add_library(additionslib add/additions.hpp add/additions.cpp )

target_link_libraries(additionslib PUBLIC platformlib)

target_include_directories(additionslib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/add)

# the Impl-Lib which needs the headers
add_library(gcc_impl STATIC impls/MutexImpl.cpp impls/TaskImpl.cpp)
# so the implementations get the headers
target_link_libraries(gcc_impl PUBLIC platformlib)

# the test-case. its a project!
project(tests)
add_executable(tests test.hpp test.cpp main.cpp )

# linking all the other libs, also the test-framework (gtest), PRIVATE, PUBLIC... doesnt matter. In this case the extensions will link the platformlib transitive.
target_link_libraries(tests PUBLIC additionslib gcc_impl)
Run Code Online (Sandbox Code Playgroud)

那么链接顺序是什么样的呢?CMake 生成的链接顺序结果是:

首先它就像一个“顶级链接”,之后所有传递库都遵循:

# static libs inside the platform
add_library(memutils memutils/ForwardDeclaredStorage.hpp)
target_include_directories(memutils INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/memutils)

set_target_properties(memutils PROPERTIES LINKER_LANGUAGE CXX)

# contains the Pimpl-Headers to use, here task and mutex as extract
add_library(osal osal/mutex.hpp osal/task.hpp osal/TaskManager.hpp osal/TaskManager.cpp) 
target_include_directories(osal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/osal)

target_link_libraries(osal PUBLIC memutils)

add_library(system sys/system.hpp sys/system.cpp)

target_link_libraries(system PUBLIC osal)

target_include_directories(system PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sys)
add_library(platformlib INTERFACE)
# example with INTERFACE keyword. Adding a dummy source and using PUBLIC does not change anything
target_link_libraries(platformlib INTERFACE osal system memutils)

# some extensions
add_library(additionslib add/additions.hpp add/additions.cpp )

target_link_libraries(additionslib PUBLIC platformlib)

target_include_directories(additionslib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/add)

# the Impl-Lib which needs the headers
add_library(gcc_impl STATIC impls/MutexImpl.cpp impls/TaskImpl.cpp)
# so the implementations get the headers
target_link_libraries(gcc_impl PUBLIC platformlib)

# the test-case. its a project!
project(tests)
add_executable(tests test.hpp test.cpp main.cpp )

# linking all the other libs, also the test-framework (gtest), PRIVATE, PUBLIC... doesnt matter. In this case the extensions will link the platformlib transitive.
target_link_libraries(tests PUBLIC additionslib gcc_impl)
Run Code Online (Sandbox Code Playgroud)

现在我们有问题。平台需要 gcc_impl 但它在平台之前链接。我需要它在平台之后链接。如果我更改生成的链接器命令并在最后一个链接中添加 impl,则它可以正常工作。如果不是,我会得到 impl 的未引用符号,因为它们那时就丢失了。

我尝试了很多。我在每个库中摆弄链接关键字 INTERFACE PUBLIC PRIVATE 。尝试通过生成器仅包含平台的标题,而不是直接链接它。我还尝试直接将具有标头的 osal 与 gcc-impl 链接起来:

target_link_libraries(tests PRIVATE additionslib osal gcc_impl)
Run Code Online (Sandbox Code Playgroud)

猜猜会发生什么?

libadditionslib.a 
libgcc_impl.a 
// now the transitive libs in order of top-level-linked libs
libsystem.a 
libosal.a 
libmemutils.a
Run Code Online (Sandbox Code Playgroud)

结果是错误仍然是相同的,因为系统 lib 还需要包含 osal 的平台,而 osal 需要在其后链接 impl。

但无论如何我都无法改变这个“级别”-链接顺序并强制 gcc_impl 最后发生。

我需要的是“就地”链接,而不是基于级别的链接:

target_link_libraries(tests PRIVATE additionslib osal gcc_impl)
Run Code Online (Sandbox Code Playgroud)

我怎样才能实现这最后一个目标?

我添加了一个产生此链接错误的最小示例(未定义的参考):https://godbolt.org/z/d19Wefrhx

您还可以看到该链接顺序:

/opt/compiler-explorer/gcc-11.3.0/bin/g++ -fdiagnostics-color=always -std=c++14 -O2 -g -L/app -Wl,-rpath,/app -Wl,-rpath,/opt/compiler-explorer/gcc-11.3.0/lib64 -Wl,-rpath,/opt/compiler-explorer/gcc-11.3.0/lib32 CMakeFiles/tests.dir/test.cpp.o CMakeFiles/tests.dir/main.cpp.o -o tests  libadditionslib.a libgcc_impl.a libsystem.a libosal.a libmemutils.a
Run Code Online (Sandbox Code Playgroud)

不幸的是我不能直接在 godbolt 中执行命令行。但我确信如果 libgcc_impl.a 再次出现作为最后一个它会起作用。至少这对我们来说正是如此。如何强制 CMake 最后链接 impl 库?

Kam*_*Cuk 4

因此,通常您需要做的就是与接口和实现进行两次链接。要么必须在库之间具有循环依赖关系(在这种情况下LINK_INTERFACE_MULTIPLICITY就会起作用),要么链接两次。

您可以通过将 system 或 osal 或 memutils 与 gcc_impl 链接来获得循环依赖。通常我会看到另一个抽象层:

target_link_libraries(osal PUBLIC memutils osal_impl)

# ...

set(CHOSEN_IMPLEMENTATION gcc)   # some logic
add_library(osal_impl INTERFACE)
target_link_libraries(osal_impl INTERFACE ${CHOSEN_IMPLEMENTATION}_impl)
Run Code Online (Sandbox Code Playgroud)

那么实现是抽象的,循环依赖会导致链接两次。

另一种解决方案是在链接最后阶段时明确并重复其中一个或另一个两次:

target_link_libraries(tests PUBLIC additionslib
  system gcc_impl system)
Run Code Online (Sandbox Code Playgroud)

您可能对嵌入式平台感兴趣的另一个解决方案是优化和使用 OBJECT 库。这意味着您必须列出链接阶段的所有依赖项,如下所示:

add_library(osal OBJECT osal/mutex.hpp osal/task.hpp osal/TaskManager.hpp osal/TaskManager.cpp) 
target_include_directories(osal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/osal)
target_link_libraries(osal PUBLIC memutils)

add_library(system OBJECT sys/system.hpp sys/system.cpp)
target_include_directories(system PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/sys)
target_link_libraries(system PUBLIC osal)

set(platformlib osal system memutils)

add_library(additionslib OBJECT add/additions.hpp add/additions.cpp )
target_include_directories(additionslib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/add)
target_link_libraries(additionslib PUBLIC ${platformlib})

add_library(gcc_impl OBJECT impls/MutexImpl.cpp impls/TaskImpl.cpp)
target_link_libraries(gcc_impl PUBLIC ${platformlib})

add_executable(tests test.hpp test.cpp main.cpp )
target_link_libraries(tests PUBLIC ${platformlib} additionslib gcc_impl)
Run Code Online (Sandbox Code Playgroud)