计算机中,有些文件专门用于存储可以重复使用的代码块,例如功能实用的函数或者类,我们通常将它们称为库文件,简称“库”(Library)。
//myMath.c
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int mul(int a, int b) {
return a * b;
}
int div(int a, int b) {
if (b != 0) {
return a / b;
}
return -1;
}
myMath.c 文件中包含 4 个函数,它们分别可以完成两个整数的加法、减法、乘法和除法运算。myMath.c 库文件的用法也很简单,直接将它添加到某一个 C 语言项目中,就可以直接调用文件中的 4 个函数,每个函数可以调用多次。
显然,实际开发中引入他人编写好的库文件可以省略某些功能的开发环节,提高项目的开发效率。但遗憾的是,类似 myMath.c 这种“开源”的库文件很难找到,多数程序员并不会直接分享源代码,他们更愿意分享库文件的二进制版本——链接库。
所谓链接库,其实就是将开源的库文件(例如上面提到的 myMath.c)进行编译、打包操作后得到的二进制文件。虽然链接库是二进制文件,但无法独立运行,必须等待其它程序调用,才会被载入内存。
一个完整的 C++ 语言项目可能包含多个 .cpp 源文件,项目的运行需要经过“编译”和“链接”两个过程:
1. 编译:由编译器逐个对源文件做词法分析、语法分析、语义分析等操作,最终生成多个目标文件。每个目标文件都是二进制文件,但由于它们会相互调用对方的函数或变量,还可能会调用某些链接库文件中的函数或变量,编译器无法跨文件找到它们确切的存储地址,所以这些目标文件无法单独执行。
2. 链接:对于各个目标文件中缺失的函数和变量的存储地址(后续简称“缺失的地址”),由链接器负责修复,并最终将所有的目标文件和链接库组织成一个可执行文件。
库文件分几种,一种是资源类型的,就是存放的东西,供外部调用的没有函数,只有变量或类的实体;另一种是有简单的函数供外部调用;还有一种,也是用的比较多的,就是包含一个完整的模块,紧提供一个外部调用的接口,当外部调用该接口就开始运行这个模块,外部几乎就处于休眠状态;
目标文件就是编译过程中产生的,链接起来生成可执行文件的。目标文件包含着机器代码,可直接被计算机中央处理器执行,以及代码在运行时使用的数据,如重定位信息,如用于链接或调试的程序符号,变量和函数的名字,此外还包括其他调试信息。
① 预处理过程
预处理相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理,内容如下:
1. 将所有的“#define”删除,并且展开所有的宏定义;
2. 处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉;
3. 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置(注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件);
4. 删除所有的注释;
5. 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生的编译错误或警告时能够显示行号;
6. 保留所有的#pragma编译器指令。
② 编译过程
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
③ 汇编过程
将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可。
④ 链接(build)过程
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
同时在代码处理过程中,代码编译器、汇编器、链接器也发挥了其相应的作用:
依赖项就是设定项目所依赖的项目,以决定具体生成解决方案时项目编译的顺序(一般一个解决方案会有很多项目组成)。
通常来说,依赖项取决于这个项目引用的组件和项目,系统可以自己决定。
作用就是让系统知道你的项目a依赖于项目b,也就是说项目b会在a之前编译(因为依赖的关系,所以系统觉得应该先有b,这样才能有a)。
上图中的箭头指向代表着函数的调用关系,上图展示的是函数之间的依赖关系。
要了解CMakelist.txt文件,首先我们先了解一下Makefile。Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。
Cmake的所有语句都写在一个CMakeLists.txt的文件中,CMakeLists.txt文件确定后,直接使用Cmake命令进行运行,但是这个命令要指向CMakeLists.txt所在的目录,Cmake之后就会产生我们想要的makefile文件,然后再直接make就可以编译出我们需要的结果了。
更简单的解释就是Cmake是为了生成Makefile而存在,这样我们就不需要再去写Makefile了,只需要写简单的CMakeLists.txt即可。
CMakelist.txt中比较常用的几大块内容按顺序依次为:声明srv/msg/action消息文件及将其转化为特定编程语言所需的功能包、catkin工程项目文件编译链接配置。我们查看CMakelist.txt文件可以知道,CMakelist.txt总共包含以下内容:
1. 编译、链接、构建可执行文件所需的最低CMake版本 (cmake_minimum_required);
2. 功能包的名称 (project(package_name)),在CMakelist.txt文件中我们常常可以看见${PROJECT_NAME},这个其实就是代表着功能包的名称: package_name;
3. 这里指明构建这个功能包需要依赖的其他功能包 (find_package());
4. 启动Python模块支持 (catkin_python_setup());
5. 消息/服务/操作(Message/Service/Action)生成器,当我们需要使用.msg.srv.action形式的文件时,我们需要特殊的预处理器把他们转化为系统可以识别特定编程语言(.h.cpp) (add_message_files(), add_service_files(), add_action_files())
6. 在此处列出的任何添加的消息和服务生成的依赖项(generate_messages());
7. 指明构建makefile文件所需的功能包 (catkin_package());
8. 用来设置头文件的相对路径 (include_directories());
9. 添加要编译的库和可执行文件 (add_library()/add_executable()/target_link_libraries());
1. 指明构建库文件所需的功能包
find_package(catkin REQUIRED COMPONENTS
roscpp
std_msgs
message_generation
actionlib
actionlib_msgs
)
主要用于指明:这里指明构建这个package需要依赖的package,我们使用catkin_make的编译方式,至少需要catkin这个包。
一个包被被find_package,那么就会导致一些CMake变量的产生,这些变量后面将在CMake的脚本中用到,这些变量描述了所依赖的包输出的头文件、源文件、库文件在哪里。这些变量的名字依照的惯例是
需要的所有包我们都可用这种方式包含进来,比如我们还需要roscpp,rospy,std_msgs。我们可以写成:
find_package(catkin REQUIRED)
find_package(roscpp REQUIRED)
find_package(rospy REQUIRED)
find_package(std_msgs REQUIRED)
这样的话,每个依赖的package都会产生几个变量,这样很不方便。所以还有另外一种方式:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
)
这样,它会把所有pacakge里面的头文件和库文件等等目录加到一组变量上,最终就只产生一组变量。例如:catkin_INCLUDE_DIRS(代指./devel/include目录)这样,我们就可以用这个变量查找需要的文件了。
2. 指明系统依赖项
find_package(Boost REQUIRED COMPONENTS system)
这个依赖项是我们在ROS中使用boost库时需要声明的。如果我们使用boost库还需要声明以下内容:
// 由于源文件的编写中调用了boost库,因此需要加载系统依赖项——boost库
find_package(Boost REQUIRED COMPONENTS system)
// 包含boost导出的.h头文件
include_directories(
include
${catkin_INCLUDE_DIRS}
${Boost_INCLUDE_DIRS}
)
// 将二进制可执行文件链接boost库
target_link_libraries(demo ${catkin_LIBRARIES} ${Boost_LIBRARIES})
3. 自定义消息文件声明与处理
# Generate messages in the 'msg' folder
// 这里的FILES代表着Message1.msg Message1.msg...
add_message_files(
FILES
Message1.msg
Message2.msg
)
# Generate services in the 'srv' folder
// 这里的FILES代表着Service1.srv Service2.srv...
add_service_files(
FILES
Service1.srv
Service2.srv
)
# Generate actions in the 'action' folder
// 这里的FILES代表着Action1.action Action2.action...
add_action_files(
FILES
Action1.action
Action2.action
)
// 总之当我们调用如上add_action_files、add_service_files、add_message_files之后
// FILES标识符就代表了我们自定义的所有消息文件(包括:.srv、.msg、.action)
# Generate added messages and services with any dependencies listed here
// DEPENDENCIES代表std_msgs生成的静态库(静态依赖项) ,用于将自定义文件生成C++头文件
generate_messages(
DEPENDENCIES
std_msgs
)
这些macros宏通过生成msg、srv、action的相关编程语言文件
当我们需要使用.msg .srv .action形式的文件时,我们需要特殊的预处理器把他们转化为系统可以识别特定编程语言(.h/.cpp)。 系统会用里面所有的(一些编程语言)生成器(比如 gencpp, genpy, genlisp, etc)生成相应的.cpp .py文件。这就需要三个宏:add_message_files, add_service_files,add_action_files来相应的控制.msg .srv .action。这些宏后面必须跟着一个调用generate_messages() 用来处理使用三个宏指明的消息文件使之生成符合C++标准(或者其他编程语言标准)的头文件和源文件。
4. 指明编译构建C++源文件所需的功能包
catkin_package()是catkin提供的CMake宏,用于为catkin提供构建、生成pkg-config和CMake文件所需要的信息。形式如下所示:
catkin_package(
INCLUDE_DIRS include
LIBRARIES amcl_sensors amcl_map amcl_pf
CATKIN_DEPENDS roscpp std_msgs message_runtime
DEPENDS Boost
)
INCLUDE_DIRS:表明我们这个功能包的.h文件都存放在这个功能包下面的include文件夹下,这里采取的是相对路径,因此只需要声明功能包中头文件所在的相对路径即可;
LIBRARIES:指明需要依赖该功能包的其他功能包,如上所示amcl_sensors amcl_map amcl_pf这三个功能包依赖本功能包;
CATKIN_DEPENDS:在catkin编译系统中,编译本功能包所需的catkin官方的依赖功能包;
DEPENDS:在编译这些源文件时,所需要的非catkin官方的依赖功能包;
我们一看见DEPENDS有些懵,什么叫做“非catkin官方的依赖功能包“?其实非catkin官方的依赖功能包就是例如Boost库、OpenCV库这样的,可用于C++编程的库。我们要想实现功能就一定会借助像OpenCV、Boost这样的库,这个依赖项就是用来声明这个功能包所用到的除了catkin官方给出的库以外的库。
DEPENDS的作用又是什么呢?
在功能包A的CMakelist.txt中我们设置了:
1. find_package设置:
find_package{Boost REQUIRED}
2. catkin_package设置:
catkin_package{
…
DEPENDS Boost
}
又由于功能包B依赖于功能包A,因此功能包B无需再用find_package{Boost REQUIRED}来包含boost依赖库了,如果DEPENDS中未包含boost,那么功能包B必须包含加载boost库所需的一切操作,即不能够继承功能包A所带来的便利。
5. 头文件声明函数
include_directories(
include
${catkin_INCLUDE_DIRS}
)
这个的含义在于:告知catkin编译器,要找头文件一方面去本功能包下的include目录去找,另外也去catkin_INCLUDE_DIRS这个目录下去找。catkin_INCLUDE_DIRS目录下存储着着由catkin功能包编译而成的.h文件,因此以“catkin功能包名称+_INCLUDE_DIRS”组成。
以此类推,要是find_package{Boost REQUIRED},那boost库中的.h文件存放路径为Boost_INCLUDE_DIRS。当我们调用boost库的.h文件时,只需加载${Boost_INCLUDE_DIRS}即可。
6. 目标文件名字的修改(没必要可不用修改,一般用不上)
目标文件的命名很重要,在catkin中目标文件的名字必须是唯一的,和你之前构建产生的和安装的都不能相同。这只是cmake内部的需要。可以利用set_target_properties()函数将以个target进行重命名。例如:
set_target_properties(rviz_image_view
PROPERTIES OUTPUT_NAME image_view
PREFIX "")
这样就可旧把目标文件名从rviz_image_view 改为了image_view。其中,我们还看见有个参数:prefix,这个参数译为“前缀”,ROS中常说的给XXX节点添加YYY前缀,其实就是将XXX节点置于YYY命名空间之下,例如:
set_target_properties(rviz_image_view
PROPERTIES OUTPUT_NAME image_view
PREFIX "my_namesapce")
上述代码有以下作用:
1)把目标文件名从rviz_image_view 改为了image_view;
2)节点名称,即目标文件名从/rviz_image_view变为了/my_namespace/image_view,这样更加可避免文件名重名(我们运行节点之后,可以使用rosnode list命令查看所有node的名称,可以看到如上所示已经修改的/my_namespace/image_view节点名称)。
加前缀的方式为catkin编译目录下文件不重名提供了有一层保障。
7. 声明C++库
add_library(${PROJECT_NAME}
src/${PROJECT_NAME}/test01.cpp
)
我们一般编写C++程序时,自定义类类型的定义与自定义类类型的实例化不在一个.cpp源文件中,因此要在XXX.cpp中使用到我们自定义的数据类型就必须要将我们自定义数据类型所在文件(也成为库文件)进行声明。要是不声明,编译器根本找不到自定义数据类型所在的文件。
## 声明C++库
add_library(head
include/test_head_src/haha.h
src/haha.cpp
)
我们上述代码就声明了一个库文件,这个库文件整体名称映射为head,这个映射名称就代表XXX.h和XXX.cpp组成的库文件。
更高级的应用:可以指出链接的属性。
add_library{...}一般什么时候使用呢?
C++代码编写风格一般有如下两种:
编写C++代码时,我们常常将“类/结构体的声明、定义和使用“相互剥离,在ROS项目文件中我们也可以这样做。在ROS项目文件中声明、定义、使用自定义数据类型时,我们可以进行如下两种方式的操作:
① 自定义数据类型的声明+自定义数据类型的定义及使用
② 自定义数据类型的声明+自定义数据类型的定义+自定义数据类型的使用
当库的定义和调用分开时(即第二种情况:自定义数据类型的定义(库文件的定义)和调用分开编写在不同文件中),我们才使用add_library{...}去告知catkin编译器(ROS使用的编译器):有一个自定义数据类型库文件是由XXX.h和XXX.cpp文件构成的,把它加载到库文件目录中以后用的时候调用即可。
8. 指明可执行文件/头文件的依赖项(所依赖的功能包)
add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
其中参数解析如下所示:
${${PROJECT_NAME}_EXPORTED_TARGETS}:如果在编译包或者执行文件时,需要用到msg和srv,就要显示调用由message_generation自动生成的用于编译消息文件的依赖项,对应于不同消息_EXPORTED_TARGETS也不同(前面的${${PROJECT_NAME}...}不变):
// .srv/.msg/.action(新版)
_gencpp
// .srv/.msg/.action(旧版)
_generate_messages_cpp
${catkin_EXPORTED_TARGETS}:用于为add_excutable()中映射的可执行文件提供catkin官方依赖包。
可用于为可执行文件和头文件添加依赖项:
// exec_name为add_executable(exec_name ...)中映射的可执行文件名
add_dependencies(exec_name ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
// lib_name为add_libraries(lib_name ...)中映射的可执行文件名
add_dependencies(lib_name ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
9. 指明需要编译的可执行文件名
不被包含在add_execuatble()中的.cpp文件不会被编译链接,形式如下:
add_executable(${PROJECT_NAME}_node src/ultrasonic_data_node.cpp)
第一个参数为期望生成的可执行文件名称;后面的参数为参与编译的源文件(cpp),如果需要多个代码文件,用空格区分开。
add_executable(exec_name src/a.cpp src/b.cpp …)
exec_name不是代表一个.cpp源文件而是src/a.cpp src/b.cpp……一堆.cpp源文件。但是一般来说,我们需要一个一个添加:
// 只编译cpp文件,不编译.h头文件
add_executable(msg_node1 src/pub.cpp)
add_executable(msg_node2 src/sub.cpp)
否则编译器会报错:
错误:有多个main函数。因为add_executable这个函数是用来将.cpp文件编译为二进制目标文件用的,因此如果add_executable{...}中包含了多个.cpp源文件会导致有多个main函数的出现。
10. 指定要链接库或可执行目标的库
使用target_link_libraries函数来指定可执行文件链接的库,这个要用在add_executable()后面。形式如下:
// 为可执行文件指定链接规则
target_link_libraries(executableTargetName
${catkin_LIBRARIES}
)
// 头文件无需指定连接规则
可以为一个或多个可执行文件链接多个库:
target_link_libraries(executableTargetName lib1 lib2 ... libN)
${catkin_LIBRARIES}表示由catkin功能包编译而成的可执行库。
一般指定链接规则之前,一定要用add_dependancies设置依赖项,例如:
// exec_name为add_executable(exec_name ...)中映射的可执行文件名
add_dependencies(exec_name ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
// 为exec_name添加依赖项:catkin_LIBRARIES
target_link_libraries(exec_name
${catkin_LIBRARIES}
)
${...}的存在可以区分谁到底是谁链接谁,即谁的运行以谁的运行完毕为前提。
上述代码指出:
// exec_name所表示的源文件链接的前提是catkin已经编译完毕
add_dependencies(exec_name ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
// 将已经编译完毕的catkin生成库文件链接exec_name可执行文件
target_link_libraries(exec_name
${catkin_LIBRARIES}
)
附加知识:
1. 动态调参配置:
generate_dynamic_reconfigure_options(
cfg/DynReconf1.cfg
cfg/DynReconf2.cfg
)
详见:
ROS探索总结(四十)——dynamic reconfigure - 古月居 (guyuehome.com)https://www.guyuehome.com/11732. 消息文件间依赖关系的声明(A.msg中包含B.msg)
ROS教程3 ROS自定义msg类型及使用 - MKT-porter - 博客园 (cnblogs.com)https://www.cnblogs.com/gooutlook/p/7401590.html
如果我们要辨析catkin_packages和find_packages中包含的功能包为何不同,我们可以了解一下“静态库和共享库的区别”:
1. 何为库?
在windows平台和linux平台下都大量存在着库,本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行
2. 何为静态库以及静态库的作用?
静态库就是一些目标文件的集合,以.a结尾。静态库在程序链接的时候使用,链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行程序的时候就不需要静态库了。由于每个使用静态库的应用程序都需要拷贝所用函数的代码,所以静态链接的文件会比较大。
3. 何为共享库以及共享库的作用?
共享库以.so结尾. (so ==share object) 在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块。所以,应用程序在运行的时候仍然需要共享库的支持。 共享库链接出来的文件比静态库要小得多。
总之,静态库是代码库可供我们调用,而共享库是编译了之后的二进制文件供编译使用。
说完了库,我们再来看运行依赖项和执行依赖项:
构建依赖项其实就是静态库,在构建程序的过程中将.srv、.msg、.action等这些文件转化为符合C++(或其他编程语言)的头文件(我们将自定义消息文件转化为了类类型定义存储在头文件中),“转化”的含义:将静态库中的代码用替换的方式将自定义文件中的对应部分进行替换得到了符合C++标准的头文件。
执行依赖项其实就是共享库(也称为动态库,后缀为.so),构建可执行文件的过程也是共享库链接由源文件编译而来的二进制文件的过程。
find_packages{...}包含的是静态库文件,主要用途是将自定义消息文件使用ROS提供的库文件转化为符合C++标准的头文件(仅以C++为例)。
catkin_packages{...}包含的是共享库文件(动态库文件),主要用途是在编译过程中将二进制共享库文件链接进“由源文件编译而来的XXX.o二进制目标文件”之中。
一般来讲,ROS中像roscpp这样的库不仅存在相应的静态库也存在相应的共享库(动态库),但有时也有例外:在消息文件的处理过程中,generation_messages依赖项使用roscpp静态库将自定义消息文件转化为C++头文件并存放在./devel/include目录之下(经此操作自定义文件转化为了C++头文件),而在进行二进制文件的链接操作时用到的功能包是runtime_messages依赖项,该依赖项使用roscpp二进制共享库进行编译时的链接操作(经此操作.o二进制文件转化为了.exe可执行文件)。
详细可参考:
1. stack overflow上的解释(英文版解释):
compilation - What is the difference between build dependency and runtime dependency - Stack Overflowhttps://stackoverflow.com/questions/51433878/what-is-the-difference-between-build-dependency-and-runtime-dependency
2. ROS wiki上的官方说明:
catkin/package.xml - ROS Wikihttp://wiki.ros.org/catkin/package.xml
3. 码农家园上的中文版解释(为stack overflow上的解释的中文版):
关于编译:构建依赖项和运行时依赖项有什么区别 | 码农家园 (codenong.com)https://www.codenong.com/51433878/
package.xml中重点说明一下构建依赖项,导出依赖项 ,执行依赖项的指定: