The System Interface

We can mitigate platform dependence to a large extent by introducing a system interface that declares common system services. Of course the implementation of the interface will vary from one platform to the next, but applications that depend on the interface will be platform independent.

More concretely, when an application begins, it instantiates the System implementation class. A pointer to this instance is assigned to a public, static System member variable:

ifdef WINDOWS
   System* System::impl = new Win32System();
endif
ifdef UNIX
   System* System::impl = new UNIXSystem();
endif
ifdef MAC
   System* System::impl = new MACSystem();
endif

Application functions can request platform services by delegating to the implementation pointer:

System::impl->serviceX();
System::impl->serviceY();
System::impl->serviceZ();

Implementing Threads

See Concurrency.htm for a discussion of how to multi-threading in C++.

System Threads

Of course the System interface will require implementers to provide basic functions for creating, running, and stopping threads. Typically, these services will be implemented by the underlying system's thread manager. Like most resource managers,  the thread manager probably won't allow clients to directly access the threads it manages. Instead, clients will refer to these threads using some form of object identifiers (OIDs) supplied by the thread manager. To be as general as possible, we define thread OIDs  to be void pointers:

typedef void* ThreadOID;

The System interface specifies a thread factory method that creates a thread and returns its corresponding OID. Subsequently, clients will use this OID to refer to the thread:

class System
{
public:
   // thread stuff:
   virtual ThreadOID makeThread(Thread* thread) = 0;
   virtual void resume(ThreadOID oid) = 0;
   virtual void suspend(ThreadOID oid) = 0;
   virtual void sleep(int ms) = 0;
   // etc.
   static System* impl; // THE implementation
};

Implementing the Thread Class

Starting a thread sets its state to RUNNING and initializes the thread peer member by calling makeThread(). Notice that the lifetime of an application-level thread does not correspond to the lifetime of its associated system-level thread. The system-level thread is created by the call to start(), which must have an implicit parameter that points to an application-level thread that already exists:

inline void Thread::start()
{
   if (System::impl && state == READY)
   {
      state = RUNNING;
      peer = System::impl->makeThread(this);
   }
}

Most Thread member functions manage the thread's state and delegate to the appropriate system implementation member function (this is just another instance of the Handle-Body pattern):

inline void Thread::suspend()
{
   if (System::impl && state == RUNNING)
   {
      state = BLOCKED;
      System::impl->suspend(peer);
   }
}

inline void Thread::resume()
{
   if (System::impl && state == BLOCKED)
   {
      state = RUNNING;
      System::impl->resume(peer);
   }
}

inline void Thread::sleep(int ms)
{
   if (System::impl && state == RUNNING)
   {
      state = BLOCKED;
      System::impl->sleep(ms);
      state = RUNNING;
   }
}

inline void stop() { state = TERMINATED; }

Implementing Locks

When a thread attempts to lock a locking mechanism such as a semaphore or mutex, several things must happen. If the lock is in an unlocked state, then it must be put into a locked state. If the lock is already in a locked state, then the thread must be blocked and placed on a queue associated with the lock. Furthermore, the lock() operation must be indivisible. This means interrupts must be disabled while the operation is in progress. (Why?)

Because locks have special needs, we will require implementations of our System interface to provide methods for creating, locking, and unlocking locks. Naturally, locks will be managed by the operating system. At the application level locks will be identified by a special object identifier:

typedef void* LockOID;

This identifier is returned by the lock factory method. Subsequent lock operations will use this identifier to refer to the lock:

class System
{
public:
   // lock stuff:
   virtual LockOID makeLock() = 0;
   virtual void lock(LockOID oid) = 0;
   virtual void unlock(LockOID oid) = 0;
   // etc.
};

Instances of the Lock class encapsulates the lock oid, which is initialized by the constructor. Lock member functions delegate to the system-level lock through this identifier:

class Lock
{
public:
   Lock()
   {
      if (System::impl) peer = System::impl->makeLock();
   }
   void lock()
   {
      if (System::impl) System::impl->lock(peer);
   }
   void unlock()
   {
      if (System::impl) System::impl->unlock(peer);
   }
private:
   LockOID peer;
};

System Exceptions

A lot can go wrong when we request a platform service: requested files may be missing or locked, network connections may go down, applications may attempt to start a thread that doesn't exist or lock a lock that doesn't exist. We can create a hierarchy of exceptions to report these situations:

class SysException: public runtime_error
{
public:
   SysException(string gripe = "system error")
      :runtime_error(gripe)
   {}
};

class ThreadException: public SysException
{
public:
   ThreadException(string gripe = "thread error")
      :SysException(gripe)
   {}
};

class LockException: public SysException
{
public:
   LockException(string gripe = "lock error")
      :SysException(gripe)
   {}
};

class SockException: public SysException
{
public:
   SockException(string gripe = "socket error")
      :SysException(gripe)
   {}
};

An MS Windows Implementation of System

All Windows platforms must implement the Win32 API, which, fortunately, includes a good supply of C functions for manipulating threads and locks. These functions are declared in:

#include <windows.h>

Here is our implementation:

class Win32System: public System
{
public:
   // thread stuff:
   ThreadOID makeThread(Thread* thread);
   void resume(ThreadOID oid)
   {
      if (ResumeThread(oid) < 0)
         throw ThreadException("can't resume thread");
   }
   void suspend(ThreadOID oid)
   {
      if (SuspendThread(oid) < 0)
         throw ThreadException("can't suspend thread");
   }
   void sleep(int ms) { Sleep(ms); }

   // lock stuff:
   LockOID makeLock();
   void lock(LockOID oid);
   void unlock(LockOID);
   //etc.
};

Win32System::makeThread() feeds the required arguments to the global, Win32 API CreateThread() function, which returns the OID of the thread it creates:

ThreadOID Win32System::makeThread(Thread* thread)
{
   unsigned long threadID; // not used
   ThreadOID oid =
      CreateThread(
         0, // default thread attributes
         0, // default stack size
         (LPTHREAD_START_ROUTINE) // barf!
         threadStarter, // run this in thread
         thread, // starter param
         0, // creation flags
         &threadID); // thread id
   if (!oid)
      throw ThreadException("can't create thread");
   return oid;
};

A Windows thread begins executing as soon as it's created. But what does it execute? The third argument passed to CreateThread() is a pointer to the function that is executed by the created thread. This function can't be any type of function. It must be a function of type LPTHREAD_START_ROUTINE, basically, any function that expects a void pointer as input and returns an unsigned long as output.

Every thread we create will execute our global threadStarter() function. But the void* argument passed to this function will always be a pointer to a Thread. Internally, threadStart() calls this thread's run() member function:

unsigned long threadStarter(void* threadObj)
{
   Thread* thread = (Thread*) threadObj;
   thread->run();
   return 0;
}

Locking Methods

The Win32 API provides a variety of synchronization objects including critical sections, mutexes, and semaphores. Our locks are based on mutexes:

Win32System::makeLock() calls feeds the appropriate arguments to the global, Win32 API CreateMutex() function:

LockOID Win32System::makeLock()
{
   LockOID oid =
      CreateMutex (
            0,       // No security attributes
            false,   // Initially not owned
            "Lock"); // Name of mutex
   if (!oid)
      throw LockException("can't create lock");
   return oid;
}

Win32System::lock() passes the oid returned by CreateMutex() to the global, Win32 API WaitForSingleObject() function. This will cause the caller to suspend either until the mutex is unlocked, or until an INFINITE amount of time elapses:

void Win32System::lock(LockOID oid)
{
   unsigned long status =
      WaitForSingleObject(oid, INFINITE);
   switch (status)
   {
    case WAIT_OBJECT_0:
      // everything's okay
      break;
    case WAIT_TIMEOUT:
      throw LockException("timed out");
    case WAIT_ABANDONED:
      throw LockException("mutex abandoned");
   }
}

Win32System::unlock() passes the oid returned by CreateMutex() to the global, Win32 API ReleaseMutex() function to unlock the associated mutex:

void Win32System::unlock(LockOID oid)
{
   if (!ReleaseMutex(oid))
      throw LockException("can't release mutex");
}

Implementing Sockets

See Sockets.htm for a discussion of how to use sockets.

Fortunately, there are only minor variations in the implementation of the Socket API from one platform to the next. We can manage the differences in the Windows and UNIX implementations using global identifiers UNIX and WINDOWS, that are defined or undefined in our socket header file:

// socket.h
#ifndef SOCK_H
#define SOCK_H

#ifndef UNIX
   #define WINDOWS
   #include <windows.h>
#else // UNIX includes
   #include <sys/types.h>
   #include <sys/socket.h>
   #include <netinet/in.h>
   #include <netdb.h>
#endif

#include <iostream>
#include <string>
#include <stdexcept>
using namespace std;

#define PacketSize  125
typedef char Packet[PacketSize];
typedef SOCKET SocketOID;
class Socket { ... };
#endif

Note: On Windows platforms the oldnames.lib and ws2_32.lib libraries need to be linked into programs that use sockets.

send() and receive()

The Socket::send() function invokes the global send() function from the Socket API:

void Socket::send(string msg)
{
   int status =
      ::send(peer, msg.c_str(), msg.size(), 0);
   if (status == SOCKET_ERROR)
      throw(SockException("send failed"));
}

The Socket::receive() function is trickier, because we need to allocate space for the received message without knowing how long it will be. In socket.h we arbitrarily defined a packet to be an array of 125 characters:

#define PacketSize  125
typedef char Packet[PacketSize];

Socket::receive() uses the global recv() function from the Socket API to fill a packet, p, with received characters. The packet is converted into a C++ string and returned to the caller:

string Socket::receive()
{
   Packet p;
   int bytes // # of bytes received
      = recv(peer, p, PacketSize, 0);
   if (bytes == SOCKET_ERROR)
      throw(SockException("receive failed"));
   string msg(p, bytes);
   return msg;
}

The Socket destructor uses the global closesocket() function from the Socket API to close the socket:

Socket::~Socket() { closesocket(peer); }

Socket::Socket()

The Socket constructor is messy. The steps are:

1. Using the IP address or DNS name of the server that we want to connect to, create a "host entry" system level object.

2. Using the host entry from step 1 and the port number of the server, create a "server location" system level object.

3. Create a system level socket object with the desired protocol (UDP, TCP, etc.). This object will be the peer of the application level socket object created by the constructor.

4. Connect the system level socket object to the system level server location object.

Here's our implementation:

Socket::Socket(int port, string name)
{
   int status;

#ifdef WINDOWS
   WSADATA wsaData;
   status = WSAStartup(0x202,&wsaData);
   if (status == SOCKET_ERROR)
      throw(SockException("WSAStartup() failed"));
#endif

   // create a host entry object
   hostent* hp = 0; // host entry pointer
   if (isalpha(name[0])) // convert domain name
      hp = gethostbyname(name.c_str());
   else 
   {  // convert IP address
      int addr = inet_addr(name.c_str());
      hp = gethostbyaddr((char *)&addr, sizeof(&addr), AF_INET);
   }
   if (!hp)
      throw(SockException("gethostbyxxx() failed"));

   // create a location object
   sockaddr_in serverLoc;  // IP address & port of server
   memset(&serverLoc, 0, sizeof(serverLoc));
   memcpy(&(serverLoc.sin_addr), hp->h_addr, hp->h_length);
   serverLoc.sin_family = hp->h_addrtype;
   serverLoc.sin_port = htons(port);

   // get a socket oid
   int protoFamily = AF_INET; // TCP/IP
   int qos = SOCK_STREAM; // reliable & connection-oriented
   int protocol = 0;
   peer = socket(protoFamily, qos, protocol);
   if (peer < 0)
      throw(SockException("socket() failed"));
   // connect to server
   status =
      connect(peer, (sockaddr*)&serverLoc, sizeof(serverLoc));
   if (status == SOCKET_ERROR)
      throw(SockException("connect() failed"));
}

Servers

Implementing the Server Constructor

Servers communicate through a special type of system-level socket called a server socket. We begin by creating a system-level "server location" object from the specified port number. (The IP address is implicitly understood to be the IP address of the machine that creates the server socket.) Next, we create a system-level socket that realizes a given protocol (TCP in our case). Finally, instead of connecting the socket to the location, we bind the socket to the location using the Socket API bind() function:

Server::Server(int port)
{
   int status;

#ifdef WINDOWS
   WSADATA wsaData;
   status = WSAStartup(0x202,&wsaData);
   if (status == SOCKET_ERROR)
      throw(SockException("Startup() failed"));
#endif

   int protoFamily = AF_INET; // TCP/IP
   int qos = SOCK_STREAM; // reliable & connection-oriented
   int protocol = 0;
   sockaddr_in serverLoc;
   memset(&serverLoc, 0, sizeof(serverLoc));
   serverLoc.sin_family = protoFamily;
   serverLoc.sin_addr.s_addr = INADDR_ANY;
   serverLoc.sin_port = htons(port);

   // creating descriptor
   peer = socket(protoFamily, qos, protocol);
   if (peer == INVALID_SOCKET)
      throw(SockException("socket() failed"));

   // binding
   status =
      bind(peer, (sockaddr*)&serverLoc, sizeof(serverLoc));
   if (status == SOCKET_ERROR)
      throw(SockException("bind() failed"));
}

Implementing Server::listen()

Server::listen() calls the global listen() function from the Socket API. This function associates a request queue with the server socket. The request queue holds pending client requests waiting for service. Each time through the while loop the server extracts a client request from this queue and creates a server-side socket connected to the client side socket. All of this is accomplished by calling the global accept() function from the Socket API. If the request queue is empty, then accept() blocks the server until a request arrives.

The socket created by the call to accept(), together with a pointer to the server, is passed to the virtual factor method that creates slave threads. The slave is started, and the loop repeats.

void Server::listen()
{
   bool more = true;
   sockaddr_in from;
   int fromlen = sizeof(from);
   int queueSize = 5;
   int status = ::listen(peer, queueSize);
   if (status == SOCKET_ERROR)
      throw(SockException("listen() failed"));

   while(more)
   {
      cout << "\nthe server is listening ... \n";
      SOCKET client;
      client = accept(peer, (sockaddr*)&from, &fromlen);

      if (client == INVALID_SOCKET)
         throw(SockException("accept() failed"));

      cout << "\n\nconnection accepted!\n";
      cout << "client IP   = " << inet_ntoa(from.sin_addr) << endl;
      cout << "client port = " << htons(from.sin_port) << "\n\n";
     
      ServerSlave* slave = makeSlave(new Socket(client), this);
      slave->start();
   }

   cout << "The server is shutting down!\n";
}