Search

ReCode /

Socket programming with shared_ptr

As far as I can tell, many of the readers of my blog find it through my posts on a couple of mailing lists: the sweng-gamedev@midnightryder.com game development mailing list, and the Boost developer mailing list.

I've blogged a few times about shared_ptr in the hopes of bringing those two audiences to appreciate the exclusive low level, C-style features of shared_ptr. The Boost crowd tends to think of shared_ptr as of any other smart pointer which simply calls delete automatically. The typical game development programmer basically ignores shared_ptr altogether, assuming that all it can do is call delete automatically (and they know when to call delete, thank you very much!)

I'll continue illustrating my point by sketching an interface design for socket programming using shared_ptr, based entirely on the standard C socket API structs.

The typical C++ approach would be to create an "object-oriented" system of classes, with all kinds of encapsulation and safety-net goodness throughout -- yet such interfaces have a fundamental problem. The whole point of defining a C++ socket programming layer is to provide type-safety for the low-level int handles. But to make the high level C++ interface practical, we have to punch a hole in the type system by providing a "getter" (and even a "setter") for the low level int handle!

And this means that we can no longer rely on type safety.

The important thing to consider is that the C interface is standard: sockets are represented by ints and that's that. The only reasonable way to improve the C API is make it easier to use, and harder to make silly errors. Below, I'm providing a few simple C++ functions which do just that.

Opening and closing sockets

First, here is how shared_ptr can be used to take care of closing a socket file descriptor when it is no longer needed:

namespace
{
  class socket_wrapper
  {
    socket_wrapper( socket_wrapper const & );
    socket_wrapper & operator=( socket_wrapper const & );
  public:
    int const s_;
    explicit socket_wrapper( int s ): s_(s) { }
    ~socket_wrapper()
    {
      (void) shutdown(s_,SHUT_RDWR);
      (void) close(s_);
    }
  }
}

boost::shared_ptr<int const> socket_adopt( int s )
{
  boost::shared_ptr<socket_wrapper> sw( new socket_wrapper(s) );
  return boost::shared_ptr<int const>(sw,&sw->s_);
}

The socket_wrapper class is an internal type, hidden in a CPP file where socket_adopt is defined. That function is intended to be called as soon as a socket file descriptor is returned from the standard C socket API, or from any other 3rd-party socket interface that opens a socket. It simply uses the shared_ptr aliasing constructor to rebind the initial shared_ptr<socket_wrapper> to point to the int stored in the socket_wrapper object. When the last shared_ptr instance expires ~socket_wrapper() will be called automatically, yet the user sees a simple shared_ptr<int const> which points the socket file descriptor!

The socket_create function below demonstrates how socket_adopt can be used:

boost::shared_ptr<int const> socket_create( int domain, int type, int protocol )
{
  int s = socket(domain,type,protocol);
  if( s!=-1 )
    return socket_adopt(s);
  else
    throw socket_error();
}

getaddrinfo

The old school gethostbyname function is now superseded by the POSIX function getaddrinfo. It is used to convert DNS names and IP addresses from their human-readable text form to a structured binary format for the OS:

int getaddrinfo(
  const char const * node,     // e.g. "www.example.com" or IP
  const char const * service,  // e.g. "http" or port number
  addrinfo const * hints,
  addrinfo * * res );

The caller passes an address and describes the type of acceptable protocols, and the function returns a linked list of addrinfo objects in res. Each returned addrinfo object's ai_family, ai_socktype and ai_protocol members can be passed to the socket() function (or to socket_create above) to open a matching new socket object. The linked list is allocated dynamically by the OS and must be disposed by calling freeaddrinfo. As before, we define a simple wrapper function that returns shared_ptr:

boost::shared_ptr<addrinfo const> socket_getaddrinfo( char const * node, char const * service, addrinfo const & hints )
{
  addrinfo * servinfo;
  if( int status=getaddrinfo(node,service,&hints,&servinfo) )
    throw socket_error();
  else
    return boost::shared_ptr<addrinfo>(servinfo,freeaddrinfo);
}

Remember, the returned shared_ptr object points to a linked list of addrinfo objects. We could keep the original shared_ptr around and walk the nodes using raw pointers, or we can advance the shared_ptr itself to the next node:

boost::shared_ptr<addrinfo const> next( boost::shared_ptr<addrinfo const> const & node )
{
  return boost::shared_ptr<addrinfo const>(node,node->ai_next);
}

The above function again takes advantage of the shared_ptr aliasing constructor. The returned shared_ptr object points to the next node in the addrinfo list, but when freeaddrinfo is called, it will be given the original addrinfo pointer, the one passed to the original shared_ptr object in socket_getaddrinfo.

Conclusion

The obvious benefit of using shared_ptr<int const> to manage the lifetime of sockets and file descriptors is that it decouples us from having to know how the file descriptor should be destroyed.

It is also possible to use type erasure to convert the shared_ptr<int const> to a shared_ptr<void const> and pass it to some kind of system which is concerned only with objects lifetime management regardless of their type.

Finally, shared_ptr comes with weak_ptr support. One example where this could be handy is in a logging system, which could easily detect if the log file has been closed by the application before attempting to write something in it.

Marshall — 07 January 2010, 17:46

That's a form of shared_ptr that I've never used before (heck, never _seen_ before); the sharing ownership with another shared pointer.

Cool!

Arseny Kapoulkine — 08 January 2010, 02:32

Alas, sockets in real world are not standard, even between win/*nix. The differences are not that great but they're enough for your code to not work :)

Add comment: 
Sign as author: 
Add 1 to 714 and enter it here: 

Formatting hint: when posting comments, surround code blocks in [@ and @].