std1.0.0-beta-7 SafeC docs

Source sched/io_nb_bsd.sc

View in the repository

// SafeC Standard Library — Non-blocking file/socket helpers: BSD backend
// (macOS / iOS / FreeBSD — see io_nb.h for the portable API and the full
// backend file list)
#pragma once
#include <std/sched/io_nb.h>
#include <std/errno.h>
#include <std/time.h>

#define SCHED_O_CREAT     0x0200
#define SCHED_O_NONBLOCK  0x0004
#define SCHED_F_GETFL     3
#define SCHED_F_SETFL     4

namespace std {

// BSD/macOS 'struct sockaddr_in' (16 bytes): sin_len(1) sin_family(1)
// sin_port(2) sin_addr(4) sin_zero(8) — note macOS has the extra 1-byte
// sin_len field Linux's/Windows' sockaddr_in doesn't, ahead of sin_family
// (which is also only 1 byte here, not 2 as on those platforms).
struct SockAddrIn {
    unsigned char  sin_len;
    unsigned char  sin_family;
    unsigned short sin_port;   // network byte order — see sched_htons
    unsigned int   sin_addr;   // network byte order — see sched_htonl
    unsigned long  sin_zero;   // 8 bytes, always zero
};

extern int open(const char* path, int flags, int mode);
extern int socket(int domain, int type, int protocol);
extern int bind(int fd, const SockAddrIn* addr, unsigned int addrlen);
extern int listen(int fd, int backlog);
extern int accept(int fd, SockAddrIn* addr, unsigned int* addrlen);
extern int connect(int fd, const SockAddrIn* addr, unsigned int addrlen);
// Real libc fcntl is 'int fcntl(int fd, int cmd, ...)' — genuinely
// variadic, not (fd, cmd, arg). Declaring it with a fixed third 'int arg'
// parameter instead of '...' is a real ABI mismatch on arm64/AAPCS64
// (Apple Silicon passes variadic-position arguments differently than
// fixed ones): verified empirically that fd_set_nonblocking's
// fcntl(fd, F_SETFL, flags|O_NONBLOCK) call silently failed to actually
// set O_NONBLOCK with the fixed-arg declaration — a freshly-listened
// socket read back as still blocking, so accept() on it blocked forever
// instead of returning EAGAIN, hanging the reactor's acceptor task. Read
// back with the correct variadic declaration below, the exact same call
// works. x86_64 (Linux, and this same declaration in io_nb_linux.sc)
// happens not to hit this — its calling convention doesn't special-case
// variadic-position arguments the way AAPCS64 does — but this was never
// something to rely on either way.
extern int fcntl(int fd, int cmd, ...);
extern int setsockopt(int fd, int level, int optname, const unsigned char* optval, unsigned int optlen);

#define SCHED_SOL_SOCKET    0xffff
#define SCHED_SO_REUSEADDR  0x0004
// A write to a socket whose peer has closed raises SIGPIPE, which kills the
// process by default; BSD sockets opt out per socket (Linux does it per
// send with MSG_NOSIGNAL). Accepted sockets don't reliably inherit it.
#if defined(__APPLE__)
#define SCHED_SO_NOSIGPIPE  0x1022
#else
#define SCHED_SO_NOSIGPIPE  0x0800   // FreeBSD
#endif

static void sched_no_sigpipe_(int fd) {
    unsafe {
        int on = 1;
        setsockopt(fd, SCHED_SOL_SOCKET, SCHED_SO_NOSIGPIPE, (const unsigned char*)&on, 4U);
    }
}
#define SCHED_IPPROTO_TCP   6
#define SCHED_TCP_NODELAY   1
#define SCHED_SO_ERROR      0x1007
#define SCHED_SO_RCVTIMEO   0x1006
#define SCHED_SO_SNDTIMEO   0x1005
#define SCHED_POLLOUT       0x004
#define SCHED_EINTR         4
#define SCHED_EINPROGRESS   36

extern int getsockopt(int fd, int level, int optname, unsigned char* optval, unsigned int* optlen);
extern int close(int fd);

struct SchedPollFd {
    int   fd;
    short events;
    short revents;
};
extern int poll(SchedPollFd* fds, unsigned int nfds, int timeout);

// 'struct timeval' for SO_RCVTIMEO/SO_SNDTIMEO (16 bytes). FreeBSD's
// tv_usec is a long; macOS's is a 4-byte int followed by 4 bytes of
// padding, which on these little-endian targets has the same memory image
// as a long holding a value below 1e6.
struct SchedTimeval {
    long tv_sec;
    long tv_usec;
};

inline unsigned short sched_htons(unsigned short host16) {
    return (unsigned short)(((host16 & (unsigned short)0xFF) << 8) |
                             ((host16 >> 8) & (unsigned short)0xFF));
}

inline unsigned int sched_htonl(unsigned int host32) {
    return ((host32 & 0xFFU) << 24) | ((host32 & 0xFF00U) << 8) |
           ((host32 >> 8) & 0xFF00U) | ((host32 >> 24) & 0xFFU);
}

inline unsigned int sched_ipv4(unsigned char a, unsigned char b,
                                unsigned char c, unsigned char d) {
    unsigned int host = ((unsigned int)a << 24) | ((unsigned int)b << 16) |
                         ((unsigned int)c << 8)  |  (unsigned int)d;
    return sched_htonl(host);
}

inline int fd_set_nonblocking(int fd) {
    int flags;
    unsafe { flags = fcntl(fd, SCHED_F_GETFL, 0); }
    if (flags < 0) {
        return -1;
    }
    int rc;
    unsafe { rc = fcntl(fd, SCHED_F_SETFL, flags | SCHED_O_NONBLOCK); }
    return rc;
}

inline int fd_set_blocking(int fd) {
    int flags;
    unsafe { flags = fcntl(fd, SCHED_F_GETFL, 0); }
    if (flags < 0) {
        return -1;
    }
    int rc;
    unsafe { rc = fcntl(fd, SCHED_F_SETFL, flags & ~SCHED_O_NONBLOCK); }
    return rc;
}

inline int sock_would_block() {
    int e = errno_get();
    return (e == ERRNO_EAGAIN()) || (e == ERRNO_EWOULDBLOCK());
}

inline int fd_open_nb(const &char path, int flags, int mode) {
    int fd;
    unsafe { fd = open(path, flags | SCHED_O_NONBLOCK, mode); }
    return fd;
}

inline int tcp_listen_nb(unsigned short port) {
    int fd;
    unsafe { fd = socket(SCHED_AF_INET, SCHED_SOCK_STREAM, 0); }
    if (fd < 0) {
        return -1;
    }
    if (fd_set_nonblocking(fd) != 0) {
        return -1;
    }
    // Without SO_REUSEADDR, rebinding this exact port fails (EADDRINUSE)
    // for as long as any connection this process just served still has a
    // socket in TIME_WAIT on this port -- which after any real traffic is
    // the normal case, not an edge case. Verified: kill a server that just
    // handled a real load test, then immediately try to start a new one on
    // the same port -- it silently fails to bind (the old code didn't
    // check tcp_listen_nb's return value against this failure mode either,
    // so the process would print its own "listening" banner and then just
    // exit). Setting this is what lets "stop the server, start it again"
    // actually work like every other language's server here does.
    unsafe {
        int reuseOpt = 1;
        setsockopt(fd, SCHED_SOL_SOCKET, SCHED_SO_REUSEADDR,
                   (const unsigned char*)&reuseOpt, 4U);
    }

    struct SockAddrIn addr;
    addr.sin_len    = (unsigned char)16;
    addr.sin_family = (unsigned char)SCHED_AF_INET;
    addr.sin_port   = sched_htons(port);
    addr.sin_addr   = 0U; // INADDR_ANY
    addr.sin_zero   = 0UL;

    int rc;
    unsafe { rc = bind(fd, &addr, 16U); }
    if (rc != 0) {
        return -1;
    }
    // 512, not the socket API's traditional tiny default (historically
    // 5, sometimes seen as low as 16 in example code) -- verified this
    // actually matters: at 16, a burst of ~50 concurrent client
    // connections (a completely ordinary load-test concurrency, see
    // safec-docs's Benchmarks page) overflows the kernel's pending-accept
    // queue, and the connections that don't fit hit real TCP SYN-retransmit
    // delays (observed: 500ms+ tail latency on connect(), not on request
    // processing) waiting for a slot to free up -- not a "the OS is slow"
    // effect, a "we told it to only queue 16" one.
    unsafe { rc = listen(fd, 512); }
    if (rc != 0) {
        return -1;
    }
    return fd;
}

inline int tcp_accept_nb(int listenfd) {
    int fd;
    unsafe { fd = accept(listenfd, (SockAddrIn*)0, (unsigned int*)0); }
    if (fd < 0) {
        return -1;
    }
    if (fd_set_nonblocking(fd) != 0) {
        return -1;
    }
    // Nagle's algorithm batches small writes to wait for either a full
    // segment or the peer's ACK before sending — request/response
    // protocols like HTTP, where each side writes once and waits, get no
    // benefit from that batching and instead just pay its latency
    // (interacting with the peer's delayed-ACK timer, tens of ms per
    // hop). Disabling it on the accepted socket is the standard fix for
    // any server whose protocol already frames its own messages.
    unsafe {
        int nodelayOpt = 1;
        setsockopt(fd, SCHED_IPPROTO_TCP, SCHED_TCP_NODELAY,
                   (const unsigned char*)&nodelayOpt, 4U);
    }
    sched_no_sigpipe_(fd);
    return fd;
}

// socket()+non-blocking connect() shared by tcp_connect_nb and
// tcp_connect_timeout. Returns the fd (or -1, with nothing left open) and
// stores 0 in 'err_out' if connect() succeeded immediately, otherwise
// connect()'s errno (normally EINPROGRESS).
static int sched_connect_begin_(unsigned int addr_network_order, unsigned short port,
                                &int err_out) {
    int fd;
    unsafe { fd = socket(SCHED_AF_INET, SCHED_SOCK_STREAM, 0); }
    if (fd < 0) {
        return -1;
    }
    sched_no_sigpipe_(fd);
    if (fd_set_nonblocking(fd) != 0) {
        unsafe { close(fd); }
        return -1;
    }

    struct SockAddrIn addr;
    addr.sin_len    = (unsigned char)16;
    addr.sin_family = (unsigned char)SCHED_AF_INET;
    addr.sin_port   = sched_htons(port);
    addr.sin_addr   = addr_network_order;
    addr.sin_zero   = 0UL;

    int rc;
    unsafe { rc = connect(fd, &addr, 16U); }
    int e = (rc == 0) ? 0 : errno_get();
    unsafe { *err_out = e; }
    return fd;
}

inline int tcp_connect_nb(unsigned int addr_network_order, unsigned short port) {
    // Non-blocking connect(): a non-zero return here almost always just
    // means EINPROGRESS (the handshake hasn't finished yet), not failure —
    // the caller awaits SCHED_WRITE on 'fd' and treats writability as
    // "connect attempt finished" per the header comment, so the return
    // value here isn't itself meaningful the way it is for a blocking
    // connect().
    int err = 0;
    return sched_connect_begin_(addr_network_order, port, &err);
}

// Waits for an in-flight non-blocking connect on 'fd' to finish, for at
// most 'timeout_ms' (<= 0: no limit). Returns 0 if it connected,
// otherwise the error (ETIMEDOUT on timeout, else SO_ERROR's value).
static int sched_wait_connected_(int fd, int timeout_ms) {
    long long deadline = 0LL;
    if (timeout_ms > 0) {
        deadline = time_mono_ns() + (long long)timeout_ms * 1000000LL;
    }
    while (1) {
        int wait_ms = -1;
        if (timeout_ms > 0) {
            long long left = deadline - time_mono_ns();
            wait_ms = (left <= 0LL) ? 0 : (int)((left + 999999LL) / 1000000LL);
        }
        struct SchedPollFd pfd;
        pfd.fd = fd;
        pfd.events = (short)SCHED_POLLOUT;
        pfd.revents = (short)0;
        int n;
        unsafe { n = poll(&pfd, 1U, wait_ms); }
        if (n > 0) {
            int soerr = 0;
            unsigned int len = 4U;
            int grc;
            unsafe { grc = getsockopt(fd, SCHED_SOL_SOCKET, SCHED_SO_ERROR,
                                      (unsigned char*)&soerr, &len); }
            return (grc != 0) ? errno_get() : soerr;
        }
        if (n == 0) {
            return ERRNO_ETIMEDOUT();
        }
        int perr = errno_get();
        if (perr != SCHED_EINTR) {
            return perr;
        }
    }
    return 0;
}

inline int tcp_wait_connected(int fd) {
    int err = sched_wait_connected_(fd, 0);
    if (err != 0) {
        errno_set(err);
        return -1;
    }
    return 0;
}

inline int tcp_connect_timeout(unsigned int addr_network_order, unsigned short port,
                               int timeout_ms) {
    int err = 0;
    int fd = sched_connect_begin_(addr_network_order, port, &err);
    if (fd < 0) {
        return -1;
    }
    if (err == SCHED_EINPROGRESS) {
        err = sched_wait_connected_(fd, timeout_ms);
    }
    if (err == 0 && fd_set_blocking(fd) != 0) {
        err = errno_get();
    }
    if (err != 0) {
        unsafe { close(fd); }
        errno_set(err);
        return -1;
    }
    return fd;
}

inline int sock_set_io_timeout(int fd, int timeout_ms) {
    struct SchedTimeval tv;
    int ms = (timeout_ms > 0) ? timeout_ms : 0;
    tv.tv_sec  = (long)(ms / 1000);
    tv.tv_usec = (long)((ms % 1000) * 1000);
    int rc1;
    int rc2;
    unsafe {
        rc1 = setsockopt(fd, SCHED_SOL_SOCKET, SCHED_SO_RCVTIMEO, (const unsigned char*)&tv, 16U);
        rc2 = setsockopt(fd, SCHED_SOL_SOCKET, SCHED_SO_SNDTIMEO, (const unsigned char*)&tv, 16U);
    }
    return (rc1 == 0 && rc2 == 0) ? 0 : -1;
}

} // namespace std