CMake 秘籍(六)(1)https://developer.aliyun.com/article/1525057
它是如何工作的
这个配方展示了如何为共享库设置符号的可见性。最佳实践是默认隐藏所有符号,只明确暴露我们希望被库依赖者使用的那些符号。这通过两个步骤实现。首先,我们需要指示编译器隐藏符号。当然,不同的编译器将有不同的选项可用,直接在我们的CMakeLists.txt
中手动设置这些将不是跨平台的。CMake 提供了一种设置符号可见性的健壮且跨平台的方法,即通过在共享库目标上设置两个属性:
CXX_VISIBILITY_PRESET hidden
:这将隐藏所有符号,除非明确标记为其他。当使用 GNU 编译器时,这为目标添加了-fvisibility=hidden
标志。VISIBILITY_INLINES_HIDDEN 1
:这将隐藏内联函数的符号。如果使用 GNU 编译器,这对应于-fvisibility-inlines-hidden
。
在 Windows 上,这是默认行为。实际上,回想一下,在前一个配方中,我们需要通过将WINDOWS_EXPORT_ALL_SYMBOLS
属性设置为ON
来覆盖它。
我们如何标记我们希望可见的符号?这是由预处理器决定的,因此我们需要提供预处理器宏,这些宏扩展为给定编译器在所选平台上将理解的可见性属性。再次,CMake 通过GenerateExportHeader.cmake
模块文件来救援。该模块定义了generate_export_header
函数,我们按如下方式调用它:
include(GenerateExportHeader) generate_export_header(message-shared BASE_NAME "message" EXPORT_MACRO_NAME "message_EXPORT" EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h" DEPRECATED_MACRO_NAME "message_DEPRECATED" NO_EXPORT_MACRO_NAME "message_NO_EXPORT" STATIC_DEFINE "message_STATIC_DEFINE" NO_DEPRECATED_MACRO_NAME "message_NO_DEPRECATED" DEFINE_NO_DEPRECATED )
该函数生成包含所需预处理器宏的messageExport.h
头文件。文件在目录${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
中生成,如通过EXPORT_FILE_NAME
选项所请求。如果此选项留空,头文件将在当前二进制目录中生成。该函数的第一
BASE_NAME
:这设置生成的头文件和宏的基本名称为传入的值。EXPORT_MACRO_NAME
:这设置导出宏的名称。EXPORT_FILE_NAME
:这设置生成的导出头文件的名称。DEPRECATED_MACRO_NAME
:这设置废弃宏的名称。这用于标记废弃代码,如果客户端使用它,编译器将发出废弃警告。NO_EXPORT_MACRO_NAME
:这设置不导出宏的名称。STATIC_DEFINE
:这是用于当也从相同源代码编译静态库时使用的宏的名称。NO_DEPRECATED_MACRO_NAME
:这设置用于排除编译中废弃代码的宏的名称。DEFINE_NO_DEPRECATED
:这指示 CMake 生成预处理器代码,以排除编译中的废弃代码。
在 GNU/Linux 上使用 GNU 编译器时,CMake 将生成以下messageExport.h
导出头文件:
#ifndef message_EXPORT_H #define message_EXPORT_H #ifdef message_STATIC_DEFINE # define message_EXPORT # define message_NO_EXPORT #else # ifndef message_EXPORT # ifdef message_shared_EXPORTS /* We are building this library */ # define message_EXPORT __attribute__((visibility("default"))) # else /* We are using this library */ # define message_EXPORT __attribute__((visibility("default"))) # endif # endif # ifndef message_NO_EXPORT # define message_NO_EXPORT __attribute__((visibility("hidden"))) # endif #endif #ifndef message_DEPRECATED # define message_DEPRECATED __attribute__ ((__deprecated__)) #endif #ifndef message_DEPRECATED_EXPORT # define message_DEPRECATED_EXPORT message_EXPORT message_DEPRECATED #endif #ifndef message_DEPRECATED_NO_EXPORT # define message_DEPRECATED_NO_EXPORT message_NO_EXPORT message_DEPRECATED #endif #if 1 /* DEFINE_NO_DEPRECATED */ # ifndef message_NO_DEPRECATED # define message_NO_DEPRECATED # endif #endif #endif
我们可以通过在类和函数前加上message_EXPORT
宏来向用户公开它们。通过在前面加上message_DEPRECATED
宏可以实现废弃。
静态库由相同的源代码构建。然而,所有符号都应在静态档案中可见,并且从messageExport.h
头文件的内容可以看出,message_STATIC_DEFINE
宏来救援。一旦目标被声明,我们就将其设置为编译定义。静态库上的额外目标属性如下:
ARCHIVE_OUTPUT_NAME "message"
:这将确保库文件的名称只是 message,而不是 message-static。DEBUG_POSTFIX "_sd"
:这将给定的后缀附加到库。这独特地将库标识为在Debug
配置中的静态。RELEASE_POSTFIX "_s"
:这与前面的属性类似,但仅在目标在Release
配置中构建时附加后缀给静态库。
还有更多内容
在构建共享库时隐藏内部符号是一种良好的实践。这意味着库的尺寸会缩小,因为你向用户暴露的内容少于库中实际拥有的内容。这定义了应用程序二进制接口(ABI),大多数情况下应该与应用程序编程接口(API)一致。这分为两个阶段进行:
- 我们使用适当的编译器标志。
- 我们使用预处理器变量(在我们的例子中是
message_EXPORT
)来标记要导出的符号。在编译时,这些符号(如类和函数)的隐藏将被解除。
静态库只是对象文件的存档。因此,首先将源代码编译成对象文件,然后存档器将它们捆绑成一个存档。这里没有 ABI 的概念:所有符号默认都是可见的,编译器的可见性标志不影响静态存档。然而,如果你打算从相同的源文件构建共享库和静态库,你需要一种方法来赋予message_EXPORT
预处理器变量在代码中两种情况下出现的意义。这就是GenerateExportHeader.cmake
模块的作用。它将定义一个包含所有逻辑的头文件,用于给出这个预处理器变量的正确定义。对于共享库,它将根据平台和编译器的组合提供所需的内容。请注意,意义也会根据我们是构建还是使用共享库而改变。幸运的是,CMake 为我们处理了这一点,无需进一步干预。对于静态库,它将扩展为一个空字符串,做我们期望的事情:什么都不做。
细心的读者会注意到,按照这里所示构建静态库和共享库实际上需要编译源代码两次。对于我们简单的例子来说,这不是一个昂贵的操作,但对于比我们例子稍大的项目来说,这显然可能会变得相当繁重。为什么我们选择这种方法而不是在第 3 个菜谱中展示的使用OBJECT
库的方法,即“构建和链接静态和共享库”,在第一章“从简单的可执行文件到库”中?OBJECT
库负责编译库的第一步:从源代码到对象文件。在这一步中,预处理器介入并评估message_EXPORT
。由于OBJECT
库的编译只发生一次,message_EXPORT
要么被评估为与构建共享库或静态库兼容的值。因此,为了避免歧义,我们选择了更稳健的方法,即编译两次,让预处理器正确评估可见性变量。
关于动态共享对象、静态存档和符号可见性的更多详细信息,我们建议阅读这篇文章:people.redhat.com/drepper/dsohowto.pdf
。
导出你的目标
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-10/recipe-03
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.6(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
我们可以想象我们的消息库在开源社区中取得了巨大的成功。人们非常喜欢它,并在自己的项目中使用它来将消息打印到屏幕上。用户特别喜欢每条打印的消息都有一个唯一标识符的事实。但用户也希望库在编译和安装到他们的系统后更容易被发现。本食谱将展示如何使用 CMake 导出我们的目标,以便使用 CMake 的其他项目可以轻松地获取它们。
准备工作
源代码与前一个食谱相比未更改,项目的结构如下:
. ├── cmake │ └── messageConfig.cmake.in ├── CMakeLists.txt ├── src │ ├── CMakeLists.txt │ ├── hello-world.cpp │ ├── Message.cpp │ └── Message.hpp └── tests ├── CMakeLists.txt └── use_target ├── CMakeLists.txt └── use_message.cpp
请注意,我们添加了一个包含messageConfig.cmake.in
文件的cmake
子目录。该文件将包含我们导出的目标。我们还添加了一个测试,以检查项目的安装和导出是否按预期工作。
如何操作
再次,根CMakeLists.txt
文件与前一个食谱相比未更改。转到包含我们源文件的叶目录src
:
- 我们需要找到 UUID 库,我们可以重用之前食谱中使用的代码:
# Search for pkg-config and UUID find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_search_module(UUID uuid IMPORTED_TARGET) if(TARGET PkgConfig::UUID) message(STATUS "Found libuuid") set(UUID_FOUND TRUE) endif() endif()
- 接下来,我们设置我们的共享库目标并生成导出头文件,如前一个食谱所示:
add_library(message-shared SHARED "") include(GenerateExportHeader) generate_export_header(message-shared BASE_NAME "message" EXPORT_MACRO_NAME "message_EXPORT" EXPORT_FILE_NAME "${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h" DEPRECATED_MACRO_NAME "message_DEPRECATED" NO_EXPORT_MACRO_NAME "message_NO_EXPORT" STATIC_DEFINE "message_STATIC_DEFINE" NO_DEPRECATED_MACRO_NAME "message_NO_DEPRECATED" DEFINE_NO_DEPRECATED ) target_sources(message-shared PRIVATE ${CMAKE_CURRENT_LIST_DIR}/Message.cpp )
- 我们为目标设置
PUBLIC
和INTERFACE
编译定义。注意后者使用$
生成器表达式:
target_compile_definitions(message-shared PUBLIC $<$<BOOL:${UUID_FOUND}>:HAVE_UUID> INTERFACE $<INSTALL_INTERFACE:USING_message> )
- 接下来,设置包含目录。再次注意使用
$
和$
生成器表达式。我们将在后面对此进行评论:
target_include_directories(message-shared PUBLIC $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}> $<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}> )
- 我们通过列出链接库和目标属性来完成共享库目标。这些与前一个食谱中未更改:
target_link_libraries(message-shared PUBLIC $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID> ) set_target_properties(message-shared PROPERTIES POSITION_INDEPENDENT_CODE 1 CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1 SOVERSION ${PROJECT_VERSION_MAJOR} OUTPUT_NAME "message" DEBUG_POSTFIX "_d" PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h" MACOSX_RPATH ON )
同样,对于message-static
库目标也是如此:
- 我们首先声明它并列出其源文件:
add_library(message-static STATIC "") target_sources(message-static PRIVATE ${CMAKE_CURRENT_LIST_DIR}/Message.cpp )
- 我们给出
PUBLIC
和INTERFACE
编译定义,就像在前一个食谱中一样,但现在使用$
生成器表达式:
target_compile_definitions(message-static PUBLIC message_STATIC_DEFINE $<$<BOOL:${UUID_FOUND}>:HAVE_UUID> INTERFACE $<INSTALL_INTERFACE:USING_message> )
- 我们使用与共享目标相同的命令列出包含目录:
target_include_directories(message-static PUBLIC $<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}> $<INSTALL_INTERFACE:${INSTALL_INCLUDEDIR}> )
- 链接库和目标属性与前一个食谱相比未更改:
target_link_libraries(message-static PUBLIC $<$<BOOL:${UUID_FOUND}>:PkgConfig::UUID> ) set_target_properties(message-static PROPERTIES POSITION_INDEPENDENT_CODE 1 ARCHIVE_OUTPUT_NAME "message" DEBUG_POSTFIX "_sd" RELEASE_POSTFIX "_s" PUBLIC_HEADER "Message.hpp;${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}/messageExport.h" )
- 使用与前一个食谱中完全相同的命令生成可执行文件:
add_executable(hello-world_wDSO hello-world.cpp) target_link_libraries(hello-world_wDSO PUBLIC message-shared ) # Prepare RPATH file(RELATIVE_PATH _rel ${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX}) if(APPLE) set(_rpath "@loader_path/${_rel}") else() set(_rpath "\$ORIGIN/${_rel}") endif() file(TO_NATIVE_PATH "${_rpath}/${INSTALL_LIBDIR}" message_RPATH) set_target_properties(hello-world_wDSO PROPERTIES MACOSX_RPATH ON SKIP_BUILD_RPATH OFF BUILD_WITH_INSTALL_RPATH OFF INSTALL_RPATH "${message_RPATH}" INSTALL_RPATH_USE_LINK_PATH ON ) add_executable(hello-world_wAR hello-world.cpp) target_link_libraries(hello-world_wAR PUBLIC message-static )
我们现在准备查看安装规则:
- 我们将所有目标的安装规则列在一起,因为 CMake 可以正确地将每个目标放置在适当的目的地。这次,我们添加了
EXPORT
关键字,以便 CMake 将为我们导出的目标生成一个导出的目标文件:
install( TARGETS message-shared message-static hello-world_wDSO hello-world_wAR EXPORT messageTargets ARCHIVE DESTINATION ${INSTALL_LIBDIR} COMPONENT lib RUNTIME DESTINATION ${INSTALL_BINDIR} COMPONENT bin LIBRARY DESTINATION ${INSTALL_LIBDIR} COMPONENT lib PUBLIC_HEADER DESTINATION ${INSTALL_INCLUDEDIR}/message COMPONENT dev )
- 自动生成的导出目标文件名为
messageTargets.cmake
,我们需要为它明确指定安装规则。该文件的目的地是在根CMakeLists.txt
文件中定义的INSTALL_CMAKEDIR
:
install( EXPORT messageTargets NAMESPACE "message::" DESTINATION ${INSTALL_CMAKEDIR} COMPONENT dev )
- 最后,我们需要生成适当的 CMake 配置文件。这些文件将确保下游项目能够找到由 message 库导出的目标。为此,我们首先包含
CMakePackageConfigHelpers.cmake
标准模块:
include(CMakePackageConfigHelpers)
- 我们让 CMake 生成一个包含我们库版本信息的文件:
write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake
VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion )
- 使用
configure_package_config_file
函数,我们生成实际的 CMake 配置文件。这是基于模板cmake/messageConfig.cmake.in
文件:
configure_package_config_file( ${PROJECT_SOURCE_DIR}/cmake/messageConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake INSTALL_DESTINATION ${INSTALL_CMAKEDIR} )
- 作为最后一步,我们为这两个自动生成的配置文件设置安装规则:
install( FILES ${CMAKE_CURRENT_BINARY_DIR}/messageConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/messageConfigVersion.cmake DESTINATION ${INSTALL_CMAKEDIR} )
cmake/messageConfig.cmake.in
模板文件的内容是什么?该文件的头部作为对其用户的文档。让我们看看实际的 CMake 命令:
- 我们从一个占位符开始,该占位符将被
configure_package_config_file
命令替换:
@PACKAGE_INIT@
- 我们包含目标的自动生成的导出文件:
include("${CMAKE_CURRENT_LIST_DIR}/messageTargets.cmake")
- 然后我们使用 CMake 提供的
check_required_components
函数检查静态库、共享库以及两个“Hello, World”可执行文件是否存在:
check_required_components( "message-shared" "message-static"
"message-hello-world_wDSO" "message-hello-world_wAR" )
- 我们检查目标
PkgConfig::UUID
是否存在。如果不存在,我们再次搜索 UUID 库,但仅限于不在 Windows 系统上时:
if(NOT WIN32) if(NOT TARGET PkgConfig::UUID) find_package(PkgConfig REQUIRED QUIET) pkg_search_module(UUID REQUIRED uuid IMPORTED_TARGET) endif() endif()
让我们尝试一下:
$ mkdir -p build $ cd build $ cmake -DCMAKE_INSTALL_PREFIX=$HOME/Software/recipe-03 .. $ cmake --build . --target install
安装树具有以下结构:
$HOME/Software/recipe-03/ ├── bin │ ├── hello-world_wAR │ └── hello-world_wDSO ├── include │ └── message │ ├── messageExport.h │ └── Message.hpp ├── lib64 │ ├── libmessage_s.a │ ├── libmessage.so -> libmessage.so.1 │ └── libmessage.so.1 └── share └── cmake └── recipe-03 ├── messageConfig.cmake ├── messageConfigVersion.cmake ├── messageTargets.cmake └── messageTargets-release.cmake
您会注意到出现了一个share
子目录,其中包含了所有我们要求 CMake 自动生成的文件。从现在开始,使用我们的message
库的用户可以在他们自己的CMakeLists.txt
文件中通过设置message_DIR
CMake 变量指向安装树中的share/cmake/message
目录来定位message
库:
find_package(message 1 CONFIG REQUIRED)
它是如何工作的
这个配方涵盖了很多内容;让我们来理解它。CMake 目标是对构建系统将要执行的操作非常有用的抽象。使用PRIVATE
、PUBLIC
和INTERFACE
关键字,我们可以设置同一项目内的目标将如何相互作用。实际上,这让我们定义了目标 A 的依赖项将如何影响依赖于 A 的目标 B。当其他项目想要将一个库作为依赖项使用时,可以充分体会到这种机制的强大之处。如果库维护者提供了适当的 CMake 配置文件,那么所有依赖项都可以很容易地用很少的 CMake 命令来解决。
这个问题可以通过遵循message-static
、message-shared
、hello-world_wDSO
和hello-world_wAR
目标的配方中概述的模式来解决。我们将单独分析message-shared
目标的 CMake 命令,但这里的讨论是通用的:
- 在项目构建中生成目标并布置其依赖项。对于
message-shared
,链接 UUID 库是一个PUBLIC
要求,因为它将用于构建项目内的目标以及下游项目中的目标。编译定义和包含目录需要在PUBLIC
或INTERFACE
级别设置。其中一些将用于构建项目内的目标,而其他一些仅与下游项目相关。此外,其中一些仅在项目安装后才相关。这就是$
和$
生成器表达式发挥作用的地方。只有message
库外部的下游目标才需要这些,也就是说,只有在目标安装后它们才会变得可见。在我们的示例中,以下适用:
$
仅在message-shared
库目标在我们的项目内使用时,才会扩展为${CMAKE_BINARY_DIR}/${INSTALL_INCLUDEDIR}
。$
仅在message-shared
库目标作为另一个构建树内的导出目标使用时,才会扩展为${INSTALL_INCLUDEDIR}
。
- 描述目标的安装规则,包括 CMake 将生成的
EXPORT
文件的名称。 - 描述 CMake 生成的导出文件的安装规则。
messageTargets.cmake
文件将安装到INSTALL_CMAKEDIR
。目标导出文件的安装规则的NAMESPACE
选项将在目标名称前加上给定的字符串。这有助于避免来自不同项目的目标之间的潜在名称冲突。INSTALL_CMAKEDIR
变量在根CMakeLists.txt
文件中设置:
if(WIN32 AND NOT CYGWIN) set(DEF_INSTALL_CMAKEDIR CMake) else() set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME}) endif() set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files")
我们CMakeLists.txt
的最后一部分生成配置文件。在包含CMakePackageConfigHelpers.cmake
模块之后,这分为三个步骤完成:
- 我们调用
write_basic_package_version_file
CMake 函数来生成一个包版本文件。宏的第一个参数是版本文件的路径:messageConfigVersion.cmake
。然后,我们使用PROJECT_VERSION
CMake 变量以 Major.Minor.Patch 格式指定版本。还可以指定与库的新版本的兼容性。在我们的例子中,我们保证当库具有相同的 major 版本时兼容,因此使用了SameMajorVersion
参数。 - 接下来,我们配置模板文件
messageConfig.cmake.in
;该文件位于项目的cmake
子目录中。 - 最后,我们为新生成的文件设置安装规则。两者都将安装在
INSTALL_CMAKEDIR
下。
还有更多内容
消息库的客户端现在非常满意,因为他们终于可以在自己的系统上安装该库,并且让 CMake 为他们发现它,而无需对其自己的CMakeLists.txt
进行太多修改。
find_package(message VERSION 1 REQUIRED)
客户端现在可以按以下方式配置他们的项目:
$ cmake -Dmessage_DIR=/path/to/message/share/cmake/message ..
我们示例中包含的测试展示了如何检查目标的安装是否按计划进行。查看tests
文件夹的结构,我们注意到use_target
子目录:
tests/ ├── CMakeLists.txt └── use_target ├── CMakeLists.txt └── use_message.cpp
该目录包含一个使用导出目标的小型项目。有趣的部分在于指定测试的CMakeLists.txt
文件:
- 我们测试小型项目是否可以配置为使用已安装的库。这是使用目标测试夹具的设置步骤,如第四章,创建和运行测试,食谱 10,使用测试夹具所示:
add_test( NAME use-target_configure COMMAND ${CMAKE_COMMAND} -H${CMAKE_CURRENT_LIST_DIR}/use_target -B${CMAKE_CURRENT_BINARY_DIR}/build_use-target -G${CMAKE_GENERATOR} -Dmessage_DIR=${CMAKE_INSTALL_PREFIX}/${ INSTALL_CMAKEDIR} -DCMAKE_BUILD_TYPE=$<CONFIGURATION> ) set_tests_properties(use-target_configure PROPERTIES FIXTURES_SETUP use-target )
- 我们测试小型项目是否可以构建:
add_test( NAME use-target_build COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/build_use-target --config $<CONFIGURATION> ) set_tests_properties(use-target_build PROPERTIES FIXTURES_REQUIRED use-target )
- 小型项目的测试也会运行:
set(_test_target) if(MSVC) set(_test_target "RUN_TESTS") else() set(_test_target "test") endif() add_test( NAME use-target_test COMMAND ${CMAKE_COMMAND} --build ${CMAKE_CURRENT_BINARY_DIR}/build_use-target --target ${_test_target} --config $<CONFIGURATION> ) set_tests_properties(use-target_test PROPERTIES FIXTURES_REQUIRED use-target ) unset(_test_target)
- 最后,我们拆卸夹具:
add_test( NAME use-target_cleanup COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/build_use-target ) set_tests_properties(use-target_cleanup PROPERTIES FIXTURES_CLEANUP use-target )
请注意,这些测试只能在项目安装之后运行。
安装超级构建
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-10/recipe-04
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.6(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
我们的示例message
库取得了巨大成功,许多其他程序员都在使用它,并且非常满意。您也想在自己的项目中使用它,但不确定如何正确管理依赖关系。您可以将message
库的源代码与您自己的代码一起打包,但如果该库已经在系统上安装了呢?第八章,超级构建模式,展示了这是一个典型的超级构建场景,但您不确定如何安装这样的项目。本食谱将引导您了解安装超级构建的细节。
CMake 秘籍(六)(3)https://developer.aliyun.com/article/1525060