ament_cmake 文档
ament_cmake 是 ROS 2 中基于 CMake 的包构建系统,大多数 C/C++ 项目都会用到它。它本质上是增强 CMake 的一组脚本,为包作者提供便捷功能。在使用 ament_cmake 之前,建议先了解 CMake 的基础知识,官方教程见这里。
运行 ros2 pkg create <package_name> 即可生成一个基本的 CMake 框架。构建信息分布在两个文件中:package.xml 和 CMakeLists.txt,二者必须位于同一目录下。package.xml 包含所有依赖项和元数据,colcon 据此确定正确的包构建顺序,CI 据此安装所需依赖,bloom 则据此完成发布。CMakeLists.txt 包含构建和打包可执行文件及库的命令,是本文档的重点。
基本项目结构
Section titled “基本项目结构”ament 包的 CMakeLists.txt 基本结构包含:
cmake_minimum_required(VERSION 3.20)project(my_project)
ament_package()project 的参数就是包名,必须与 package.xml 中的包名一致。
项目设置由 ament_package() 完成,每个包必须只调用一次。ament_package() 会安装 package.xml,将包注册到 ament 索引中,并安装 CMake 配置文件(可能还有 target 文件),以便其他包可以通过 find_package 找到它。ament_package() 需要从 CMakeLists.txt 中收集大量信息,因此它必须是 CMakeLists.txt 中的最后一条调用。
ament_package 可以接受额外的参数:
-
CONFIG_EXTRAS:一个 CMake 文件列表(.cmake或通过configure_file()展开的.cmake.in模板文件),这些文件应当对包的客户端可用。关于何时使用这些参数,请参见添加资源一节。关于模板文件的更多信息,请参见官方文档。 -
CONFIG_EXTRAS_POST:与CONFIG_EXTRAS相同,但文件添加的顺序不同。CONFIG_EXTRAS文件在ament_export_*调用生成的文件之前被包含,而CONFIG_EXTRAS_POST文件则在之后被包含。
除了添加到 ament_package 之外,你也可以将文件添加到变量 ${PROJECT_NAME}_CONFIG_EXTRAS 和 ${PROJECT_NAME}_CONFIG_EXTRAS_POST 中,效果相同。唯一的区别同样是文件添加的顺序,总体顺序如下:
-
通过
CONFIG_EXTRAS添加的文件 -
通过追加到
${PROJECT_NAME}_CONFIG_EXTRAS添加的文件 -
通过追加到
${PROJECT_NAME}_CONFIG_EXTRAS_POST添加的文件 -
通过
CONFIG_EXTRAS_POST添加的文件
编译器和链接器选项
Section titled “编译器和链接器选项”ROS 2 的目标编译器遵循 C++20 和 C17 标准。未来可能会以更新的版本为目标,相关参考请见这里。因此,通常需要设置相应的 CMake 标志:
if(NOT CMAKE_C_STANDARD) set(CMAKE_C_STANDARD 17)endif()if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 20)endif()为了保持代码整洁,应让编译器对潜在问题发出警告,并及时修复这些警告。
建议至少涵盖以下警告级别:
-
对于 Visual Studio:默认的
W1警告 -
对于 GCC 和 Clang:强烈建议使用
-Wall -Wextra -Wpedantic,推荐使用-Wshadow
目前建议使用 add_compile_options 来为所有目标添加这些选项。这样可以避免为所有可执行文件、库和测试编写基于目标的编译选项,使代码更加简洁:
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic)endif()大多数 ament_cmake 项目会依赖其他包。在 CMake 中,通过调用 find_package 来实现。例如,如果你的包依赖于 rclcpp,那么 CMakeLists.txt 文件应包含:
find_package(rclcpp REQUIRED)注意: 不要对那些并非直接需要、只是作为其他依赖的传递依赖的库调用
find_package。如果遇到这种情况,请向相应的包提交 bug 报告。
在 CMake 术语中,targets(目标)是本项目将要创建的产物。可以创建库或可执行文件,单个项目可以包含零个或多个各类型的目标。
库通过调用 add_library 来创建,其中应包含目标名称和需要编译以创建库的源文件。
由于 C/C++ 中头文件和实现的分离,通常不需要将头文件作为 add_library 的参数添加。
以下是推荐的最佳实践:
-
将所有需要向库使用者公开的头文件(即需要安装的头文件)放在以包名命名的
include子目录中,其余文件(.c/.cpp和不应导出的头文件)放在src目录中 -
只有
.c/.cpp文件需要在add_library中显式引用 -
通过以下方式查找库
my_library的头文件:
target_include_directories(my_library PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>" "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")构建时,这会将 ${CMAKE_CURRENT_SOURCE_DIR}/include 目录下的所有文件加入公共接口;安装时,则加入 include 目录(相对于 ${CMAKE_INSTALL_DIR})下的文件。
ros2 pkg create 创建的包结构遵循这些规则。
注意: Windows 是官方支持的平台之一,因此包应当尽量能在 Windows 上构建。Windows 的库格式对符号可见性有严格要求:客户端用到的每个符号都必须由库显式导出(使用方则需要隐式导入)。
GCC 和 Clang 构建通常没有这种限制,但仍建议采用 GCC wiki 中的方案。对于名为
my_library的包,使用方法如下:
将链接中的逻辑复制到名为
visibility_control.hpp的头文件中。将
DLL替换为MY_LIBRARY(示例参见 rviz_rendering 的可见性控制)。对所有需要导出的符号(即类或函数)使用宏「MY_LIBRARY_PUBLIC」。
在项目的
CMakeLists.txt中使用:target_compile_definitions(my_library PRIVATE "MY_LIBRARY_BUILDING_LIBRARY")更多详情,请参见 Windows 技巧与提示文档中的 Windows 符号可见性。
可执行文件通过调用 add_executable 来创建,同样需要指定目标名称和源文件。如有需要,还可以用 target_link_libraries 将可执行文件与本包中创建的库链接。
可执行文件通常不会作为库被其他包使用,因此无需将头文件放在 include 目录中。
如果包同时包含库和可执行文件,请同时遵循上述”库”和”可执行文件”两部分的建议。
使用 target_link_libraries 来链接依赖,它会为你的目标提供必要的头文件、库及其所有依赖。
例如,假设我们要将 my_library 链接到线性代数库 Eigen3。Eigen3 定义了目标 Eigen3::Eigen。
find_package(Eigen3 REQUIRED)target_link_libraries(my_library PUBLIC Eigen3::Eigen)在构建可重用的库时,需要导出一些信息,以便下游包能轻松使用。
首先,安装需要向使用者公开的头文件。include 目录的自定义设置是为了支持 colcon 中的 overlay,详见 colcon 文档。
install( DIRECTORY include/ DESTINATION include/${PROJECT_NAME})接下来,安装目标并创建导出目标(export_${PROJECT_NAME}),其他包将通过它来找到此包。注意,可以用一次 install 调用来安装项目中的所有库。
install( TARGETS my_library EXPORT export_${PROJECT_NAME} LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin)
ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)ament_export_dependencies(some_dependency)上面代码片段中发生了以下操作:
-
ament_export_targets宏为 CMake 导出目标。这是让库的使用者能够使用target_link_libraries(client PRIVATE my_library::my_library)语法的必要步骤。如果导出集包含库,请在ament_export_targets中添加HAS_LIBRARY_TARGET选项,这会将潜在的库添加到环境变量中。 -
ament_export_dependencies将依赖导出给下游包。这样库的用户就不必再为这些依赖调用find_package。
警告: 从 CMake 子目录中调用
ament_export_targets、ament_export_dependencies或其他 ament 命令将无法按预期工作,因为 CMake 子目录无法在调用ament_package的父作用域中设置必要的变量。
注意: Windows DLL 被视为运行时产物,安装到
RUNTIME DESTINATION文件夹中。因此,即使在基于 Unix 的系统上开发库,也建议保留RUNTIME安装。
-
install 调用中的
EXPORT表示法需要特别注意:它为my_library目标安装 CMake 文件。其名称必须与ament_export_targets中的参数完全相同。按照惯例,导出名会加上export_前缀,但这个前缀并非必须。 -
所有安装路径都相对于
CMAKE_INSTALL_PREFIX,colcon/ament 已经正确设置了该路径。
还有两个额外的函数可用,但对于基于 target 的安装来说是多余的:
ament_export_include_directories("include/${PROJECT_NAME}")ament_export_libraries(my_library)第一个宏标记导出的 include 目录的位置。第二个宏标记安装库的位置(这已通过调用 ament_export_targets 时的 HAS_LIBRARY_TARGET 参数完成了)。仅当下游项目不能或不想使用基于 CMake target 的依赖时,才使用这些函数。
某些宏可以接受不同类型的参数用于非 target 导出,但由于现代 CMake 推荐使用 target,此处不做介绍。这些选项的文档可以在源代码中找到。
安装可执行文件时,必须严格遵循以下模式,以便其余 ROS 工具能够找到它:
install(TARGETS my_exe DESTINATION lib/${PROJECT_NAME})如果包同时包含库和可执行文件,请同时遵循上述”库”和”可执行文件”两部分的建议。
代码检查与测试
Section titled “代码检查与测试”为了在 colcon 构建时能将测试与库的构建分开,请将所有 linter 和测试的调用放在条件判断中:
if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) ament_add_gtest(<tests>)endif()建议使用 ament_lint_auto 的组合调用:
find_package(ament_lint_auto REQUIRED)ament_lint_auto_find_test_dependencies()这将运行 package.xml 中定义的 linter。建议使用 ament_lint_common 包定义的一组 linter,其中包含的各个 linter 及其功能可以在 ament_lint_common 文档中查看。
ament 提供的 linter 也可以单独添加,无需运行 ament_lint_auto。具体示例参见 ament_cmake_lint_cmake 文档。
ament 包含 CMake 宏来简化 GTest 的设置。调用:
find_package(ament_cmake_gtest)ament_add_gtest(some_test <test_sources>)即可添加一个 GTest。此后它就是一个常规目标,可以与其他库(如项目库)链接。该宏还支持以下额外参数:
APPEND_ENV:追加环境变量。例如,可以通过以下方式向 ament prefix path 添加路径:
find_package(ament_cmake_gtest REQUIRED)ament_add_gtest(some_test <test_sources> APPEND_ENV PATH=some/additional/path/for/testing/resources)-
APPEND_LIBRARY_DIRS:追加库目录,使链接器在运行时能够找到它们。这可以通过在 Windows 上设置PATH、在 Linux 上设置LD_LIBRARY_PATH等环境变量来实现,但会使调用与平台相关。 -
ENV:设置环境变量(语法与APPEND_ENV相同)。 -
TIMEOUT:设置测试超时时间(秒)。GTest 的默认超时时间为 60 秒。例如:
ament_add_gtest(some_test <test_sources> TIMEOUT 120)-
SKIP_TEST:跳过此测试(在控制台输出中会显示为「passed」)。 -
SKIP_LINKING_MAIN_LIBRARIES:不链接 GTest 主库。 -
WORKING_DIRECTORY:设置测试的工作目录。
默认的工作目录是 CMAKE_CURRENT_BINARY_DIR,详见 CMake 文档。
类似地,还有一个 CMake 宏用于设置包含 GMock 的 GTest:
find_package(ament_cmake_gmock REQUIRED)ament_add_gmock(some_test <test_sources>)它具有与 ament_add_gtest 相同的额外参数。
扩展 ament
Section titled “扩展 ament”可以通过多种方式向 ament_cmake 注册额外的宏/函数并加以扩展。
向 ament 添加函数/宏
Section titled “向 ament 添加函数/宏”扩展 ament 通常意味着你希望让某些函数可供其他包使用。向客户端包提供宏的最佳方式是将其注册到 ament。
这可以通过追加 ${PROJECT_NAME}_CONFIG_EXTRAS 变量来实现,该变量被 ament_package() 使用:
list(APPEND ${PROJECT_NAME}_CONFIG_EXTRAS path/to/file.cmake" other/pathto/file.cmake")或者,你可以直接将文件添加到 ament_package() 调用中:
ament_package(CONFIG_EXTRAS path/to/file.cmake other/pathto/file.cmake)添加到扩展点
Section titled “添加到扩展点”除了包含可在其他包中使用的函数的简单文件外,你还可以向 ament 添加扩展。这些扩展是在定义扩展点的函数执行时运行的脚本。ament 扩展最常见的用例是注册 rosidl 消息生成器:在编写生成器时,通常希望使用该生成器生成所有消息和服务,而无需修改消息/服务定义包的代码。这可以通过将生成器注册为 rosidl_generate_interfaces 的扩展来实现。
例如,参见:
ament_register_extension( "rosidl_generate_interfaces" "rosidl_generator_cpp" "rosidl_generator_cpp_generate_interfaces.cmake")它将包 rosidl_generator_cpp 的宏 rosidl_generator_cpp_generate_interfaces.cmake 注册到扩展点 rosidl_generate_interfaces。当扩展点被执行时,脚本 rosidl_generator_cpp_generate_interfaces.cmake 会被触发。具体来说,每当函数 rosidl_generate_interfaces 被调用时,就会执行该生成器。
除了 rosidl_generate_interfaces 之外,生成器最重要的扩展点是 ament_package,它会在 ament_package() 调用时执行脚本。此扩展点在注册资源时非常有用(见下文)。
ament_register_extension 是一个接受恰好三个参数的函数:
-
extension_point:扩展点的名称(大多数情况下是ament_package或rosidl_generate_interfaces之一) -
package_name:包含 CMake 文件的包名(即文件所在项目的项目名) -
cmake_filename:扩展点运行时执行的 CMake 文件
注意: 可以按类似于
ament_package和rosidl_generate_interfaces的方式定义自定义扩展点,但这几乎不需要。
在极少数情况下,定义一个新的 ament 扩展点可能会有意义。
扩展点可以在宏内注册,这样当相应的宏被调用时,所有扩展都会被执行。步骤如下:
-
为你的扩展定义并记录一个名称(例如
my_extension_point),这也是使用扩展点时传递给ament_register_extension宏的名称。 -
在需要执行扩展的宏/函数中调用:
ament_execute_extensions(my_extension_point)ament 扩展的工作方式是定义一个包含扩展点名称的变量,并用要执行的宏来填充它。调用 ament_execute_extensions 时,变量中定义的脚本会被依次执行。
在开发插件或允许使用插件的包时,通常需要从一个 ROS 包(例如插件)向另一个 ROS 包添加资源,比如使用 pluginlib 的工具的插件。
这可以通过 ament 索引(也称为”资源索引”)来实现。
ament 索引说明
Section titled “ament 索引说明”有关设计和意图的详细信息,请参见这里
原则上,ament 索引位于 install space 中的一个文件夹内。它包含以不同资源类型命名的浅层子文件夹。每个子文件夹中,每个提供该资源的包都通过名称以”标记文件”的方式被引用。该文件可以包含获取资源所需的任何内容(例如资源安装目录的相对路径),也可以为空。
举个例子,考虑为 RViz 提供 display 插件:
在一个名为 my_rviz_displays 的项目中提供由 pluginlib 读取的 RViz 插件时,你需要提供一个 plugin_description.xml 文件,该文件将被安装并被 pluginlib 用于加载插件。为此,通过以下方式将 plugin_description.xml 作为资源注册到 resource_index 中:
pluginlib_export_plugin_description_file(rviz_common plugins_description.xml)运行 colcon build 时,这会在 resource_index 的 rviz_common__pluginlib__plugin 子文件夹中安装一个名为 my_rviz_displays 的文件。rviz_common 中的 Pluginlib 工厂会从所有名为 rviz_common__pluginlib__plugin 的文件夹中收集导出插件的包的信息。pluginlib 工厂的标记文件包含 plugins_description.xml 文件的安装路径(标记文件名则是库名称)。有了这些信息,pluginlib 就能加载库,并从 plugin_description.xml 文件中获知要加载哪些插件。
作为第二个例子,考虑让你自己的 RViz 插件使用自定义网格(meshes)。网格在启动时加载,这样插件用户就不必自行处理,但这意味着 RViz 必须知道这些网格。为此,RViz 提供了一个函数:
register_rviz_ogre_media_exports(DIRECTORIES <my_dirs>)这会将目录作为 ogre_media 资源注册到 ament 索引中。简而言之,它会在名为 rviz_ogre_media_exports 的子文件夹中安装一个以调用该函数的项目命名的文件。该文件包含宏中列出的目录相对于安装路径的相对路径。启动时,RViz 会搜索所有名为 rviz_ogre_media_exports 的文件夹,并加载所提供文件夹中的所有资源。这些搜索操作使用 ament_index_cpp(Python 包使用 ament_index_py)完成。
在以下章节中,我们将探讨如何将自己的资源添加到 ament 索引中,并介绍相关最佳实践。
查询 ament 索引
Section titled “查询 ament 索引”如有需要,可以通过 CMake 查询 ament 索引中的资源。有以下三个函数可用:
ament_index_has_resource:检查资源是否存在,若存在则获取其前缀路径。参数如下:
-
var:输出参数。如果资源不存在则填充为 FALSE,否则填充为资源的前缀路径 -
resource_type:资源类型(例如rviz_common__pluginlib__plugin) -
resource_name:资源名称,通常等于添加了该类型资源的包的名称(例如rviz_default_plugins)
ament_index_get_resource:获取特定资源的内容,即 ament 索引中标记文件的内容。
-
var:输出参数。如果资源存在,则填充为资源标记文件的内容 -
resource_type:资源类型(例如rviz_common__pluginlib__plugin) -
resource_name:资源名称,通常等于添加了该类型资源的包的名称(例如rviz_default_plugins) -
PREFIX_PATH:要搜索的前缀路径(通常,默认的ament_index_get_prefix_path()就足够了)
注意,如果资源不存在,ament_index_get_resource 会抛出错误,因此可能需要先用 ament_index_has_resource 检查。
ament_index_get_resources:从索引中获取注册了特定类型资源的所有包
-
var:输出参数。填充为注册了该类型资源的所有包的名称列表 -
resource_type:资源类型(例如rviz_common__pluginlib__plugin) -
PREFIX_PATH:要搜索的前缀路径(通常,默认的ament_index_get_prefix_path()就足够了)
添加到 ament 索引
Section titled “添加到 ament 索引”定义资源需要两条信息:
-
资源的名称,必须唯一
-
标记文件的布局,可以是任意内容,也可以为空(例如,用于标记 ROS 2 包的 “package” 资源就是如此)
对于 RViz 网格资源,相应的选择是:
-
rviz_ogre_media_exports作为资源名称 -
包含资源的所有文件夹的安装路径相对路径。这已经能够让你在包中编写使用相应资源的逻辑。
为了让用户能够方便地为你的包注册资源,你还应该提供类似 pluginlib 函数或 rviz_ogre_media_exports 函数的宏或函数。
要注册资源,请使用 ament 函数 ament_index_register_resource,它会在 resource_index 中创建并安装标记文件。例如,rviz_ogre_media_exports 的相应调用如下:
ament_index_register_resource(rviz_ogre_media_exports CONTENT ${OGRE_MEDIA_RESOURCE_FILE})这会在 resource_index 的 rviz_ogre_media_exports 文件夹中安装一个以 ${PROJECT_NAME} 命名的文件,内容由变量 ${OGRE_MEDIA_RESOURCE_FILE} 给出。该宏支持以下常用参数:
-
第一个(未命名的)参数是资源名称,即 resource_index 中的文件夹名称
-
CONTENT:标记文件的内容(字符串)。可以是相对路径列表等。CONTENT不能与CONTENT_FILE同时使用。 -
CONTENT_FILE:用于创建标记文件的文件路径。文件可以是普通文件或通过configure_file()展开的模板文件。CONTENT_FILE不能与CONTENT同时使用。 -
PACKAGE_NAME:导出资源的包/库名称,即标记文件的名称。默认为${PROJECT_NAME}。 -
AMENT_INDEX_BINARY_DIR:生成的 ament 索引的基本路径。除非确实必要,否则始终使用默认的${CMAKE_BINARY_DIR}/ament_cmake_index。 -
SKIP_INSTALL:跳过安装标记文件。
由于每个包只有一个标记文件,如果同一个项目两次调用 CMake 函数/宏,通常会出现问题。但对于大型项目,最好将注册资源的调用分开。
因此,最佳实践是让注册资源的宏(如 register_rviz_ogre_media_exports.cmake)仅填充一些变量。然后,在 ament_package 的 ament 扩展中添加对 ament_index_register_resource 的实际调用。由于每个项目只能有一次 ament_package 调用,资源注册只会在一个地方进行。对于 rviz_ogre_media_exports,这相当于以下策略:
-
宏
register_rviz_ogre_media_exports接受一个目录列表并将它们追加到名为OGRE_MEDIA_RESOURCE_FILE的变量中。 -
另一个名为
register_rviz_ogre_media_exports_hook的宏在${OGRE_MEDIA_RESOURCE_FILE}非空时调用ament_index_register_resource。 -
register_rviz_ogre_media_exports_hook.cmake文件通过调用以下命令在第三个文件register_rviz_ogre_media_exports_hook-extras.cmake中注册为 ament 扩展:
ament_register_extension("ament_package" "rviz_rendering" "register_rviz_ogre_media_exports_hook.cmake")- 文件
register_rviz_ogre_media_exports.cmake和register_rviz_ogre_media_exports_hook-extra.cmake作为CONFIG_EXTRA注册到ament_package()中。
设置环境变量
Section titled “设置环境变量”ament_cmake 提供了一种机制,可以在 ROS 2 workspace 被 source 时自动设置环境变量。这在以下配置中非常有用:
-
RMW 实现(设置 CycloneDDS、FastDDS 等)
-
Gazebo 仿真(设置插件和资源的路径)
-
其他自定义机器人特定的设置配置
这可以通过 ament_environment_hooks 来实现,它允许包定义在 workspace 被 source 时设置的持久环境变量。
关于环境钩子
Section titled “关于环境钩子”环境钩子是由 ROS 2 包提供的 shell 脚本。当 workspace 中的 setup 文件被 source 时,钩子也会被 source。这些脚本允许你设置或扩展环境变量,无需手动修改 setup.bash 或 setup.zsh 文件。
这些环境钩子可以通过创建两种类型的脚本文件来实现:
-
.dsv.in文件:机器可读的文件,用于指定预期的环境变量更改。ament 处理这些文件的效率比传统 shell 脚本更高,从而提升了环境设置的性能。 -
.sh.in文件:由 Linux/macOS shell(如 sh、bash 和 zsh)执行的 shell 脚本。它们在 source workspace 时在运行时设置环境变量。
这些文件由 colcon 处理以生成最终的环境钩子脚本。
ament_environment_hooks 的实际实现可以在官方 ament-cmake 仓库中找到。
通过钩子定义持久环境变量
Section titled “通过钩子定义持久环境变量”本节提供一个快速示例,说明如何使用环境钩子为你的 ROS 2 包配置 FastDDS XML profiles。
定义环境钩子时的一个推荐最佳实践是将它们放在包 workspace 中的专用 hooks 目录中。
在你创建的 hooks 文件夹中,创建一个 my_package.sh.in 文件,内容如下:
export RMW_IMPLEMENTATION=rmw_fastrtps_cppexport RMW_FASTRTPS_USE_QOS_FROM_XML=1export FASTDDS_DEFAULT_PROFILES_FILE="$COLCON_CURRENT_PREFIX/my_dds_profile.xml"在同一文件夹中,创建一个 my_package.dsv.in 文件,内容如下:
set;RMW_IMPLEMENTATION;rmw_fastrtps_cppset;RMW_FASTRTPS_USE_QOS_FROM_XML;1set;FASTDDS_DEFAULT_PROFILES_FILE;my_dds_profile.xml添加完成后,你可以在 CMakeLists.txt 文件中使用 ament_environment_hooks 函数注册它们:
ament_environment_hooks( "${CMAKE_CURRENT_SOURCE_DIR}/hooks/my_package.dsv.in" "${CMAKE_CURRENT_SOURCE_DIR}/hooks/my_package.sh.in")另一个使用环境钩子设置 Gazebo 插件路径的示例可以在官方 ros_gz_project_template中找到。
API 版本管理
Section titled “API 版本管理”ROS 2 通过 ament_generate_version_header 提供自动版本头文件生成功能,用于创建编译时宏以实现 API 版本控制和特性检测。这对于维护向后兼容性和根据库版本有条件地启用功能特别有用。
注意:
ament_generate_version_header功能仅适用于 C、C++ 和其他基于 C 的语言。它生成包含预处理器宏的 C/C++ 头文件,不适用于 Python 或其他非基于 C 的包。
了解自动生成的版本宏
Section titled “了解自动生成的版本宏”许多 ROS 2 C/C++ 包(如 rclcpp、rcl 和 rmw)会自动生成版本头文件,其中包含暴露库版本信息的宏。这些版本头文件是使用 ament_generate_version_header.cmake 脚本从 package.xml 文件生成的。
生成的版本宏遵循以下命名约定:
-
<PACKAGE_NAME>_VERSION_MAJOR:主版本号 -
<PACKAGE_NAME>_VERSION_MINOR:次版本号 -
<PACKAGE_NAME>_VERSION_PATCH:补丁版本号 -
<PACKAGE_NAME>_VERSION:组合版本号(单个整数,major 10000 + minor 100 + patch) -
<PACKAGE_NAME>_VERSION_STR:版本的字符串表示(例如 “1.2.3”) -
<PACKAGE_NAME>_VERSION_GTE(major, minor, patch):用于检查版本是否大于或等于指定版本的宏
例如,rclcpp 提供以下宏:
-
RCLCPP_VERSION_MAJOR -
RCLCPP_VERSION_MINOR -
RCLCPP_VERSION_PATCH -
RCLCPP_VERSION -
RCLCPP_VERSION_STR -
RCLCPP_VERSION_GTE(major, minor, patch)
为你的包生成版本头文件
Section titled “为你的包生成版本头文件”要为你自己的包生成版本头文件,请在 CMakeLists.txt 中添加以下内容:
find_package(ament_cmake_gen_version_h REQUIRED)ament_generate_version_header(my_library)这会在 <build_dir>/my_library/version.h 处生成一个头文件,可以在你的代码中包含它:
#include "my_library/version.h"版本信息会自动从 package.xml 中的 <version> 标签中提取。
默认情况下,生成的头文件放在构建目录的 <package_name>/version.h 下。你可以自定义输出位置:
ament_generate_version_header(my_library HEADER_PATH "my_library/my_version.h")使用版本宏进行 API 协商
Section titled “使用版本宏进行 API 协商”版本宏支持运行时和编译时的特性检测,这对于在不同 ROS 2 发行版之间编写可移植代码至关重要。
虽然 ROS 2 保证在同一发行版内保持 ABI(应用程序二进制接口)兼容性,但新的接口和功能可能会被向后移植。这意味着在单个发行版内,根据安装的补丁版本不同,可能会有不同的 API 版本可用。版本宏允许开发者在使用特定功能前检查其是否可用。
示例:版本检查
Section titled “示例:版本检查”#include "rclcpp/version.h"
// 检查新功能是否可用#if RCLCPP_VERSION_GTE(28, 3, 0) use_new_api_with_feature();#else use_old_api_without_feature();#endif-
在使用新功能之前进行检查:当使用可能在旧版本库中不可用的功能时,始终使用版本宏。
-
提供回退实现:尽可能为旧 API 版本提供替代实现,以保持向后兼容性。
-
记录版本要求:在包文档中清楚地记录特定功能所需的最低版本。
-
跨版本测试:如果你的包需要支持多个 ROS 2 发行版,请针对最低支持版本进行测试。
-
使用 GTE 宏:优先使用
_VERSION_GTE(major, minor, patch)宏进行版本比较,相比手动比较各个版本组件,它提供了更清晰、更可读的语法。