Skip to main content

Callback Function In ROS2 Pub and Sub

Intro

In ROS2 humble tutorial for publisher and subscriber, function pointer plays an important part in the callback function:

publisher:

class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}

private:
//...
};

subscriber:

class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}

private:
//...
};

This blog aims to get the readers familiar with the usage like:

std::bind(&MinimalSubscriber::topic_callback, this, _1))

and master the most basic part in ROS2: publisher and subscriber.

Basics for C++11 Function Pointer and std::bind

Funtion Pointer

In C language and ancient C++, function pointer is not a specific type. After C++11, function pointer is wrapped in a specific type std::function(See cpp reference)

A simple example(Relevant headers are omitted):

std::function<double(const std::vector<double>&)> fn;
double sum(const std::vector<double>& data) {
return std::accumulate(data.begin(), data.end(), 0.);
}
fn = sum;
std::vector<double> data = {0.5, 1.0, 2.3};
auto res1 = fn(data);

std::bind

std::bind is a method to "bind" a function to an object. This is hard to understand. However, the easiest usage of std::bind is to create a "partial function". We begin here:

Suppose we have a function:

f(a,b,c);

We want a function:

g := f(a, 4, b)

std::bind can be used here:

auto g = bind(f, _1, 4, _2);

In this usage, there is no "object" to bind. The only purpose of the std::bind is to pass an argument.

std::bind has three arguments(See [reference](std::bind - cppreference.com)): the function that is bound, the object that is bound to, and the arguments which will be passed to the bound function. Generally, std::bind bind a member function to the class object to implement callback. Actually it is a function pointer itself.

Callback Implementation in ROS2

In subscription creation:

subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));

Here _1 is std::placeholders::_1, which is used to pass the argument of MinimalSubscriber::topic_callback.

If you understand the usage of std::bind now, you may well ask: why don't we use std::function directly as the last argument of this->create_subscription? In this situation, if you write:

subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, &MinimalSubscriber::topic_callback)

You will encounter a compilation error, since the member function requires an implicit this pointer to the object they are invoked on. This is a good example for the flexibility of std::bind.

However, nobody says the callback function must be a member function, if you use a free function as callback function, a direct use of function pointer is possible.

Besides, the number of arguments is 1, which is the message itself. However, you may encounter the situation where you want more arguments passed.   std::bind can implement this easily:

auto callback = std::bind(callback_func, std::placeholders::_1, topic_name);

The above is an example to pass topic name as an argument.

An Application Example

Tinker chassis hardware uses CAN Bus as communication media, and a can adpater interface is needed. In CAN adapter implementation, a receive callback function is always needed. However, the callback relies on the specifc motor type. So it's not appropriate for us to implement the callback in CanAdapter class. Here is a trick to decouple our code using function pointer:

In class declaration, we leave a function pointer argument for construction function.

class CanBusNode : public rclcpp::Node
{
public:
using FrameCallback = std::function<void(const can_msgs::msg::Frame::SharedPtr)>;

CanBusNode(const std::string & node_name, FrameCallback callback);

private:
rclcpp::Publisher<can_msgs::msg::Frame>::SharedPtr to_can_bus_publisher_;
rclcpp::Subscription<can_msgs::msg::Frame>::SharedPtr from_can_bus_subscriber_;
FrameCallback frame_callback_;
};

In construction, we use this function pointer as callback function of CAN subscriber. Here the callback function is not a member function of it, though std::bind is still needed.

CanBusNode::CanBusNode(const std::string & node_name, FrameCallback callback)
: Node(node_name), frame_callback_(callback)
{
// Initialize publisher
to_can_bus_publisher_ = this->create_publisher<can_msgs::msg::Frame>("to_can_bus", 10);

// Initialize subscriber with the provided callback function
from_can_bus_subscriber_ = this->create_subscription<can_msgs::msg::Frame>(
"from_can_bus", 500, std::bind(callback, std::placeholders::_1));
}

More advanced way to implement callback function

Though ROS2 document uses std::bind to create callback, it is somewhat outdated now. The fashion is to use lambda expression:

subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, [this] (const std_msgs::msg::String msg){this->callback(msg)}););

This is more elegant way.

For more about lambda expression, see [cpp reference](Lambda expressions (since C++11) - cppreference.com).

Use lambda expression in ROS2, see this post.

Implement callback function in Python

ROS2 document gives the example to implement callback function: document. To pass multiple arguments, we can use lambda function in python:

node.create_subscription(std_msgs.msg.String, "my_topic", lambda msg: common_callback(msg, other_args), 10)

For the basics of python lambda, see ref.