ROS 编译系统 catkin 详解

引言

最近项目中遇到一个需求:将 C++ 程序 (不是 ROS node,只是普通的 C++ 程序)中的变量发布到 ROS topic 上,以便 ROS 中的其他 node 进行后续处理。

原 C++ 程序比较复杂,我们希望尽量少修改原程序,只要输出其中某些变量的值即可,不要大规模改写成 ROS node 的形式,不要新建 ROS package。

在以往使用 ROS 的过程中,我们一般是借助 catkin 来编译 ROS node C++ 程序。这可以看成是将 C++ 程序放入 ROS 框架中,以 ROS 的标准形式来编译。现在这个项目需求正好相反,我们要将 ROS 的相关库(library)嵌入到普通 C++ 程序中,采用 C++ 标准的 cmake 方式来编译。这就要求我们对 cmake 和 catkin 的关系有比较深入的了解。

在查找资料的过程中,我们发现了一篇讲解 catkin 编译系统的文章,从最基本的命令行编译方式,到 makefile 文件编译,再到 catkin 编译,每一步发展的必要性都讲解的很清楚,看完之后,我们对 catkin 有了更深入的认识。

本文是一篇学习笔记,也可以看成是对原文的意译。

对原文感兴趣的读者可以移步这里 http://jbohren.com/tutorials/2014-02-12-gentle-catkin-intro/

预安装

原文例子中使用了 hydro 版本的 ROS,现在看来比较古老了,这里替换为 kinetic 版本。

  • Ubuntu 16.04
  • ROS kinetic (base 即可)

从最简单的例子开始

首先创建一个文件夹 hello_world_tutorial,存放我们的程序

mkdir hello_world_tutorial
cd hello_world_tutorial

创建 C++ 源文件,名为 hello_world_node.cpp

// 为了与 ROS 交互,需要调用 ROS C++ APIs
#include <ros/ros.h>

// 标准的 C++ main 函数
int main(int argc, char** argv) {

  // 该命令告诉 ROS 初始化了一个 node,名为 hello_world_node 
  ros::init(argc, argv, "hello_world_node");

  // 在一般的 ROS node 程序中,我们会用 ros::NodeHandle nh 来启动 node 程序,
  // ros::NodeHandle nh 默认会调用 ros::start() 函数,程序关闭时也会自动调用 ros::shutdown() 函数。
  // 我们也可以直接通过 ros::start() 和 ros::shutdown() 来手动控制 node 的开启和关闭
  ros::start();

  // 显示 hello, world! 信息
  ROS_INFO_STREAM("Hello, world!");

  // 用 ros::spin() 保持该程序运行,一直等待处理 subscribe 的数据
  // 由于该程序并没有 sub,所以就是简单的保持程序不退出而已, 直到接受到终止信号 SIGINT (ctrl-c)
  ros::spin();

  // 关闭 node 程序
  ros::shutdown();

  // 结束主程序
  return 0;
}

下边将 C++ 源文件编译成可执行文件

g++ hello_world_node.cpp -o hello_world_node -I/opt/ros/kinetic/include -L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole

各参数含义

  • -I<dir> 指定头文件的搜索路径
  • -L<dir> 指定静态库的搜索路径
  • -Wl,-rpath,/opt/ros/kinetic/lib 指定共享库的搜索路径
  • -lroscpp -lrosconsole 指定需要链接的具体的库文件

编译之后,生成 hello_world_node 可执行文件。由于程序中生成了 ROS node,而 ROS node 需要与 ROS master 进行通讯注册,否则会报错。因此为了正常运行程序,需要先开启 ROS master

roscore

然后运行 hello_world_node

./hello_world_node

如果一切顺利,应该显示类似如下信息:

[ INFO] [1561908777.116073864]: Hello, world!

上述编译方式扩展性很差,对于如此简单的 hello_world 程序,需要设置的参数已经这么多了。而且在 terminal 中书写比较麻烦,修改也不方便。

改进:使用 Makefile 文件进行编译

Makefile 编译方式是将上述编译命令和参数设置放入一个文件中,然后基于该文件,完成编译过程。Makefile 文件有自己的一套语法规则,可以实现批量、相对自动化的编译。

与前述 hello world 程序对应的 Makefile 文件内容如下:

# 声明要使用的编译器
CC=g++
# 声明一些变量,实际上就是对应上述搜索路径设置
CFLAGS=-I/opt/ros/kinetic/include
LDFLAGS=-L/opt/ros/kinetic/lib -Wl,-rpath,/opt/ros/kinetic/lib -lroscpp -lrosconsole

# % 作为通配符,代表对一类满足条件的文件进行操作
# 这是由源文件 *.cpp 编译成目标文件 *.o 的操作
%.o: %.cpp
  $(CC) -c -o $@ $< $(CFLAGS)  

# 也可以不用通配符,具体写出要编译的文件
# 这是由目标文件 *.o 通过链接 (linking) 操作生成最终的可执行文件
hello_world_node: hello_world_node.o 
  $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)

对于 Makefile 的介绍,可以参考这里

Makefile 文件的基本格式是

target: pre-req
    command

即,希望生成 target 文件,依赖 pre-req 文件,通过 command 命令实现。

需要注意的是,Makefile 要求 command 那一行开头用 TAB 键缩进,不能用空格,如果出现如下报错:

makefile:...: *** missing separator. Stop

说明误用了空格键。如果你跟我一样用的是 vs code 编辑器,可以在右下角选择 Indent Using Tabs

将上述 Makefile 文件放在与 hello_world_node.cpp 同一路径下,然后编译

make  # 或者指明某个 target 编译任务,如:  make hello_world_node

Makefile 编译方式相比于刚才的命令行编译方式有如下优点:

  • 在设置好 Makefile 的前提下,编译命令更简单,只需要 make,不必每次都输入一长串命令
  • Makefile 中将编译和链接分开进行,如果项目中包含多个 c++ 源文件,改动了其中的一个,只需要重新生成改动文件的目标文件 (*.o) 即可,其他源文件不需要重新编译,然后基于更新之后的目标文件,生成新的可执行文件。也就是说,如果源文件没有改变,就不会浪费时间更新目标文件。

在书写上边的 Makefile 文件时,我们依然要明确设定头文件和 library 的搜索路径。为了进一步简化这个过程,我们可以在 Makefile 中使用 pkg-config 设置搜索路径。

改进:在 Makefile 中使用 pkg-config 设置搜索路径

实际上,library 对应的搜索路径包含在与该 library 对应的.pc 文件中,例如
roscpp library 对应的 .pc 文件为 /opt/ros/kinetic/lib/pkgconfig/roscpp.pc,里面内容如下

prefix=/opt/ros/kinetic

Name: roscpp
Description: Description of roscpp
Version: 1.12.14
Cflags: -I/opt/ros/kinetic/include -I/usr/include
Libs: -L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so  ...
Requires: cpp_common message_runtime rosconsole roscpp_serialization ...

可以看出,这个 .pc 文件里面的 CflagsLibs 条目就是调用 roscpp 时要设置的路径信息。我们可以通过 pkg-config 这个工具查找 roscpp.pc 文件,然后提取其中的路径信息,放入 Makefile 中,这样就避免了手动输入。例如

$ pkg-config --cflags roscpp

-I/opt/ros/kinetic/include

$ pkg-config --libs roscpp

-L/opt/ros/kinetic/lib -lroscpp -lpthread /usr/lib/x86_64-linux-gnu/libboost_chrono.so ...

因此,我们可以改写 Makefile 文件如下:

CC=g++

# 通过 pkg-config 设置相应的路径信息
CFLAGS=$(shell pkg-config --cflags roscpp)
LDFLAGS=$(shell pkg-config --libs roscpp)

%.o:  %.cpp
    $(CC) -c  -o  $@  $<  $(CFLAGS) 
hello_world_node: hello_world_node.o 
    $(CC) -o hello_world_node hello_world_node.o $(LDFLAGS)

然后依然用 make 命令编译文件,与前边编译方式相同,最终也是生成 hello_world_node 可执行文件。

在使用 pkg-config 时需要确保它能够找到相应的 library。pkg-config 有自己的搜索 library 的路径,存放在环境变量 PKG_CONFIG_PATH中,可以通过 echo 命令查看

echo $PKG_CONFIG_PATH

如果我们安装完 ROS,并且运行了source /opt/ros/kinetic/setup.bash, ROS 相关的 library 对应的 .pc 文件就被加入了 pkg-config 的搜索路径。通过 pkg-config <library> 就可以搜到相应的信息。

尽管 pkg-config 简化了 Makefile 中设置头文件和 library 路径的过程,但是 Makefile 文件中后续的编译过程依然需要手动设置。另外这里手动书写的编译命令是与操作系统平台相关的,Linux 中的编译命令不能在 Windows 中使用,这就导致 Makefile 不能跨平台使用。

改进: CMake 跨平台编译方式

CMake 的一个功能是自动生成 Makefile 文件。另外,CMake 可以在 Linux 、Windows 和 Mac OS 上使用。

要使用 CMake,首先要创建一个 CMakeLists.txt 文件,包含必要的编译设置。
与上述 hello_world_node 例子对应的 CMakeLists.txt 内容如下:

# 声明 CMake API 版本
cmake_minimum_required(VERSION 2.8)

# 声明项目名称
project(hello_world_tutorial)

# 搜索依赖 library (即 roscpp) 的信息
# 与 pkg-config 功能类似,但可以跨平台使用
# pkg-config 查找 .pc 配置文件,而 find_package 查找 .cmake 配置文件
find_package(roscpp REQUIRED)

# 搜索 roscpp 中调用的头文件
include_directories(${roscpp_INCLUDE_DIRS})

# 设置待生成的可执行文件名字 
add_executable(hello_world_node hello_world_node.cpp)

# 设置编译过程中 linking library 
target_link_libraries(hello_world_node ${roscpp_LIBRARIES})

其中 find_package(roscpp REQUIRED) 会自动定义几个变量,包括 roscpp_INCLUDE_DIRSroscpp_LIBRARY_DIRSroscpp_LIBRARIES。在 CMakeLists.txt 中可以直接使用这些变量。REQUIRED 参数的作用是在找不到相应 library 时停止并报错,提示

-- Configuring incomplete, errors occurred!
See also ".../CMakeFiles/CMakeOutput.log".

如果不加 REQUIRED,则只会提示找不到 library,整个过程并不会停止,显示信息如下:

-- Configuring done
-- Generating done
-- Build files have been written to: ...

尽管显示各种 done,由于没有找到必要的 library ,后续的编译肯定会不成功。

通过 CMakeLists.txt 进行编译时会产生一些中间文件,如果都放在 .cpp 源文件目录下,会显得很杂乱。最好单独建一个文件夹,存放这些编译文件。例如在 .cpp 源文件和 CMakeLists.txt 同一路径下新建 build 文件夹。新的路径结构如下:

├── build
├── CMakeLists.txt
└── hello_world_node.cpp

CMakeLists.txt 中的 find_package 之所以能找到相应的 library,是因为已经设置了搜索路径,存放在环境变量 CMAKE_PREFIX_PATH 中,通过 echo $CMAKE_PREFIX_PATH 可以显示当前 find_package 使用的搜索路径。在安装完 ROS 之后,source 命令会自动将 ROS 相关的 library 加入上述搜索路径中。

通过 cmakeCMakeLists.txt 自动生成编译文件 Makefile:

cd build       # 进入刚才创建的 build 文件夹
cmake ..      # 运行 cmake,它会调用上一层路径中的 CMakeLists.txt 文件 

运行完上述命令以后,产生了一些新文件,路径结构如下:

├── build
│   ├── CMakeCache.txt
│   ├── CMakeFiles
│   ├── cmake_install.cmake
│   └── Makefile
├── CMakeLists.txt
└── hello_world_node.cpp

可以看到自动产生了 Makefile ,此时就可以用 make 命令编译文件了。

这里借张图展示一下 CMake 编译方式跨平台的能力
( From: https://cgold.readthedocs.io/en/latest/overview/cmake-can.html)

cmake.png

到了这里,我们就已经解决了最初的项目需求:让 C++ 程序将内部变量以 ros topic 的形式发布出来。基本步骤:
1 . 改写 C++ 程序,加入 ROS 元素,如 ros 头文件,msg 头文件等,设置 ros::init,
ros::NodeHandle ,pub msg 等,这些 ROS 元素可以使 C++ 程序在 ROS master 中以 ROS node 的形式注册。
2. 我们原来的 C++ 程序有自己的 CMakeLists.txt 文件,在其中添加依赖的 ROS library。
3. 用基本的 cmake 方式编译即可。

改进:针对 ROS 系统的 Catkin 编译方式

ROS 的 Catkin 编译系统的一个特点是将程序做成 package (称为 catkin package 或者 ROS package) 的形式,可以理解成模块化。典型的 ROS workspace 中包含 src, build, devel 三个文件夹,在分享时只需要分享 src 中的某个 package 即可,所有的编译信息都在此 package 中。一个 package 在编译时可以指定依赖于另一个 package。
另外,由于 ROS 中程序以及 library 变动比较频繁,不太适合在整个系统层面安装编译之后的文件,通过 source devel 文件中的 setup.bash 文件可以告知系统去哪里查找相应的文件,避免了系统级的安装 。

要构造 ROS package,我们首先要修改 CMakeLists.txt 文件如下:

cmake_minimum_required(VERSION 2.8)
project(hello_world_tutorial)

#  要用到 catkin
find_package(catkin REQUIRED)

# 声明该项目为一个 catkin package
catkin_package()

find_package(roscpp REQUIRED)
include_directories(${roscpp_INCLUDE_DIRS})
add_executable(hello_world_node hello_world_node.cpp)
target_link_libraries(hello_world_node ${roscpp_LIBRARIES})

另外,还需要添加一个 package.xml 文件,指明该 package 在编译和运行时依赖于哪些其他 package,同时也包含该 package 的一些描述信息,如作者、版本等。内容如下:

<package>
  <name>hello_world_tutorial</name>
  <maintainer email="you@example.com">Your Name</maintainer>
  <description>
    A ROS tutorial.
  </description>
  <version>0.0.0</version>
  <license>BSD</license>

  <!-- Required by Catkin -->
  <buildtool_depend>catkin</buildtool_depend>

  <!-- Package Dependencies -->
  <build_depend>roscpp</build_depend>
  <run_depend>roscpp</run_depend>
</package>

现在路径结构如下:

├── build
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml

跟之前一样,进入 build 文件夹中,用 cmake + make 方式编译

cd build
cmake ..
make

编译结束之后会发现,并没有在 build 根目录下生成可执行文件。与普通的 cmake 编译不同,catkin 编译会生成一个 devel 文件夹,这里包含了生成的可执行文件,以及作为 library 使用的配置文件 .pc.cmake

对于我们的 hello_world_node package 来说,上述文件路径如下:

  • 可执行文件:devel/lib/hello_world_tutorial/hello_world_node
  • .pc 配置文件:devel/lib/pkgconfig/hello_world_tutorial.pc
  • .cmake 配置文件:devel/share/hello_world_tutorial/cmake/hello_world_tutorialConfig.cmake

当作为 library 使用时,只需要将路径 .../devel/lib/pkgconfig 添加到 PKG_CONFIG_PATH 环境变量中,或者将 .../devel 添加到 CMAKE_PREFIX_PATH 变量中。实际上,我们不需要手动设置这些环境变量,只需要通过 source devel 文件夹下的 setup.bash 文件即可,source setup.bash 不仅添加了以上两个环境变量,还有诸如 ROS_PACKAGE_PATHPYTHONPATH等。

source 之后,由于该 package 加入了 ROS_PACKAGE_PATH,此时可以通过 ROS 相关的命令对该 package 进行操作,如 rospack find ..., rosrun <package> <exe>, roscd <package> 等。

为了更有条例地存放不同类型的文件,可以建立三个文件夹 src, build, devel,其中 src 存放源文件,源文件又以 package 为单位分别存放,build 存放编译过程中的中间文件,devel 存放最终生成的可执行文件和配置文件。这就是所谓的 out-of-source 编译方式。在分享、发布程序时,我们可以很清楚的知道哪些是必要的源文件,哪些是最终生成的可执行文件和 library,哪些是作为副产品存在的中间文件。

路径结构如下:

├── build
├── devel
└── src
    └── hello_world_tutorial
        ├── CMakeLists.txt
        ├── hello_world_node.cpp
        └── package.xml

在做了以上路径设置之后,在编译时,我们就需要特别指定各类文件对应的路径:

cd build
cmake ../src/hello_world_tutorial  -DCATKIN_DEVEL_PREFIX=../devel
make

catkin 的特点还体现在编译多个 package 中。

我们可以在 src 文件夹中再添加一个 catkin package,这里我们就直接从网上下载一个简单的 package:

git clone https://github.com/ros/robot_state_publisher.git -b kinetic-devel

现在路径结构如下:

├── build
├── devel
└── src
    ├── hello_world_tutorial
    │   ├── CMakeLists.txt
    │   ├── hello_world_node.cpp
    │   └── package.xml
    └── robot_state_publisher
        ├── CHANGELOG.rst
        ├── CMakeLists.txt
        ├── doc.dox
        ├── include
        ├── package.xml
        ├── src
        └── test

上述两个 package 各有一个 CMakeLists.txt 文件,按照普通的 cmake 方法,我们不能同时编译它们。catkin 为我们提供了一个更高层的 CMakeLists.txt 文件,可以从 ROS 安装文件夹中以超链接的形式复制过来,放在更高层的 src 目录下:

cd src
ln  -s  /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake  CMakeLists.txt

实际上,ROS 为我们提供了专门的命令,实现上述操作:

cd src
catkin_init_workspace src

此时,路径结构如下:

├── build
├── devel
└── src
    ├── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake
    ├── hello_world_tutorial
    └── robot_state_publisher

这就是典型的 ROS workspace 的结构。
此时就可以使用 cmake 同时编译 src 中的所有 package 了,命令如下:

cd build
cmake ../src  -DCATKIN_DEVEL_PREFIX=../devel
make

将以上三个命令合并在一起就是 ROS 中的 catkin_make 命令。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容