Conor Taylor
Conor Taylor

Reputation: 3108

Transfer ownership of boost::asio::socket stack variable

I'm writing a simple tcp socket server capable of handling multiple concurrent connections. The idea is that the main listening thread will do a blocking accept and offload socket handles to a worker thread (in a thread pool) to handle the communication asynchronously from there.

void server::run() {
  {
    io_service::work work(io_service);

    for (std::size_t i = 0; i < pool_size; i++)
      thread_pool.push_back(std::thread([&] { io_service.run(); }));

    boost::asio::io_service listener;
    boost::asio::ip::tcp::acceptor acceptor(listener, ip::tcp::endpoint(ip::tcp::v4(), port));

    while (listening) {
      boost::asio::ip::tcp::socket socket(listener);
      acceptor.accept(socket);
      io_service.post([&] {callback(std::move(socket));});
    }
  }

  for (ThreadPool::iterator it = thread_pool.begin(); it != thread_pool.end(); it++)
    it->join();
}

I'm creating socket on the stack because I don't want to have to repeatedly allocate memory inside the while(listening) loop.

The callback function callback has the following prototype:

void callback(boost::asio::socket socket);

It is my understanding that calling callback(std::move(socket)) will transfer ownership of socket to callback. However when I attempt to call socket.receive() from inside callback, I get a Bad file descriptor error, so I assume something is wrong here.

How can I transfer ownership of socket to the callback function, ideally without having to create sockets on the heap?

Upvotes: 2

Views: 1021

Answers (1)

Tanner Sansbury
Tanner Sansbury

Reputation: 51891

Undefined behavior is potentially being invoked, as the lambda may be invoking std::move() on a previously destroyed socket via a dangling reference. For example, consider the case where the loop containing the socket ends its current iteration, causing socket to be destroyed, before the lambda is invoked:

 Main Thread                       | Thread Pool
-----------------------------------+----------------------------------
tcp::socket socket(...);           |
acceptor.accept(socket);           |
io_service.post([&socket] {...});  |
~socket(); // end iteration        |
... // next iteration              | callback(std::move(socket));

To resolve this, one needs to transfer socket ownership to the handler rather than transfer ownership within the handler. Per documentation, Handlers must be CopyConstructible, and hence their arguments, including the non-copyable socket, must be as well. Yet, this requirement can be relaxed if Asio can eliminate all calls to the handler's copy constructor and one has defined BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS.

#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>

void callback(boost::asio::ip::tcp::socket socket);

...

// Transfer ownership of socket to the handler.
io_service.post(
  [socket=std::move(socket)]() mutable
  {
    // Transfer ownership of socket to `callback`.
    callback(std::move(socket));
  });

For more details on Asio's type checking, see this answer.


Here is a complete example demonstrating a socket's ownership being transferred to a handler:

#include <functional> // std::bind
#include <utility>    // std::move
#include <vector>     // std::vector
#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>

const auto noop = std::bind([]{});

void callback(boost::asio::ip::tcp::socket socket)
{
  const std::string actual_message = "hello";
  boost::asio::write(socket, boost::asio::buffer(actual_message));
}

int main()
{
  using boost::asio::ip::tcp;

  // Create all I/O objects.
  boost::asio::io_service io_service;
  tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
  tcp::socket client_socket(io_service);

  // Connect the sockets.
  client_socket.async_connect(acceptor.local_endpoint(), noop);
  {
    tcp::socket socket(io_service);
    acceptor.accept(socket);
    // Transfer ownership of socket to the handler.
    assert(socket.is_open()); 
    io_service.post(
      [socket=std::move(socket)]() mutable
      {
        // Transfer ownership of socket to `callback`.
        callback(std::move(socket));
      });
    assert(!socket.is_open()); 
  } // ~socket

  io_service.run();

  // At this point, sockets have been conencted, and `callback`
  // should have written data to `client_socket`.
  std::vector<char> buffer(client_socket.available());
  boost::asio::read(client_socket, boost::asio::buffer(buffer));

  // Verify the correct message was read.
  const std::string expected_message = "hello";
  assert(std::equal(
    begin(buffer), end(buffer),
    begin(expected_message), end(expected_message)));
}

Upvotes: 2

Related Questions