1. 理解ROS2中.so库集成的核心概念
在ROS2开发中,我们经常会遇到需要复用已有C++代码的情况。这时候,将代码编译成.so动态链接库就成了一个非常实用的选择。简单来说,.so文件就像是Windows系统中的.dll文件,它包含了编译好的二进制代码,可以在不同的程序之间共享使用。
我自己在实际项目中就经常遇到这种情况:有些核心算法或者工具类需要在多个ROS2节点中使用,如果每次都重新编译这些代码,不仅浪费时间,还容易导致版本不一致的问题。把这些功能封装成.so库,就像是在代码库中建立了一个共享工具箱,随用随取,非常方便。
ROS2提供了两种主要的.so库集成方式:手动复制方式和ament导出方式。手动复制就像是你自己把工具从工具箱里拿出来放到工作台上,简单直接但不够灵活;ament导出方式则像是给工具贴上了标签并登记在册,系统能自动帮你找到和管理这些工具。两种方式各有优劣,适用于不同的场景。
理解这两种方式的区别很重要,因为这直接影响到你的项目结构和维护成本。手动复制方式适合小型项目或者快速原型开发,而ament导出方式更适合中大型项目,特别是需要多人协作或者有复杂依赖关系的场景。
2. 项目环境准备与基础配置
在开始集成.so库之前,我们需要先搭建好开发环境。首先确保你已经安装了ROS2,我使用的是Humble版本,但其他版本的操作也基本类似。创建两个包:mylib用于生成.so库文件,mynode用于调用这个库。
创建包的指令如下:
ros2 pkg create --build-type ament_cmake --dependencies rclcpp rclpy std_msgs --node-name mycpp mynode
ros2 pkg create --build-type ament_cmake --dependencies rclcpp rclpy std_msgs --node-name myclass mylib
这里有几个细节需要注意:--build-type指定为ament_cmake,因为我们要使用CMake来构建C++项目;--dependencies添加了rclcpp、rclpy和std_msgs,这些都是ROS2开发的基础依赖;--node-name参数指定了默认生成的节点文件名。
创建完成后,项目结构应该是这样的:
workspace/
├── mylib/
│ ├── CMakeLists.txt
│ ├── package.xml
│ ├── include/mylib/
│ └── src/
└── mynode/
├── CMakeLists.txt
├── package.xml
├── include/mynode/
└── src/
在mylib包的include目录下,我们创建myclass.hpp头文件,定义需要封装的类和函数:
#ifndef MY_CLASS_HPP
#define MY_CLASS_HPP
#include <iostream>
class MYCLASS {
public:
int a;
int b;
MYCLASS(int a, int b);
~MYCLASS();
int MYCLASS_ADD();
};
void print_myclass() {
std::cout << "我创建的.so文件输出东西了\n";
}
#endif //MY_CLASS_HPP
对应的源文件myclass.cpp实现这些功能:
#include "mylib/myclass.hpp"
int MYCLASS::MYCLASS_ADD() {
return this->a + this->b;
}
MYCLASS::MYCLASS(int a, int b) {
this->a = a;
this->b = b;
}
MYCLASS::~MYCLASS() {
std::cout << "对象销毁\n";
}
int main(int argc, char ** argv) {
(void) argc;
(void) argv;
}
注意头文件包含的路径是"mylib/myclass.hpp",这是因为在ROS2的ament编译系统中,头文件是按照包名组织的。这种组织方式虽然一开始可能觉得麻烦,但在大型项目中能有效避免命名冲突。
3. 手动复制方式集成.so库
手动复制方式是最直接的集成方法,适合刚入门的新手或者简单的项目。这种方式的核心思想就是:先编译生成.so文件,然后手动复制到需要使用它的包中。
在mylib包的CMakeLists.txt中,我们需要配置生成.so库:
add_library(myclass SHARED src/myclass.cpp)
target_include_directories(myclass PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)
ament_target_dependencies(
myclass
"rclcpp"
"rclpy"
"std_msgs"
)
install(DIRECTORY include/ DESTINATION include)
install(TARGETS myclass DESTINATION lib)
这里有几个关键点:add_library指定生成SHARED库(动态库);target_include_directories设置头文件搜索路径;install命令指定安装位置。编译成功后,在install目录下你会看到生成的libmyclass.so文件和对应的头文件。
接下来就是手动复制环节:把mylib/install/include/mylib/myclass.hpp复制到mynode/include/mynode/目录下,把mylib/install/lib/libmyclass.so复制到mynode/lib/目录下。这个过程虽然简单,但很容易出错,特别是当库文件更新时,需要重新手动复制。
在mynode包中,我们创建调用.so库的节点代码:
#include "rclcpp/rclcpp.hpp"
#include "mynode/myclass.hpp"
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node_ = std::make_shared<rclcpp::Node>("mynode");
MYCLASS* myclass = new MYCLASS(1, 1);
while (rclcpp::ok()) {
std::cout << "计算结果: " << myclass->MYCLASS_ADD() << "\n";
myclass->a++;
sleep(1);
}
delete myclass;
rclcpp::shutdown();
return 0;
}
对应的CMakeLists.txt需要配置链接.so库:
add_executable(mycpp src/mycpp.cpp)
target_include_directories(mycpp PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
ament_target_dependencies(
mycpp
rclcpp
)
target_link_libraries(mycpp
${CMAKE_CURRENT_SOURCE_DIR}/lib/libmyclass.so
)
install(DIRECTORY include/ DESTINATION include)
install(FILES lib/libmyclass.so DESTINATION lib)
install(TARGETS mycpp DESTINATION lib/${PROJECT_NAME})
手动复制方式的优点是很直观,不需要理解复杂的ament机制。但缺点也很明显:每次更新库都需要重新复制文件,多个项目使用同一个库时难以维护,容易产生版本不一致的问题。
4. ament导出方式集成.so库
ament导出方式是ROS2推荐的库集成方式,它利用了ament的包管理功能,能够自动处理依赖关系。这种方式虽然配置稍复杂,但长期来看更利于项目维护。
在mylib包的CMakeLists.txt中,我们需要增加导出配置:
add_library(mylib SHARED src/myclass.cpp)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)
ament_target_dependencies(
mylib
"rclcpp"
"rclpy"
"std_msgs"
)
install(DIRECTORY include/ DESTINATION include)
install(TARGETS mylib
EXPORT export_mylib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
ament_export_include_directories(include)
ament_export_libraries(mylib)
ament_package()
这里的关键变化是:库名称改为与项目名一致的mylib(这是ament的约定);增加了install的EXPORT选项;添加了ament_export_include_directories和ament_export_libraries调用。这些配置告诉ament系统如何导出这个库,让其他包能够找到并使用它。
在mynode包中,我们不再需要手动复制文件,而是通过find_package来自动定位依赖:
find_package(mylib REQUIRED)
add_executable(mycpp src/mycpp.cpp)
target_include_directories(mycpp PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
ament_target_dependencies(
mycpp
rclcpp
mylib
)
install(TARGETS mycpp DESTINATION lib/${PROJECT_NAME})
节点代码也需要稍作调整,包含路径改为mylib的头文件:
#include "mylib/myclass.hpp"
编译时需要特别注意顺序:先单独编译mylib包,source setup文件,再编译mynode包:
colcon build --packages-select mylib
source ./install/local_setup.bash
colcon build --packages-select mynode
这种方式的好处是自动化程度高,依赖关系清晰。当库更新时,只需要重新编译库包,其他依赖它的包会自动使用新版本。特别适合大型项目和团队协作。
5. 两种方式的对比与选择建议
经过实际项目的验证,我对这两种方式有了更深入的理解。手动复制方式就像是用螺丝刀,简单直接,适合偶尔使用;ament导出方式像是用电动螺丝刀,需要学习但效率更高。
从维护成本来看,手动复制方式在项目初期可能更快,但随着项目规模扩大,维护成本会急剧上升。我曾经在一个项目中使用手动方式管理5个.so库,每次更新都要手动复制到3个不同的包中,经常出现版本不一致的问题。后来切换到ament导出方式,这些烦恼都消失了。
从编译效率角度,ament方式支持增量编译,只编译变更的部分;而手动方式往往需要重新编译所有依赖的包。在大项目中,这种差异可能是几分钟和几小时的区别。
从代码复用性考虑,ament导出方式生成的库可以被任何其他ROS2包使用,只需要在package.xml中声明依赖即可。而手动方式需要每个使用库的包都维护一份副本。
那么如何选择呢?我的建议是:如果是学习或者快速原型开发,可以从手动方式开始,快速验证想法;如果是正式项目,特别是需要多人协作的项目,强烈建议使用ament导出方式。即使项目规模较小,使用ament方式也能培养良好的工程习惯。
在实际使用中,还有一些小技巧:给库起名时最好加上版本后缀,如mylib_v1;定期清理install目录,避免旧版本库文件造成冲突;使用colcon list命令检查包的依赖关系是否正确。
6. 常见问题排查与调试技巧
在集成.so库的过程中,难免会遇到各种问题。根据我的经验,最常见的问题是链接错误和运行时库找不到。
链接错误通常表现为"undefined reference"这类编译错误。这时候首先要检查CMakeLists.txt中的target_link_libraries配置是否正确,库路径是否准确。如果是ament方式,确认是否正确执行了source setup.bash步骤。
运行时库找不到的错误(如"error while loading shared libraries")往往是因为系统找不到.so文件的位置。在ROS2中,通常是因为没有source安装目录的setup文件。记得每次编译后都要source一下,或者将source命令添加到.bashrc中。
调试的小技巧:使用ldd命令检查可执行文件依赖的库是否都能找到:
ldd install/mynode/lib/mynode/mycpp
如果发现某个库显示"not found",就需要检查库的安装路径和LD_LIBRARY_PATH环境变量。
另一个有用的工具是nm命令,可以查看.so库中的符号表,确认需要的函数是否确实在库中:
nm -D install/mylib/lib/libmylib.so | grep MYCLASS
有时候还会遇到版本冲突问题,即多个版本的库同时存在。这时候可以使用roscd命令跳转到包目录,检查实际使用的是哪个版本的库。
对于ament导出方式,如果其他包找不到你的库,可以检查package.xml文件中是否正确定义了依赖关系。有时候需要手动添加export标签来声明导出的内容。
我在实际项目中还遇到过一种情况:库编译成功了,但使用时出现段错误。后来发现是因为库的ABI(应用程序二进制接口)不兼容。这是因为编译库和使用库的环境不同(如gcc版本不同)。解决方法是确保整个项目使用相同的编译环境和 flags。
最后建议在开发过程中保持耐心,遇到问题时从基础检查开始:文件路径是否正确、编译顺序是否对、环境变量是否设置。有时候最简单的重启终端或者重新source就能解决看似复杂的问题。
2919



被折叠的 条评论
为什么被折叠?



