第二章:Python与C++开发基础

2.1 为什么ROS2要支持两种语言?

很多刚接触ROS2的朋友会问我:为什么非要搞两套语言?用Python不香吗?或者全上C++不更统一?

说实话,这个问题我当年也纠结过。后来在项目里摔了几次跟头,才真正理解了这个设计哲学。

Python和C++在ROS2里不是竞争关系,而是互补关系。Python开发快、调试方便,适合做原型验证和工具脚本。C++性能强、实时性好,适合做核心算法和底层驱动。

我个人的习惯是:算法验证用Python,产品落地用C++。你想想看,一个激光雷达的驱动节点,每秒要处理几十万点云数据,用Python写?那CPU直接冒烟了。但反过来,一个调试用的可视化工具,用C++写?那开发周期够你喝一壶的。

核心结论:ROS2的rclcpp和rclpy共享同一个底层通信库(rcl),所以两种语言写的节点可以无缝通信。你完全可以在一个系统里混用Python和C++节点。

2.2 rclcpp与rclpy的底层设计

先看一张图,这是ROS2的软件栈分层结构:

DDS/RTPS 通信层 rcl (ROS Client Library) 统一C接口 rclcpp (C++客户端库) 高性能、零拷贝、模板元编程 rclpy (Python客户端库) 快速开发、动态类型、C扩展绑定 应用层(你的节点代码) Python节点 ↔ C++节点 完全互通 图:ROS2软件栈分层架构

看到这个分层结构了吗?rclcpp和rclpy都建立在同一个rcl层之上。rcl层封装了DDS的复杂性,提供了统一的C语言接口。所以不管上层用哪种语言,底层通信机制是完全一致的。

我记得刚开始接触ROS2时,以为rclpy就是rclcpp的简单Python包装。后来看了源码才发现,rclpy是用C扩展实现的,性能比纯Python包装好得多。但即便如此,Python的GIL(全局解释器锁)还是限制了它的并发能力。

避坑指南:我曾经在一个项目中,用Python写了图像处理节点,结果发现CPU占用率居高不下。后来换成C++实现,同样的算法,CPU占用率从85%降到了15%。所以,计算密集型的节点,请优先考虑C++。

2.3 编写第一个发布-订阅节点

好,理论说完了,咱们直接上手写代码。我会分别用Python和C++实现一个简单的发布-订阅节点对。

2.3.1 Python版本:发布者节点

# talker_py.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class TalkerNode(Node):
    def __init__(self):
        super().__init__('talker_py')
        self.publisher_ = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.count = 0
        
    def timer_callback(self):
        msg = String()
        msg.data = f'Hello ROS2 from Python: {self.count}'
        self.publisher_.publish(msg)
        self.get_logger().info(f'Publishing: "{msg.data}"')
        self.count += 1

def main(args=None):
    rclpy.init(args=args)
    node = TalkerNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

2.3.2 Python版本:订阅者节点

# listener_py.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class ListenerNode(Node):
    def __init__(self):
        super().__init__('listener_py')
        self.subscription = self.create_subscription(
            String, 'chatter', self.listener_callback, 10)
        
    def listener_callback(self, msg):
        self.get_logger().info(f'I heard: "{msg.data}"')

def main(args=None):
    rclpy.init(args=args)
    node = ListenerNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

2.3.3 C++版本:发布者节点

// talker_cpp.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TalkerNode : public rclcpp::Node
{
public:
    TalkerNode() : Node("talker_cpp"), count_(0)
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("chatter", 10);
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&TalkerNode::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto msg = std_msgs::msg::String();
        msg.data = "Hello ROS2 from C++: " + std::to_string(count_++);
        publisher_->publish(msg);
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", msg.data.c_str());
    }
    
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
    size_t count_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<TalkerNode>());
    rclcpp::shutdown();
    return 0;
}

2.3.4 C++版本:订阅者节点

// listener_cpp.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class ListenerNode : public rclcpp::Node
{
public:
    ListenerNode() : Node("listener_cpp")
    {
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "chatter", 10,
            std::bind(&ListenerNode::listener_callback, this, std::placeholders::_1));
    }

private:
    void listener_callback(const std_msgs::msg::String & msg)
    {
        RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }
    
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<ListenerNode>());
    rclcpp::shutdown();
    return 0;
}

2.4 两种语言的对比分析

写完了代码,我们来做个对比。我整理了一个表格,方便你直观地看到差异:

对比维度 Python (rclpy) C++ (rclcpp)
代码行数 约30行 约50行
开发速度 快(无需编译) 慢(需要CMake编译)
运行性能 中等(受GIL限制) 高(零拷贝、内联优化)
内存管理 自动垃圾回收 智能指针(RAII)
调试难度 低(动态类型、交互式) 高(编译错误、段错误)
适用场景 原型验证、工具脚本、配置管理 核心算法、实时控制、驱动层

注意:C++版本中我用了std::bind来绑定回调函数。在C++17及以上版本,你可以用lambda表达式替代,代码会更简洁。但为了兼容性,我建议在ROS2项目中统一使用std::bind。

2.5 编译与运行

Python节点不需要编译,直接运行即可:

# 终端1:运行Python发布者
python3 talker_py.py

# 终端2:运行Python订阅者
python3 listener_py.py

C++节点需要先编译:

# 在CMakeLists.txt中添加:
add_executable(talker_cpp talker_cpp.cpp)
target_link_libraries(talker_cpp rclcpp::rclcpp std_msgs::std_msgs)

add_executable(listener_cpp listener_cpp.cpp)
target_link_libraries(listener_cpp rclcpp::rclcpp std_msgs::std_msgs)

# 编译
colcon build --packages-select your_package

# 运行
ros2 run your_package talker_cpp
ros2 run your_package listener_cpp

你还可以混搭运行:比如用Python发布者 + C++订阅者,或者反过来。这就是ROS2的魅力所在——语言不再是障碍。

我的经验:刚开始学ROS2时,我建议你先用Python把逻辑跑通,然后再用C++重写性能瓶颈部分。这样既保证了开发效率,又保证了最终产品的性能。我在做自动驾驶感知模块时,就是先用Python验证算法,再用C++做工程化落地。

2.6 常见问题与避坑

  • QoS设置不一致:发布者和订阅者的QoS配置必须匹配,否则收不到消息。我遇到过最坑的一次,就是忘了设置durability策略,导致后启动的订阅者永远收不到之前发布的消息。
  • 节点名称冲突:同一个ROS2网络中,节点名称必须唯一。我曾经在调试时开了两个同名节点,结果消息乱飞,排查了半天才发现是命名问题。
  • Python的GIL问题:如果你的Python节点中有耗时操作,记得用asyncio或multiprocessing来避免阻塞主线程。否则,整个节点的回调都会被卡住。

嗯,到这里,你应该已经掌握了ROS2中Python和C++的基本开发范式。两种语言各有千秋,关键是根据场景选对工具。下一节我们会深入节点的生命周期管理,到时候你会看到更多有意思的设计模式。

可可精品资料库