Skip to content

Input/Output (I/O) abstraction

When the Sandwich library needs to transport a stream of data, it does so through a generic I/O interface. Said differently, Sandwich isn't opinionated on the way data should be transported between the peers of a tunnel.

The I/O interface has three high-level API calls that are provided by the user:

  • read: read a specified number of bytes for the underlying transport
  • write: write a buffer to the underlying transport

It also contains a view of a generic object that can represent any state that is needed to do the actual transport. I/O objects are always owned by a tunnel, giving the ability to the I/O APIs to have access to their parent tunnel, and for instance getting the current state of the tunnel.

The I/O interface also supports asynchronous operations, and can return specific error codes for such a purpose.

C API

The I/O interface is described in the C API through the SandwichIO structure.

Here is an example of an I/O structure that would forward the data to a socket in C++:

/// \brief Read from a socket.
///
/// This method is a SandwichIOReadFunction.
auto SandwichReadFromSocket(void *uarg, void *buf, const size_t count,
                            enum ::SandwichIOError *err) -> size_t {
  *err = SANDWICH_IOERROR_OK;

  const auto fd = static_cast<int>(reinterpret_cast<uintptr_t>(uarg));

  ssize_t r{0};

  do {
    if (r = ::read(fd, buf, count); r > -1) {
      return static_cast<size_t>(r);
    }
  } while ((r == -1) && (errno == EINTR));

  switch (errno) {
  case 0: {
    return *err = SANDWICH_IOERROR_OK, 0;
  }
  case EINPROGRESS:
  case EINTR: {
    return *err = SANDWICH_IOERROR_IN_PROGRESS, 0;
  }

  case EWOULDBLOCK:
#if EWOULDBLOCK != EAGAIN
  case EAGAIN:
#endif
  {
    return *err = SANDWICH_IOERROR_WOULD_BLOCK, 0;
  }

  case ENOTSOCK:
  case EPROTOTYPE:
  case EBADF: {
    return *err = SANDWICH_IOERROR_INVALID, 0;
  }
  case EACCES:
  case EPERM:
  case ETIMEDOUT:
  case ENETUNREACH:
  case ECONNREFUSED: {
    return *err = SANDWICH_IOERROR_REFUSED, 0;
  }

  default: {
    return *err = SANDWICH_IOERROR_UNKNOWN, 0;
  }
  }
}

/// \brief Write to a socket.
///
/// This method is a SandwichIOWriteFunction.
auto SandwichWriteToSocket(void *uarg, const void *buf, const size_t count,
                           enum ::SandwichIOError *err) -> size_t {
  *err = SANDWICH_IOERROR_OK;

  const auto fd = static_cast<int>(reinterpret_cast<uintptr_t>(uarg));

  ssize_t w{0};

  do {
    if (w = ::write(fd, buf, count); w > -1) {
      return static_cast<size_t>(w);
    }
  } while ((w == -1) && (errno == EINTR));

  switch (errno) {
  case 0: {
    return *err = SANDWICH_IOERROR_OK, 0;
  }
  case EINPROGRESS:
  case EINTR: {
    return *err = SANDWICH_IOERROR_WOULD_BLOCK, 0;
  }
  case ENOTSOCK:
  case EPROTOTYPE:
  case EBADF: {
    return *err = SANDWICH_IOERROR_INVALID, 0;
  }
  case EACCES:
  case EPERM:
  case ETIMEDOUT:
  case ENETUNREACH:
  case ECONNREFUSED: {
    return *err = SANDWICH_IOERROR_REFUSED, 0;
  }

  default: {
    return *err = SANDWICH_IOERROR_UNKNOWN, 0;
  }
  }
}

/// \brief Close a socket.
///
/// This method is a SandwichIOCloseFunction.
void CloseSocket(void *uarg) {
  const auto fd = static_cast<int>(reinterpret_cast<uintptr_t>(uarg));
  ::close(fd);
}

/// \brief Global IO interface for sockets.
constexpr struct ::SandwichIO SandwichSocketIO = {
    .read = SandwichReadFromSocket,
    .write = SandwichWriteToSocket,
    .uarg = nullptr};

/// \brief Global tunnel IO interface for sockets.
constexpr struct ::SandwichTunnelIO SandwichSocketTunnelIO {
  .base = SandwichSocketIO, .set_state = nullptr,
};

Go API

Go users need to implement the sandwich.TunnelIO interface. This interface extends the io.ReadWriter go interface.