Search code examples
c++boostboost-asioasio

Iterating over buffers in an asio::streambuf


While transferring data over named pipes I need to serialize some data structure and then transmit a sequence consisting of the size in bytes of the serialized structure as a uint32_t followed by the bytes of the serialized structure.

I only know the serialized size after serialization is complete. Serialization outputs to a stream (using asio::streambuf for this purpose). I would like prepend this size to the buffer without unnecessary copies or writing twice to the pipe.

I have found the concept of gather/scatter in boost.asio using buffer sequences that seems to cover my exact use case. However, when iterating over the asio::const_buffers in the streambuf, I find there are more elements than expected (1 expected, 2 found when doing pointer distance calculation). Indeed, when iterating over the buffers to concatenate them in my sequence, dereferencing on the 2nd pass causes an access violation exception.

Is there something I'm missing about iterating over the buffers in an asio::streambuf?

class PacketBufferProxy
{
    uint32_t size_;
    std::vector<as::const_buffer> seq_;
public:
    PacketBufferProxy(as::streambuf& payload) :
        size_{ (uint32_t)payload.size() }
    {
        auto i = as::buffer_sequence_begin(payload.data());
        const auto e = as::buffer_sequence_end(payload.data());
        auto nStreambufBuffers = std::distance(i, e);
        seq_.reserve(nStreambufBuffers + 1);
        seq_.emplace_back(as::buffer(&size_, sizeof(size_)));
        for (; i != e; ++i) {
            seq_.emplace_back(*i);
        }
    }
    operator const std::vector<as::const_buffer>& () const
    {
        return seq_;
    }
};

Solution

  • streambuf::data returns an object, so

    auto i = as::buffer_sequence_begin(payload.data());
    const auto e = as::buffer_sequence_end(payload.data());
    

    Both iterators point to different - destructed - temporaries. Using them invokes UB.

    It's easier to write against buffer sequence concepts:

    template <typename ConstBufferSequence>
    PacketBufferProxy(ConstBufferSequence bufs) : size_{(uint32_t)as::buffer_size(bufs)} {
        auto i = as::buffer_sequence_begin(bufs), e = as::buffer_sequence_end(bufs);
        seq_.reserve(std::distance(i, e) + 1);
        seq_.emplace_back(as::buffer(&size_, sizeof(size_)));
        for (; i != e; ++i)
            seq_.emplace_back(*i);
    }
    

    So you could unify as follows, replacing uint32_t size_ with a big-endian type for good measure:

    #include <boost/asio.hpp>
    #include <boost/endian/arithmetic.hpp>
    #include <iostream>
    
    namespace as = boost::asio;
    
    class PacketBufferProxy {
        boost::endian::big_uint32_t   size_;
        std::vector<as::const_buffer> seq_;
    
      public:
        template <typename ConstBufferSequence>
        PacketBufferProxy(ConstBufferSequence bufs) : size_{(uint32_t)as::buffer_size(bufs)} {
            auto i = as::buffer_sequence_begin(bufs), e = as::buffer_sequence_end(bufs);
            seq_.reserve(std::distance(i, e) + 1);
            seq_.emplace_back(as::buffer(&size_, sizeof(size_)));
            for (; i != e; ++i)
                seq_.emplace_back(*i);
        }
        PacketBufferProxy(as::streambuf& payload) : PacketBufferProxy(payload.data()) {}
        operator std::vector<as::const_buffer> const&() const { return seq_; }
    };