4

I'm surveying c++ libraries for portable, blocking I/O access to the filesystem and network. It looks like boost::filesystem, boost::iostreams and boost::asio will, between the three of them, do the job.

To be clear, I'm not currently interested in the asynchronous aspects of boost::asio; I just want a portable, blocking interface to the network.

Digging in, I see boost::iostreams has a notion of Devices, each of which has an associated mode concept. The bidirectional mode specifically seems hand-tailored for streaming access to a full-duplex TCP connection. Awesome.

boost::iostreams does not seem to offer support for actually opening TCP connections (unlike the local filesystem.) That's fine, surely boost::asio will let me open the connection, appropriately model it as a bidirectional Device, and wrap it in a boost::iostreams::stream.

..except it won't? I see boost::asio::ip::tcp::iostream, which would replace the boost::iostreams::stream, but presumably not act as a Device.

I understand the tcp::iostream would act similarly, but I would still prefer to learn and code against just one interface, not two. Specifically, dealing with two error handling regimes & exception hierarchies is not very palatable.

So, the question: am I blind? Maybe an adapter between the two libraries exists, that I missed googling around. Or perhaps someone has already released such an adapter as a 3rd-party component I could drop in?

phs
  • 10,687
  • 4
  • 58
  • 84

1 Answers1

7

I'm not aware of a direct mapping. However, if you were interested, writing such a device is fairly straightforward. This version throws boost::system::system_error for non-EOF errors, but you could choose to do something else.

#include <iosfwd>

#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/iostreams/categories.hpp>
#include <boost/system/system_error.hpp>


class asio_stream_device
{
public:
    typedef char char_type;
    typedef boost::iostreams::bidirectional_device_tag category;

    explicit asio_stream_device(boost::asio::ip::tcp::socket& sock) : socket_(sock)
    {

    }

    std::streamsize read(char* s, std::streamsize n)
    {
        // Read up to n characters from the underlying data source
        // into the buffer s, returning the number of characters
        // read; return -1 to indicate EOF
        boost::system::error_code ec;

        std::size_t rval = socket_.read_some(boost::asio::buffer(s, n), ec);
        if (!ec)
        {
            return rval;
        }
        else if (ec == boost::asio::error::eof)
        {
            return -1;
        }
        else
        {
            throw boost::system::system_error(ec,"read_some");
        }

    }


    std::streamsize write(const char* s, std::streamsize n)
    {
        // Write up to n characters to the underlying
        // data sink into the buffer s, returning the
        // number of characters written

        boost::system::error_code ec;
        std::size_t rval = socket_.write_some(boost::asio::buffer(s, n), ec);
        if (!ec)
        {
            return rval;
        }
        else if (ec == boost::asio::error::eof)
        {
            return -1;
        }
        else
        {
            throw boost::system::system_error(ec,"write_some");
        }

    }



private:

    boost::asio::ip::tcp::socket& socket_;

};

Basically, open/connect the socket as normal, then pass it to the constructor. The example simply reads and outputs to the screen.

void test
{
   namespace asio = boost::asio;
   namespace io = boost::iostreams;

   asio::io_service service;
   asio::ip::tcp::socket socket(service);


   asio::ip::tcp::endpoint remote -  ...; ////

   socket.connect(remote);

   io::stream<asio_stream_device> str(socket);

   std::string line;

   while (std::getline(str, line)) {
    std::cout << line << std::endl;
   }
}
Dave S
  • 20,507
  • 3
  • 48
  • 68
  • I agree this looks simple on the surface, but I was hoping to avoid it. Particularly, I fear mapping asio errors onto iostream ones "correctly" (i.e. minimizing surprises across platforms) may be quite hard. Maybe I'm over-thinking it. – phs Aug 19 '12 at 02:07
  • @phs: Well, as far as I can tell, the `boost::iostreams` device really only has one interface to report a problem, and that's by returning '-1' from it's read/write, which sets `eofbit` for read failures, and `badbit | failbit` for write failures. So if you wanted, you could simply map ALL asio errors to the '-1' code, and use that instead. – Dave S Aug 19 '12 at 02:23
  • Actually, they mention preferring exceptions, and list out some specific semantics they try to respect: http://www.boost.org/doc/libs/1_50_0/libs/iostreams/doc/guide/exceptions.html . – phs Aug 19 '12 at 02:25
  • @phs. Interesting, I hadn't read that. In C++11, `std::ios_base::failure` now derives from `std::system_error`, so you would simply use the ASIO error code with the `ios_base::failure` (once ASIO use C++11 error code, not boost's). Otherwise, I would probably recommend creating a new exception that is basically the same as `boost::system::system_error` except deriving from `ios_base::failure` – Dave S Aug 19 '12 at 02:35
  • Yea, the new exception route is what I was looking at just now. Thankfully asio does list out all its error codes, so I could at least attempt the mapping. If iostreams doesn't actually react to its own exceptions in any particular way, then the mapping could be simple (i.e. the one wrapping exception.) Still, pity this isn't just already done somewhere. – phs Aug 19 '12 at 02:41