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 transportwrite
: 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.