Zero-Copy Communication

Safe DDS provides zero-copy communication between matched DataWriters and DataReaders when certain conditions are met.

When zero-copy communication is possible, the DataWriter can directly share the memory of the data instance with the DataReader, avoiding unnecessary copying of data and improving performance.

The conditions for zero-copy communication to be possible are:

  • The DataWriter and DataReader must be matched, meaning they are associated with the same Topic and have compatible QoS settings. This is a fundamental requirement for any communication to occur between a DataWriter and a DataReader.

  • The DataWriter and DataReader must be configured to allow zero-copy communication through their QoS settings. This typically involves setting the appropriate QoS policies that enable zero-copy communication, such as the data_sharing QoS policy. In particular, both should have enabled data sharing by setting the max_remote_entities to a value greater than zero.

  • The data type being communicated must be compatible with zero-copy communication. This usually means that the data type must be a Plain Old Data (POD), final type, without strings or sequences, and that it must be in-memory compatible with its serialized representation.

  • Specific APIs must be used to write and read data that is intended for zero-copy communication. See below for more details on how to use the APIs for zero-copy communication.

APIs for zero-copy communication

A typical zero-copy workflow starts by defining a loanable type. The example below uses a final type with a fixed-size array, which allows the generated type support to treat the serialized buffer as an in-memory instance when the selected representation is compatible:

@final
struct LoanableHelloWorld
{
    unsigned long index;
    octet payload[32];
};

After generating the type support for that IDL, the application can validate at compile time that the type is loanable for the data representation it plans to use:

    constexpr serialization::cdr::XCDRVersion xcdr_version = serialization::cdr::XCDRVersion::XCDRV1;
    static_assert(
        LoanableHelloWorldTypeSupport::is_loanable(xcdr_version),
        "LoanableHelloWorld must be loanable");

Both endpoints must also be configured to allow the zero-copy path. In practice, this means enabling data sharing on both sides and using a compatible data representation:

    DataWriterQos datawriter_qos{};
    datawriter_qos.reliability().kind = ReliabilityQosPolicyKind::BEST_EFFORT_RELIABILITY_QOS;
    datawriter_qos.data_sharing().max_remote_entities = 1;

    DataReaderQos datareader_qos{};
    datareader_qos.reliability().kind = ReliabilityQosPolicyKind::BEST_EFFORT_RELIABILITY_QOS;
    datareader_qos.data_sharing().max_remote_entities = 1;

Writer-side flow

On the DataWriter side, instead of calling the regular TypedDataWriter::write method, the application requests a loaned buffer with DataWriter::get_loaned_buffer. The generated TypeSupport provides loanable_serialized_size to request a buffer of the right size, and cast_from_buffer to interpret that buffer as the generated data type. Once the sample has been filled, it is published with DataWriter::write_loaned_buffer. After that call, the application must no longer access the loaned buffer:

    auto publish = [&]()
            {
                static uint32_t sample_index = 0;

                memory::byte_array::ByteArrayView loaned_buffer{};
                datacentric::SampleKey sample_key{};

                const uint32_t buffer_size = LoanableHelloWorldTypeSupport::loanable_serialized_size(xcdr_version);

                if (dds::ReturnCode::OK != datawriter->get_loaned_buffer(buffer_size, loaned_buffer, sample_key))
                {
                    std::cerr << "Error getting loaned buffer" << std::endl;

                    return;
                }

                auto* data = LoanableHelloWorldTypeSupport::cast_from_buffer(loaned_buffer, xcdr_version);

                if (nullptr == data)
                {
                    std::cerr << "Error casting loaned buffer to LoanableHelloWorld" << std::endl;

                    return;
                }

                data->index = sample_index++;

                for (size_t i = 0; i < (sizeof(data->payload) / sizeof(data->payload[0])); ++i)
                {
                    data->payload[i] = static_cast<uint8_t>(data->index + i);
                }

                if (dds::ReturnCode::OK != datawriter->write_loaned_buffer(loaned_buffer, sample_key, HANDLE_NIL))
                {
                    std::cerr << "Error writing loaned buffer" << std::endl;
                }
            };

Reader-side flow

On the DataReader side, instead of using TypedDataReader::take_next_sample, the application can use DataReader::take_next_sample_buffer to access the internal serialized buffer directly. The generated TypeSupport::cast_from_buffer helper can then be used to obtain a typed view of that sample without deserializing it. Once the application is done with the data, it must return the loan with DataReader::release_sample_buffer:

struct LoanableHelloWorldDataReaderListener :
    public DataReaderListener
{
    void on_data_available(
            DataReader& reader) noexcept override
    {
        memory::byte_array::ByteArrayView data_view{};
        protocol::PayloadKind payload_kind = protocol::PayloadKind::EMPTY;
        InlineQoS inline_qos{};
        SampleInfo info{};

        while (reader.take_next_sample_buffer(data_view, payload_kind, inline_qos, info) == dds::ReturnCode::OK)
        {
            if (info.valid_data)
            {
                const auto* data = LoanableHelloWorldTypeSupport::cast_from_buffer(data_view);

                if (nullptr != data)
                {
                    std::cout << "Received sample " << data->index
                              << " with first payload byte " << static_cast<uint32_t>(data->payload[0])
                              << std::endl;
                }
            }

            reader.release_sample_buffer(data_view, info);
        }
    }

};

Resource cleanup

Zero-copy communication based on data sharing may leave shared-memory related resources behind after the participants terminate. If they are not cleaned up, relaunching the applications may fail because those resources already exist.

This cleanup can be performed with the safedds_shm_cleanup.sh script provided in the posix_shm_transport extra support package. It can also be done manually by removing the corresponding resources from /dev/shm and /tmp (or from /dev/shmem and /var/run on QNX).