2

Background: I am currently trying to improve a project of mine: I want to make a specific camera usable with a framework. The camera is accessible via a proprietary SDK (Pylon). The framework is ROS2.

Now for the challenge: As memory bandwidth is a bottle-neck in my use case, I want to avoid copying image data at all costs. The camera SDK provides a mechanism where the user-defined buffer is allocated only once. Buffer is a native ROS2 type called sensor_msgs::msg::Image (please note: There is no std::unique_ptr involved here). The camera is writing into it asynchronously. This is working fine.

ROS2 provides a zero-copy mechanism to implicitly share memory between components using a std::unique_ptr<sensor_msgs::msg::Image> in publish. This is also working fine.

Problem: The mechanism assumes you std::move the std::unique_ptr away from the sender. This is totally reasonable for the default use-case. After the data has been transferred, std::unique_ptr calls the deleter which then de-allocates the transferred object. Unfortunately, that still is the buffer the camera driver wants to write to.

The first thing I tried was wrapping the "static" object into a std::unique_ptr with a no-op deleter using a lambda:

auto deleter = [](sensor_msgs::msg::Image*){ std::cerr << "NoDeleter is not deleting an Image." << std::endl; };
std::unique_ptr<sensor_msgs::msg::Image, decltype(deleter)> img_msg_ptr(img_msg, deleter);

But the types are incompatible:

no known conversion for argument 1 from ‘unique_ptr<[...],…::<lambda(sensor_msgs::msg::Image_<std::allocator<void> >*)>>’ to ‘unique_ptr<[...],std::default_delete<sensor_msgs::msg::Image_<std::allocator<void> > >

It looks like the deleter must inherit from std::default_delete. sensor_msgs::msg::Image is a shorthand for sensor_msgs::msg::Image_<std::allocator<void> >.

Next I tried inheriting from std::default_delete:

class NoDeleter : public std::default_delete<sensor_msgs::msg::Image> {
    public:
    void operator()(sensor_msgs::msg::Image *) const override {
        std::cerr << "NoDeleter is not deleting an Image." << std::endl;
    }
};

But gcc tells me I am doing it wrong:

error: ‘void NoDeleter::operator()(sensor_msgs::msg::Image*) const’ marked ‘override’, but does not override

I tried specializing std::default_delete, but it had no effect. I assume, I am using the wrong type.

How can I (manually) determine the type I need to use?

Update: As requested, here is a complete example (it depends on ROS2 "foxy"):

staticbufferpublisher.cpp:

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_components/register_node_macro.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <chrono> // for 1s literal
using namespace std::chrono_literals;
class StaticBufferPublisher : public rclcpp::Node {
private:
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr imagePublisher;
    rclcpp::TimerBase::SharedPtr timer;
    sensor_msgs::msg::Image buffer;
public:
    StaticBufferPublisher(const rclcpp::NodeOptions & options) : rclcpp::Node("StaticBufferPublisher", options) {
        if (!options.use_intra_process_comms()) {
            throw std::runtime_error("Must use use_intra_process_comms!");
        }
        imagePublisher = this->create_publisher<sensor_msgs::msg::Image>("image", 1);
        buffer.data.resize(1024); // prepare buffer once
        // camera.set_buffer(buffer.data.data()) // tell camera where to put the data
        // camera.start_grabbing() // start camera
        // this timer simulates a while(camera.block_until_image_ready())
        timer = this->create_wall_timer(1s, [this]() -> void {
            std::cerr << "Sending  data starting at " << static_cast<void*>(buffer.data.data()) << "." << std::endl;
            /*
             * THE INTERESTING PART IS HERE
             */
            imagePublisher->publish(buffer); // this works, but creates a copy
            std::unique_ptr<sensor_msgs::msg::Image> img_msg_ptr(&buffer);
            imagePublisher->publish(std::move(img_msg_ptr)); // this does not create a copy, but releases the buffer – so it "works", but only once
            //auto deleter = [](sensor_msgs::msg::Image*){ std::cerr << "NoDeleter is not deleting an Image." << std::endl; };
            //std::unique_ptr<sensor_msgs::msg::Image, decltype(deleter)> img_msg_ptr(img_msg, deleter); // publish() will not accept this unique_ptr
        });
    }
};
RCLCPP_COMPONENTS_REGISTER_NODE(StaticBufferPublisher)

addressprinter.cpp:

#include <rclcpp/rclcpp.hpp>
#include <rclcpp_components/register_node_macro.hpp>
#include <sensor_msgs/msg/image.hpp>
class AddressPrinter : public rclcpp::Node {
private:
    rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr imageSubscription;
public:
    AddressPrinter(const rclcpp::NodeOptions & options) : rclcpp::Node("AddressPrinter", options) {
        if (!options.use_intra_process_comms()) {
            throw std::runtime_error("Must use use_intra_process_comms!");
        }
        imageSubscription = this->create_subscription<sensor_msgs::msg::Image>("image", 1,
            [this](sensor_msgs::msg::Image::UniquePtr msg) {
                std::cerr << "Received data starting at " << static_cast<void*>(msg->data.data()) << "." << std::endl;
            }
        );
    }
};
RCLCPP_COMPONENTS_REGISTER_NODE(AddressPrinter)

Launchfile:

import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
    image_processing = ComposableNodeContainer(
            name = 'container',
            namespace = '',
            package = 'rclcpp_components',
            executable = 'component_container',
            composable_node_descriptions = [
                ComposableNode(
                    name = 'address_printer',
                    namespace = '',
                    package = 'pylon_instant_camera',
                    plugin = 'AddressPrinter',
                    extra_arguments=[{'use_intra_process_comms': True}]
                ),
                ComposableNode(
                    name = 'publisher',
                    namespace = '',
                    package = 'pylon_instant_camera',
                    plugin = 'StaticBufferPublisher',
                    extra_arguments=[{'use_intra_process_comms': True}]
                )
            ],
            output = 'screen'
    )
    return launch.LaunchDescription([image_processing])

CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)
project(pylon_instant_camera)
set (CMAKE_CXX_STANDARD 17)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
add_library(AddressPrinter SHARED src/addressprinter.cpp)
ament_target_dependencies(AddressPrinter 
    rclcpp
    rclcpp_components
    sensor_msgs
)
rclcpp_components_register_node(AddressPrinter PLUGIN "AddressPrinter" EXECUTABLE "addressprinter")
add_library(StaticBufferPublisher SHARED src/staticbufferpublisher.cpp)
ament_target_dependencies(StaticBufferPublisher 
    rclcpp
    rclcpp_components
    sensor_msgs
)
rclcpp_components_register_node(StaticBufferPublisher PLUGIN "StaticBufferPublisher" EXECUTABLE "staticbufferpublisher")
install(TARGETS 
  StaticBufferPublisher
  AddressPrinter
  DESTINATION lib
)
install(DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)
ament_package()
Hermann
  • 604
  • 7
  • 23
  • 2
    If you try to assign your deleter as an instance of `std::default_delete` then it will experience object slicing. You'll end up with a normal `std::default_delete`. `std::default_delete::operator()` is not `virtual` so you can't achieve what you want this way. The reason you get an error with `override` is because the base function is not `virtual`. – François Andrieux Jan 27 '22 at 20:47
  • 2
    This sounds like an [XY Problem](https://meta.stackexchange.com/questions/66377/). If a `unique_ptr` is transferred, there is nothing for the original `unique_ptr` to delete, so its `deleter` doesn't matter. If you don't want the original `unique_ptr` to delete the data, and you are not going to delete the data either, then you shouldn't be transferring the data to another `unique_ptr` to begin with, simply make the original `unique_ptr` `release()` ownership of the data instead, and then you can use the raw pointer however you want. You don't need a custom do-nothing `deleter`. – Remy Lebeau Jan 27 '22 at 21:09
  • @RemyLebeau I am very open to alternative solutions. Unfortunately, I do not understand your suggestion. I am actually using raw pointers up to the call to `publish`. The method expects a `unique_ptr`. That is why I try to "wrap" a raw pointer into a `unique_ptr`. There is no "original" `unique_ptr`. I can neither change the interface nor the implementation of `publish`. So I cannot use a raw pointer. Or am I mistaken? – Hermann Jan 27 '22 at 21:47
  • @FrançoisAndrieux Oh. Right. I forgot C++ is not Java. Feel free to post it as an answer so I can accept it. – Hermann Jan 27 '22 at 21:51
  • 1
    @Hermann: "*But the types are incompatible:*" Provide the code where you're seeing this error. Even if it's inside of `publish` somewhere. – Nicol Bolas Jan 27 '22 at 21:59
  • 1
    @Hermann I'm not familiar with ROS2, but the `publish()` you linked to takes in a `unique_ptr` that uses a custom `MessageDeleter` based on the `Publisher's` allocator, so can't you provide a compatible allocator so you can customize the `deleter`? Otherwise, why not just use `shared_ptr` instead? You can `publish()` a `shared_ptr`, and that will allow you to share ownership of the buffer with ROS2 so it doesn't try to free the buffer while you are still using it. – Remy Lebeau Jan 27 '22 at 22:00
  • @NicolBolas I added a complete example to the question. – Hermann Jan 28 '22 at 13:47
  • @Hermann: You didn't post the line where you get the error. And I mean the line from the *error message* itself, not the line in *your* code in the call stack that caused it. – Nicol Bolas Jan 28 '22 at 14:24
  • It is the line `imagePublisher->publish(std::move(img_msg_ptr));` when `img_msg_ptr` is declared as `std::unique_ptr`. This is a compile-time error. There is no call stack. I doubt I can find the appropriate lines in the gcc implementation. – Hermann Jan 28 '22 at 14:29
  • @RemyLebeau I have succeeded in declaring a deleter which works with the const reference variant, but failed to declare a deleter for the `unique_ptr` variant of the publish method. I want to try for myself first and then report back later. – Hermann Jan 28 '22 at 15:28

0 Answers0