第十六章:高级链接

Posted by lili on

前一章介绍了通常用于控制目标链接的属性。讨论的属性和命令应该涵盖了大多数典型的场景。然而,会出现一些情况,项目可能希望使用附加的链接技术和约束,而这些技术和约束不在这些方法的范围之内。

16.1 要求链接的目标

CMake 3.23 版本增加了对 LINK_LIBRARIES_ONLY_TARGETS 目标属性的支持。当将其设置为 true 时,它根据其 LINK_LIBRARIES 属性以及其依赖项的 INTERFACE_LINK_LIBRARIES 和 INTERFACE_LINK_LIBRARIES_DIRECT 属性(有关后者,请参见第16.3节“向上传播直接链接依赖关系”)对添加到目标的链接项添加约束。这些属性中的任何项,如果可能是有效的目标名称,则必须是目标名称。对于此检查,如果一个项没有路径组件并且在任何生成器表达式被评估后不以 -、$ 或 ` 开头,则认为该项可能是一个有效的目标名称。

add_library(glob STATIC ...)
add_executable(App ...)
set_target_properties(App PROPERTIES
 LINK_LIBRARIES_ONLY_TARGETS TRUE
)
target_link_libraries(App PRIVATE glib)
# NOTE: typo here

在上面的例子中,开发者在 target_link_libraries() 行上打了一个错字。他们本打算使用 glob 目标的名称,但却使用了 glib。在某些平台上,系统可能提供了一个名为 glib 的库,可以在链接器的库搜索路径上找到。如果没有设置 LINK_LIBRARIES_ONLY_TARGETS 属性,链接器将找到 glib 并链接到它,但无法解析应由 glob 提供的符号。然后,链接器将发出有关缺少符号而不是链接到错误库的错误消息。开发者会感到困惑,认为他们链接到了 glob,它应该提供缺失的符号。将 LINK_LIBRARIES_ONLY_TARGETS 设置为 true 后,CMake 发现没有名为 glib 的 CMake 目标,并在配置时发生错误。CMake 的错误消息将立即诊断出真正的问题(导致链接错误的拼写错误)。

在实际应用中,可能希望在整个项目中启用此行为,而不仅仅是对个别目标。CMAKE_LINK_LIBRARIES_ONLY_TARGETS 变量用于初始化 LINK_LIBRARIES_ONLY_TARGETS 目标属性。在创建任何目标之前在项目的顶层设置该变量将确保所有目标都启用了该功能。

在某些情况下,工具链可能会使用裸名称链接提供的某些库,这可能是一个目标名称。在 Unix 平台上,m 库是一个相对常见的例子。工具链文件或项目可能会将这样的库添加到所有可执行文件和共享库目标的链接器命令行中。如果 LINK_LIBRARIES_ONLY_TARGETS 设置为 true,这可能导致 CMake 拒绝该库的链接关系。这些库需要包装在一个 INTERFACE IMPORTED 目标中(参见第18.2.5节“接口导入的库”)。还需要设置 IMPORTED_LIBNAME 目标属性,该属性也需要设置为在链接器命令行上使用的名称。以下是一个构想中的例子(摘自官方 CMake 文档,有小修改)演示了这种技术:

add_library(Toolchain::m INTERFACE IMPORTED)
set_target_properties(Toolchain::m PROPERTIES
 IMPORTED_LIBNAME "m"
)
target_link_libraries(App PRIVATE Toolchain::m)

CMake 不会检查 IMPORTED_LIBNAME 的值是否与目标名称匹配。该值直接用作链接器命令行上提供的值。可以利用这一点,使用与同名的 CMake 目标掩盖裸库名称:

# No Toolchain:: namespace, masks bare "m" name
add_library(m INTERFACE IMPORTED)
set_target_properties(m PROPERTIES
 IMPORTED_LIBNAME "m"
 # Never treated as a target name
)
target_link_libraries(App PRIVATE m)

然后,第三方代码可以在启用 LINK_LIBRARIES_ONLY_TARGETS 的情况下链接到裸的 m。

16.2. 自定义库的链接方式

在一些项目中,可能不仅有关于链接哪些库的需求,还有关于如何链接它们的需求。例如,一些链接器可能会优化掉它认为不需要的已链接库和框架。目标可能以链接器无法检测到的方式使用这些库或框架,因此项目必须以某种方式防止链接器丢弃它们。另一个例子是一组库之间存在复杂的相互依赖关系,无法通过 CMake 的循环依赖处理轻松捕获(参见第22.2节“链接静态库”)。在这种情况下,项目可能希望指导链接器重复扫描一组相互依赖的库以满足未解析的符号。

CMake 3.24 中引入的两个新生成器表达式提供了更多控制库如何添加到链接器命令行的方式。它们直接支持上述提到的场景。这两个新的生成器表达式以及围绕它们的各种约束和特性可能相当复杂。然而,对于更常见的用例,一个相当简单的理解可能就足够了。尽管如此,建议读者将以下讨论用作起点,但也要查阅这些生成器表达式的官方文档。大多数特性并非对每个工具链都受支持,因此请查阅文档以确认在使用的工具链(们)上是否可用,并了解可能适用的任何其他限制。

这两个新的生成器表达式密切相关,但它们有不同的用途:

$<LINK_GROUP:groupFeature,libs...>
$<LINK_LIBRARY:libFeature,libs...>

两个表达式只能在 LINK_LIBRARIES、INTERFACE_LINK_LIBRARIES 或 INTERFACE_LINK_LIBRARIES_DIRECT 目标属性中使用。它们也可以在 target_link_libraries() 和 link_libraries() 命令中使用。

当将指定的库添加到链接器命令行时,groupFeature 或 libFeature 定义了应该如何进行。\$<LINK_GROUP:…> 用于表示对一组库整体施加约束,而 \$<LINK_LIBRARY:…> 表示对每个列出的库个别施加约束。这两个表达式支持的特性集是独立且不重叠的。

16.2.1. 链接组特性

CMake 仅定义了一个用于 $<LINK_GROUP:…> 的内置 groupFeature,名为 RESCAN。它确保将库列表保持在链接器命令行上。它用链接器选项将该库列表包围起来,使链接器重复扫描组成员以解析符号。对于某些链接器,库通常只会在单个传递中被扫描一次。对于这样的链接器,通常必须由命令行上稍后的内容提供库较早所需的符号。RESCAN 特性通过使用多次传递尽可能解析组内的符号,使链接器更加努力工作。某些链接器默认已经是多次传递,不需要添加任何附加选项即可获得此行为。

add_library(MyThings STATIC ...)
add_executable(App ...)
target_link_libraries(App PRIVATE
 $<LINK_GROUP:RESCAN,MyThings,externalTC>
)

在上面的例子中,项目定义了一个 MyThings 目标,它与外部提供的 externalTC 库密切耦合。两者都需要彼此的符号。在链接 App 目标时,CMake 将选择适当的工具链链接标志来表示组约束。对于使用 GNU ld 链接器的工具链,上面的例子可能导致链接器命令行包含以下片段:

-Wl,--start-group /path/to/libMyThings.a -lexternalTC -Wl,--end-group

一旦在项目的任何地方提到了一个库属于 RESCAN 组,CMake 将在链接任何目标时将该库的任何独立使用替换为整个组。一个库可以是多个 RESCAN 组的成员,尽管这样的情况可能表明这些组可能都被不充分地指定。

16.2.2. 链接库特性

\$<LINK_LIBRARY:…> 生成表达式具有一个不同且更大的内置特性集。生成表达式的官方文档列出了所有支持的特性,但这里讨论一些以演示用法。

在以下示例中,Things 是期望消费者链接到的主要库。然而,项目在单独的 SubPart 和 AnotherPart 静态库中实现了一些 Things 的公共 API。这些不仅仅是私有实现细节,它们是消费 Things 的函数等的实现,而 Things 本身可能不引用它们。

add_library(SubPart STATIC ...)
add_library(AnotherPart STATIC ...)
add_library(Things SHARED ...)
target_link_libraries(Things PRIVATE
 SubPart
 AnotherPart
)

add_executable(App ...)
target_link_libraries(App PRIVATE Things)

在上面所示的示例中,链接器通常会在将 Things 链接为共享库时丢弃 SubPart 和 AnotherPart 的所有符号。内置的 WHOLE_ARCHIVE 库特性可用于防止链接器丢弃这些符号:

target_link_libraries(Things PRIVATE
 $<LINK_LIBRARY:WHOLE_ARCHIVE,SubPart,AnotherPart>
)

CMake 再次选择适当的链接器命令行标志来实现约束。对于使用 Visual Studio 工具链的情况,将使用 /WHOLEARCHIVE 选项。对于使用 GNU ld 链接器的工具链,可能会使用 –whole-archive 标志,以及其他推送和弹出该特性状态的标志(如果支持)。

CMake 可以利用以单个表达式表示的库来减少添加到链接器命令行的标志数量。请注意,这些库不能保证作为一组列出。对于需要将库集合保持在一起的情况,请使用 \$<LINK_GROUP:…>。

在针对 Apple 平台时,还提供了许多其他内置的 \$<LINK_LIBRARY:…> 特性。FRAMEWORK 特性可用于明确强制将库视为框架。在按名称而不是作为 CMake 目标链接外部框架时,这更有用(参见第 24.9 节,“链接框架”)。NEEDED_FRAMEWORK 和 NEEDED_LIBRARY 特性可用于强制链接器使目标链接到框架或库,即使目标不使用其中的任何符号。如果目标不直接引用框架或库的任何符号,但框架或库具有具有副作用的全局对象的构造函数(注册处理程序等),这可能是可取的。其他内置特性提供对弱引用和重新导出符号的支持,但这些是相当高级的用例。

在更复杂的项目中,存在冲突的 \$<LINK_LIBRARY:…> 表达式的可能性较大。CMake 3.24 及更高版本支持 LINK_LIBRARY_OVERRIDE 和 LINK_LIBRARY_OVERRIDE_<LIBRARY> 目标属性,作为解决这些情况的潜在方式。它们允许目标覆盖它使用的库附加的链接特性。这些覆盖属性应该被视为对已经是高级功能的最后一招解决方法。它们适用于某些情景,但如果可能的话应该避免使用。有关使用示例,请参阅官方 CMake 文档中的属性。

16.2.3. 自定义特性

\$<LINK_LIBRARY:…> 和 \$<LINK_GROUP:…> 生成表达式还支持由项目定义的自定义特性。这是一个高级领域,大多数项目不需要。官方 CMake 文档解释了对那些确实需要该级别的自定义和控制的项目涉及的步骤。

16.2.4. 特性有效性

项目有责任确保仅对 \$<LINK_LIBRARY:…> 或 \$<LINK_GROUP:…> 特性使用一组该特性有效的库。CMake 不会尝试检测无效的组合。例如,使用 \$<LINK_LIBRARY:WHOLE_ARCHIVE,…> 与除静态库之外的任何东西一起使用将是不适当的。

16.3. 传递直接链接依赖关系

对于大多数项目,目标之间的链接关系可以且应该纯粹通过 target_link_libraries() 调用来指定。这些调用清晰地表达了关系的性质,即目标所需的东西(PRIVATE),目标的消费者所需的东西(INTERFACE),或者两者都需要的东西(PUBLIC)。

在某些场景中,关系的性质更加复杂。有时,一组对象必须仅由链接依赖链头部的顶层可执行文件或共享库进行链接。链接渗透技术就是一个例子。接口在较低级别库中定义,虽然该库可能使用该接口,但它不提供其实现。相反,应用程序预期提供实现,通常作为链接器命令行上的目标文件(使用目标文件可确保避免库排序问题)。项目可以利用此技术在不同的可执行文件中使用不同的实现。生产应用程序可能使用真实的实现,而测试可执行文件可能提供可预测的值或使用带有非必要功能存根的模拟实现。

像上面的关系不能在 CMake 3.23 及更早版本中轻松而鲁棒地表达。库依赖于消费它的可执行文件,但可能存在不同的可执行文件,因此库无法表达这种依赖关系。项目必须依赖直接将目标文件添加到每个可执行文件或共享库目标中。它不能将该逻辑附加到可执行文件和共享库链接到的中间库。

使用 CMake 3.24 或更高版本,可以使用 INTERFACE_LINK_LIBRARIES_DIRECT 目标属性来处理这些情况。它允许目标指定应视为直接链接依赖关系的库,直到依赖链顶部的可执行文件或共享库。与 INTERFACE_LINK_LIBRARIES 属性相比,该属性仅向该目标的直接消费者添加间接依赖项。

直接和间接依赖项之间的差异微妙。其中一个更重要的差异在于对象库受到的影响(参见第 18.2.2 节,“对象库”)。间接链接的对象库不会将它们的对象添加到消费者中。它们的对象仅添加到直接链接依赖关系中。这很重要,因为这意味着 INTERFACE_LINK_LIBRARIES_DIRECT 可以列出对象库,并且这些对象将添加到链中每个目标的链接器命令行,直到可执行文件或共享库。在 INTERFACE_LINK_LIBRARIES 中列出的对象库仅会将对象文件添加到其直接消费者的链接器命令行。如果该直接消费者不是可执行文件或共享库,则这些对象文件最终不会成为最终可执行文件或共享库的一部分,从而导致未解析的符号。

16.3.1. 链接渗透示例

考虑以下部分示例,其中包含最终所需的一组直接链接关系的图表:

add_executable(Exe1 ...)
add_executable(Exe2 ...)
add_library(Middle STATIC ...)
add_library(SeamIface STATIC ...)
add_library(SeamImpl OBJECT ...)

# INCOMPLETE: Exe1 and Exe2 still need SeamImpl to be linked
target_link_libraries(Exe1 PRIVATE Middle)
target_link_libraries(Exe2 PRIVATE Middle)
target_link_libraries(Middle PRIVATE SeamIface)

在上面的示例中,SeamIface 使用它没有提供并且没有从自己的链接依赖中拉取的符号。它期望这些符号由最终链接到它的可执行文件或共享库提供。SeamImpl 提供这些实现。

在 CMake 3.23 中,必须显式地将 SeamImpl 添加到每个直接或间接链接到 SeamIface 的可执行文件中:

target_link_libraries(Exe1 PRIVATE SeamImpl)
target_link_libraries(Exe2 PRIVATE SeamImpl)

在 CMake 3.24 或更高版本中,要求可以附加到 Middle 目标上。任何链接到 Middle(直接或间接)的东西都将 SeamImpl 添加到其直接依赖项中:

set_target_properties(Middle PROPERTIES
 INTERFACE_LINK_LIBRARIES_DIRECT SeamImpl
)

将实现附加到 Middle 更加方便,如果有许多可执行文件的话。

进一步扩展示例,可以更清楚地看到这种模式允许向不同组的可执行文件提供不同的链接缝实现。

如前所述,列在 INTERFACE_LINK_LIBRARIES_DIRECT 中的项目将链接到依赖链中每个目标,直到可执行文件或共享库。这可能导致中间目标不希望的链接。可以使用生成器表达式仅将直接链接依赖项添加到链接依赖链顶部的头目标。这将是一个可执行文件、共享库或模块库。可以修改先前的示例以演示此行为:

add_executable(TopExe ...)
add_library(Extra STATIC ...)
add_library(Middle STATIC ...)
add_library(SeamIface STATIC ...)
add_library(SeamImpl OBJECT ...)
target_link_libraries(TopExe PRIVATE Extra)
target_link_libraries(Extra PRIVATE Middle)
target_link_libraries(Middle PRIVATE SeamIface)

# OPTION A: No generator expression
set_target_properties(Middle PROPERTIES
 INTERFACE_LINK_LIBRARIES_DIRECT SeamImpl
)

# OPTION B: With generator expression
# Build up a generator expression that evaluates to 1 only for the head target
set(type "$<TARGET_PROPERTY:TYPE>")
set(head_targets EXECUTABLE SHARED_LIBRARY MODULE_LIBRARY)
set(is_head "$<IN_LIST:${type},${head_targets}>")
set_target_properties(Middle PROPERTIES 
 INTERFACE_LINK_LIBRARIES_DIRECT "$<${is_head}:SeamImpl>"
)

Extra 是一个静态库,因此不需要 SeamImpl 对象。但是,如果不使用生成器表达式来限制 SeamImpl 的直接链接,Extra 将包含其自己的 SeamImpl 对象的副本。

16.3.2. 静态插件

INTERFACE_LINK_LIBRARIES_DIRECT 也可以用于其他场景。一些项目定义了一个具有一个或多个关联插件的库。当项目被构建为共享库和插件时,库会在运行时动态加载插件。当它们被静态构建时,插件必须链接到任何使用库的可执行文件中,而库必须使用某种其他机制找到插件。还必须有某个东西使用插件的符号,以防止链接器丢弃它们。

项目可能选择将插件列在库的 INTERFACE_LINK_LIBRARIES_DIRECT 属性中(这是 CMake 自己文档中用于该属性的示例)。更可能的是,会列出一个带有引用插件符号的注册函数的对象库。这种安排意味着可执行文件可以链接到库,而不管事物是作为共享库还是静态库构建,插件都将对应用程序可用。

插件场景可能比上述简短描述更复杂。实现插件注册函数和库中的符号搜索可能并非易事。它们特定于项目和目标平台的能力。在某些情况下,可能还需要一个配套的 INTERFACE_LINK_LIBRARIES_DIRECT_EXCLUDE 属性。它允许在将直接链接依赖项添加到链接器命令行之前对最终的直接链接依赖项列表进行过滤。如果使用不当,它可能通过以不满足库之间依赖关系的方式重新排列事物而破坏链接器命令行。因此,除非绝对必要,否则应该避免使用它。请查阅官方的 CMake 文档,了解如何使用 INTERFACE_LINK_LIBRARIES_DIRECT_EXCLUDE 属性以及可能需要它的情景的更多详细信息。

16.4. 推荐做法

在可能的情况下,链接到目标而不是原始库名称或路径。目标在跨平台时更具可移植性,并支持引入其他用法要求,而不仅仅是指定基本的链接关系。考虑在 cmake 命令行或预设中将 CMAKE_LINK_LIBRARIES_ONLY_TARGETS 设置为 true,以将此作为要求强制执行。

在 CMake 3.24 或更高版本中提供的 \$<LINK_GROUP:…> 和 \$<LINK_LIBRARY:…> 生成器表达式不应在没有仔细考虑替代方案的情况下使用。它们提供的一些功能很容易被误用为掩盖项目中的结构问题的方法。如果库之间的相互依赖关系可以通过重构和重组来消除,避免使用 \$<LINK_GROUP:RESCAN,…>。与其使用 \$<LINK_LIBRARY:WHOLE_ARCHIVE,…>,考虑使用对象库或将单独的库组合成一个单一库可能是更好的解决方案。有关该领域的进一步讨论,请参阅第 22.6 节,“混合静态和共享库”。

同样,如果目标之间的关系可以在没有它的情况下得到充分表达,则避免使用 INTERFACE_LINK_LIBRARIES_DIRECT 目标属性。有一些有效的情景可能是适当的,比如在使用链接缝技术或某些类型的静态插件处理时。这些应被视为解决特定问题的更高级方法,而不是首选解决方案。