Source
stdlib/socket/socket.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
/**6
* @file socket.hpp7
* @brief cheatah `socket` — a small wrapper around BSD/POSIX TCP sockets,8
* in the spirit of Python's `socket`. `import socket` to use it.9
*10
* The recommended API is the owning `socket::Conn` / `socket::Listener` guards (from11
* `socket.open(host, port)` / `socket.serve(host, port, backlog)`), which close their fd at12
* scope exit. A **flat, file-descriptor-based** API is also available for hand-built servers:13
* pass the integer fd from `socket()` / `tcp_listen()` / `accept()` to the other calls (an14
* unclosed fd there is a resource leak — prefer the guards). IPv4 + TCP only; host names are15
* resolved with `getaddrinfo` (so `"localhost"`, `"127.0.0.1"`, and DNS names all work).16
* Errors are a negative return (or empty string for `recv`); `last_error()` gives the `errno` text.17
*18
* `import socket` includes this header AND links `libcheatah_socket`. Unit tests:19
* `stdlib/tests/socket_test.cpp`; the suite runs under AddressSanitizer (the20
* `asan` preset) and Valgrind (`security/run-valgrind.sh`) on every QA-gate run.21
*22
* @note Every call is a thin wrapper over one or two syscalls. Only `recv` and23
* `last_error` allocate (their returned `std::string`, plus `recv`'s reused24
* per-thread scratch buffer); the fd/status calls return a `long long` and do25
* not allocate (the resolver's transient `getaddrinfo` list is freed in-call).26
*/27
#include <string>29
namespace cheatah::socket {31
// ---- high-level convenience (what a server/client usually wants) ----33
/**34
* Create a TCP socket bound to @p host:@p port and put it in the listening state (sets35
* `SO_REUSEADDR`).36
*37
* Performs socket()/set_reuseaddr/bind/listen in one shot; on any failure it closes the38
* partially-created socket and returns -1, so the caller never leaks an fd. On success the39
* returned fd is owned by the caller and must be passed to close() when done.40
* @param host interface to bind ("127.0.0.1", "0.0.0.0", …).41
* @param port TCP port (0 = let the OS pick — read it back with local_port()).42
* @param backlog pending-connection queue length.43
* @return the listening fd, or -1 on error.44
* @complexity O(1) (a few syscalls).45
* @alloc none (the resolver's transient `getaddrinfo` list is freed before returning).46
* @test CheatahSocket.Loopback47
* @crtest SocketCompileRun.TcpListen48
* @systest StdlibE2E.Socket49
*/50
long long tcp_listen(const std::string& host, long long port, long long backlog);52
/**53
* Create a TCP socket and connect it to @p host:@p port.54
*55
* The host is resolved via `getaddrinfo`, so names, "localhost", and dotted IPs all work; the56
* connect blocks until the handshake completes or fails. On failure the socket is closed and57
* -1 is returned; on success the caller owns the connected fd and must close() it.58
* @param host destination host (name or IP).59
* @param port destination port.60
* @return the connected fd, or -1 on error.61
* @complexity O(1) + DNS resolution.62
* @alloc none (the resolver's transient `getaddrinfo` list is freed before returning).63
* @concurrency blocks until the TCP handshake completes or fails.64
* @test CheatahSocket.Loopback65
* @crtest SocketCompileRun.TcpConnect66
* @systest StdlibE2E.Socket67
*/68
long long tcp_connect(const std::string& host, long long port);70
// ---- per-connection I/O ----72
/**73
* Accept one pending connection.74
*75
* Blocks until a client connects, then returns a new fd for that one connection (the listening76
* fd stays open for further accepts). The returned client fd is owned by the caller and must be77
* closed separately; the peer address is discarded.78
* @param fd a listening fd.79
* @return the connected client fd, or -1 on error.80
* @complexity O(1) syscall (blocks until a client arrives).81
* @alloc none.82
* @concurrency blocks the calling thread until a client connects.83
* @test CheatahSocket.Loopback84
* @crtest SocketCompileRun.Accept85
* @systest StdlibE2E.Socket86
*/87
long long accept(long long fd);89
/**90
* Receive up to @p bufsize bytes.91
*92
* Blocks for one `recv` and returns whatever bytes arrive (possibly fewer than @p bufsize); the93
* result is binary-safe, so a returned string may contain embedded NULs and its length is the94
* true byte count. A clean EOF (peer closed) and an error both yield "", so check last_error()95
* to tell them apart; @p bufsize <= 0 also returns "" without touching the socket.96
* @param fd a connected fd.97
* @param bufsize maximum bytes to read.98
* @return the bytes read (binary-safe), or "" on EOF/error.99
* @complexity O(@p bufsize).100
* @alloc allocates the returned string (and grows a reused per-thread scratch buffer101
* up to @p bufsize on first use).102
* @concurrency blocks until data, EOF, or the set_timeout() deadline; a shutdown() from103
* another thread wakes it with EOF.104
* @test CheatahSocket.Loopback105
* @crtest SocketCompileRun.Recv106
* @systest StdlibE2E.Socket107
*/108
std::string recv(long long fd, long long bufsize);110
/**111
* Send some of @p data (one `send`).112
*113
* Issues a single `send`, which may transmit fewer bytes than supplied (a partial send); the114
* caller is responsible for re-sending the remainder, or use sendall() to loop automatically.115
* @param fd a connected fd.116
* @param data bytes to send.117
* @return bytes actually sent, or -1 on error.118
* @complexity O(n).119
* @alloc none (`MSG_NOSIGNAL`, so a broken pipe never raises `SIGPIPE`).120
* @test CheatahSocket.Sendall121
* @crtest SocketCompileRun.Send122
* @systest StdlibE2E.Socket123
*/124
long long send(long long fd, const std::string& data);126
/**127
* Send @p data in full, looping until all bytes are written.128
*129
* Repeatedly calls `send` on the unsent remainder until every byte is written, so unlike send()130
* there are no partial sends to handle; it aborts with -1 the moment a `send` returns <= 0131
* (error or peer hang-up), in which case some bytes may already have been transmitted.132
* @param fd a connected fd.133
* @param data bytes to send.134
* @return 0 on success, -1 on error.135
* @complexity O(n).136
* @alloc none.137
* @concurrency may block while the peer's receive window is full; bounded per `send`138
* by the set_timeout() send deadline.139
* @test CheatahSocket.Sendall140
* @crtest SocketCompileRun.Sendall141
* @systest StdlibE2E.Socket142
*/143
long long sendall(long long fd, const std::string& data);145
/**146
* Close a socket.147
*148
* Releases the fd back to the OS; after this the fd is invalid and must not be reused. Closing an149
* already-closed or never-opened fd fails with -1 (EBADF), which is how the BadFd test exercises150
* the error path.151
* @param fd the fd to close.152
* @return 0 on success, -1 on error.153
* @complexity O(1) syscall.154
* @alloc none.155
* @test CheatahSocket.BadFd156
* @crtest SocketCompileRun.Close157
* @systest StdlibE2E.Socket158
*/159
long long close(long long fd);161
/**162
* Half-close @p fd in both directions (::shutdown SHUT_RDWR) WITHOUT releasing it.163
* A blocking recv() on another thread returns immediately (EOF) — the safe way to164
* wake a reader for a clean shutdown. The fd is still owned by the caller and must165
* be close()d afterwards.166
* @param fd the fd to half-close.167
* @return 0 on success, -1 on error.168
* @complexity O(1) syscall.169
* @alloc none.170
* @concurrency safe to call from another thread while a recv() on @p fd blocks — waking171
* that reader is exactly what it is for.172
* @test CheatahSocket.TimeoutThenShutdown173
*/174
long long shutdown(long long fd);176
// ---- low-level BSD primitives (for clients/servers built by hand) ----178
/**179
* Create an IPv4 TCP socket.180
*181
* Allocates an unbound, unconnected AF_INET/SOCK_STREAM fd; you must follow up with bind()+listen()182
* or connect() before it can carry data, and close() it when done.183
* @return the new fd, or -1 on error.184
* @complexity O(1).185
* @alloc none.186
* @test CheatahSocket.ListenLowLevel187
* @crtest SocketCompileRun.Socket188
* @systest StdlibE2E.Socket189
*/190
long long socket();192
/**193
* Enable `SO_REUSEADDR` on @p fd.194
*195
* Lets a subsequent bind() reuse a local address still lingering in TIME_WAIT, so a restarted196
* server can re-listen on the same port immediately; call it before bind().197
* @warning `SO_REUSEADDR` trades TIME_WAIT protection for restartability: by skipping the198
* kernel's cooldown, delayed segments from a previous connection on the same199
* address can in principle reach the new socket.200
* @param fd the socket.201
* @return 0 on success, -1 on error.202
* @complexity O(1).203
* @alloc none.204
* @test CheatahSocket.ListenLowLevel205
* @crtest SocketCompileRun.SetReuseaddr206
* @systest StdlibE2E.Socket207
*/208
long long set_reuseaddr(long long fd);210
/**211
* Bind @p fd to @p host:@p port.212
*213
* Resolves @p host via `getaddrinfo` and assigns the resulting local address to the socket; a214
* resolution failure returns -1 with errno set to EADDRNOTAVAIL (see the ResolveFailure test).215
* @param fd the socket.216
* @param host interface to bind.217
* @param port TCP port (0 = OS-assigned).218
* @return 0 on success, -1 on error.219
* @complexity O(1) + resolution.220
* @alloc none (the resolver's transient `getaddrinfo` list is freed before returning).221
* @test CheatahSocket.ListenLowLevel, CheatahSocket.ResolveFailure222
* @crtest SocketCompileRun.Bind223
* @systest StdlibE2E.Socket224
*/225
long long bind(long long fd, const std::string& host, long long port);227
/**228
* Mark @p fd as a passive (listening) socket.229
*230
* Switches an already-bound socket into the listening state so accept() can pull connections from231
* it; @p backlog caps how many fully-established connections may queue before new ones are refused.232
* @param fd the socket.233
* @param backlog queue length.234
* @return 0 on success, -1 on error.235
* @complexity O(1).236
* @alloc none.237
* @test CheatahSocket.ListenLowLevel238
* @crtest SocketCompileRun.Listen239
* @systest StdlibE2E.Socket240
*/241
long long listen(long long fd, long long backlog);243
/**244
* Connect @p fd to @p host:@p port.245
*246
* Resolves @p host and blocks until the TCP handshake succeeds or fails; a refused connection247
* returns -1 with errno ECONNREFUSED (see the ConnectRefused test). Unlike tcp_connect() it does248
* not close the fd on failure — the caller still owns @p fd.249
* @param fd the socket.250
* @param host destination.251
* @param port destination port.252
* @return 0 on success, -1 on error.253
* @complexity O(1) + DNS.254
* @alloc none (the resolver's transient `getaddrinfo` list is freed before returning).255
* @concurrency blocks until the TCP handshake completes or fails.256
* @test CheatahSocket.ConnectRefused257
* @crtest SocketCompileRun.Connect258
* @systest StdlibE2E.Socket259
*/260
long long connect(long long fd, const std::string& host, long long port);262
/**263
* The local TCP port @p fd is bound to (useful after binding to port 0).264
*265
* Reads the address actually assigned via `getsockname` and returns its port in host byte order;266
* this is the way to discover the ephemeral port the OS chose when you bound to port 0.267
* @param fd a bound socket.268
* @return the port, or -1 on error.269
* @complexity O(1) syscall.270
* @alloc none.271
* @test CheatahSocket.Loopback272
* @crtest SocketCompileRun.LocalPort273
* @systest StdlibE2E.Socket274
*/275
long long local_port(long long fd);277
/**278
* Bound both blocking directions of @p fd by @p timeout_ms (SO_RCVTIMEO + SO_SNDTIMEO), so a279
* silent peer cannot hang a recv/send forever. A recv that times out returns "" (check280
* last_error() to distinguish from EOF). @p timeout_ms <= 0 clears the timeouts (block forever).281
*282
* @param fd a socket.283
* @param timeout_ms the per-operation bound in milliseconds.284
* @return 0 on success, -1 on error.285
* @complexity O(1) (two setsockopt calls).286
* @alloc none.287
* @test CheatahSocket.TimeoutThenShutdown288
*/289
long long set_timeout(long long fd, long long timeout_ms);291
/**292
* The message for the current `errno`.293
*294
* Returns the human-readable text for the thread's current `errno`; call it right after a function295
* reports failure (a -1 return, or "" from recv), since any later syscall may overwrite `errno`.296
* @return `strerror(errno)`.297
* @complexity O(1).298
* @alloc allocates the returned string.299
* @test CheatahSocket.ConnectRefused300
* @crtest SocketCompileRun.LastError301
* @systest StdlibE2E.Socket302
*/303
std::string last_error();305
// ---- owning RAII connections (the `with`-friendly, leak-proof API) ----307
/**308
* @brief An owning TCP connection — a socket fd whose destructor closes it.309
*310
* The RAII counterpart to the fd-based calls above, and the C++/cheatah analog of a311
* Python socket used in a `with` block. A `Conn` owns exactly one fd; when it is312
* destroyed (scope exit, including a `return`/`break`/exception out of a `with` body)313
* or explicitly close()d, the fd is released — so a connection opened with314
* `with socket.open(host, port) as c { … }` cannot leak. Move-only: copying would give315
* two owners of one fd and double-close it, so the copy operations are deleted and a316
* moved-from `Conn` is left closed.317
*/318
class Conn {319
public:320
/**321
* Construct a closed connection (owns no fd).322
* @complexity O(1).323
* @alloc none.324
* @test CheatahSocket.ConnDefaultIsClosed325
*/326
Conn() = default;327
/**328
* Adopt an already-connected fd (e.g. from tcp_connect()/accept()); the `Conn` now owns it.329
* @param fd a connected fd to take ownership of (-1 for a closed connection).330
* @complexity O(1).331
* @alloc none.332
* @test CheatahSocket.ConnGuardClosesOnScopeExit333
*/334
explicit Conn(long long fd) : fd_(fd) {}335
Conn(const Conn&) = delete;336
Conn& operator=(const Conn&) = delete;337
/**338
* Move-construct, taking over @p other's fd (the moved-from `Conn` becomes closed).339
* @param other the connection to move from.340
* @complexity O(1).341
* @alloc none.342
* @test CheatahSocket.ConnMoveTransfersOwnership343
*/344
Conn(Conn&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }345
/**346
* Move-assign, closing this fd first, then taking over @p other's (which becomes closed).347
* @param other the connection to move from.348
* @return reference to this connection.349
* @complexity O(1).350
* @alloc none.351
* @test CheatahSocket.ConnMoveTransfersOwnership352
*/353
Conn& operator=(Conn&& other) noexcept;354
/**355
* Close the fd if still open.356
* @complexity O(1) syscall.357
* @alloc none.358
* @test CheatahSocket.ConnGuardClosesOnScopeExit359
*/360
~Conn();362
/**363
* Is a connection open?364
* @return true iff this owns an open fd.365
* @complexity O(1).366
* @alloc none.367
* @test CheatahSocket.ConnDefaultIsClosed368
*/369
bool is_open() const { return fd_ >= 0; }370
/**371
* The raw fd, for the low-level calls or to hand to tls.open(conn.fd(), …).372
* @return the owned fd, or -1 when closed.373
* @complexity O(1).374
* @alloc none.375
* @test CheatahSocket.ConnGuardClosesOnScopeExit376
*/377
long long fd() const { return fd_; }378
/**379
* Send some of @p data (one send(); see the free send()).380
* @param data bytes to send.381
* @return bytes actually sent, or -1 on error.382
* @complexity O(n).383
* @alloc none.384
* @test CheatahSocket.ConnLoopback385
*/386
long long send(const std::string& data);387
/**388
* Send @p data in full, looping until every byte is written (see the free sendall()).389
* @param data bytes to send.390
* @return 0 on success, -1 on error.391
* @complexity O(n).392
* @alloc none.393
* @test CheatahSocket.ConnLoopback394
*/395
long long sendall(const std::string& data);396
/**397
* Receive up to @p bufsize bytes (see the free recv()).398
* @param bufsize maximum bytes to read.399
* @return the bytes read (binary-safe), or "" on EOF/error.400
* @complexity O(@p bufsize).401
* @alloc allocates the returned string (plus the free recv()'s reused per-thread402
* scratch buffer on growth).403
* @concurrency blocks until data, EOF, or the set_timeout() deadline.404
* @test CheatahSocket.ConnLoopback405
*/406
std::string recv(long long bufsize);407
/**408
* Bound both blocking directions by @p timeout_ms (see the free set_timeout()).409
* @param timeout_ms per-operation bound in milliseconds (<= 0 clears it).410
* @return 0 on success, -1 on error.411
* @complexity O(1).412
* @alloc none.413
* @test CheatahSocket.ConnLoopback414
*/415
long long set_timeout(long long timeout_ms);416
/**417
* The local TCP port this fd is bound to (see the free local_port()).418
* @return the port, or -1 on error.419
* @complexity O(1) syscall.420
* @alloc none.421
* @test CheatahSocket.ConnLoopback422
*/423
long long local_port() const;424
/**425
* Half-close both directions WITHOUT releasing the fd (see the free shutdown()) — wakes a426
* blocked reader for a clean shutdown; still call close() (or let the destructor) afterward.427
* @return 0 on success, -1 on error.428
* @complexity O(1) syscall.429
* @alloc none.430
* @test CheatahSocket.ConnLoopback431
*/432
long long shutdown();433
/**434
* Close the fd now (idempotent — the destructor will not close it again).435
* @return 0 on success, -1 if already closed.436
* @complexity O(1) syscall.437
* @alloc none.438
* @test CheatahSocket.ConnGuardClosesOnScopeExit439
*/440
long long close();442
private:443
long long fd_ = -1;444
};446
/**447
* @brief An owning listening socket; accept() yields owning Conn clients; the destructor closes it.448
*449
* The server-side RAII guard: `with socket.serve(host, port, backlog) as server { … }` keeps the450
* listening fd for the block and closes it on exit. Each accept() returns an owning Conn, so a451
* whole server loop leaks neither the listener nor its clients. Move-only, like Conn.452
*/453
class Listener {454
public:455
/**456
* Construct a closed listener (owns no fd).457
* @complexity O(1).458
* @alloc none.459
* @test CheatahSocket.ListenerDefaultIsClosed460
*/461
Listener() = default;462
/**463
* Adopt an already-listening fd (e.g. from tcp_listen()); the `Listener` now owns it.464
* @param fd a listening fd to take ownership of (-1 for a closed listener).465
* @complexity O(1).466
* @alloc none.467
* @test CheatahSocket.ListenerLoopback468
*/469
explicit Listener(long long fd) : fd_(fd) {}470
Listener(const Listener&) = delete;471
Listener& operator=(const Listener&) = delete;472
/**473
* Move-construct, taking over @p other's fd (the moved-from `Listener` becomes closed).474
* @param other the listener to move from.475
* @complexity O(1).476
* @alloc none.477
* @test CheatahSocket.ListenerLoopback478
*/479
Listener(Listener&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }480
/**481
* Move-assign, closing this fd first, then taking over @p other's (which becomes closed).482
* @param other the listener to move from.483
* @return reference to this listener.484
* @complexity O(1).485
* @alloc none.486
* @test CheatahSocket.ListenerLoopback487
*/488
Listener& operator=(Listener&& other) noexcept;489
/**490
* Close the listening fd if still open.491
* @complexity O(1) syscall.492
* @alloc none.493
* @test CheatahSocket.ListenerLoopback494
*/495
~Listener();497
/**498
* Is the listener open?499
* @return true iff this owns an open listening fd.500
* @complexity O(1).501
* @alloc none.502
* @test CheatahSocket.ListenerDefaultIsClosed503
*/504
bool is_open() const { return fd_ >= 0; }505
/**506
* The raw listening fd.507
* @return the owned fd, or -1 when closed.508
* @complexity O(1).509
* @alloc none.510
* @test CheatahSocket.ListenerLoopback511
*/512
long long fd() const { return fd_; }513
/**514
* Accept one pending connection, returned as an owning Conn (the listener stays open).515
* @return an owning Conn for the client (its is_open() is false on error).516
* @complexity O(1) syscall (blocks until a client arrives).517
* @alloc none.518
* @concurrency blocks the calling thread until a client connects.519
* @test CheatahSocket.ListenerLoopback520
*/521
Conn accept();522
/**523
* The local TCP port this listener is bound to (useful after binding to port 0).524
* @return the port, or -1 on error.525
* @complexity O(1) syscall.526
* @alloc none.527
* @test CheatahSocket.ListenerLoopback528
*/529
long long local_port() const;530
/**531
* Close the listening fd now (idempotent — the destructor will not close it again).532
* @return 0 on success, -1 if already closed.533
* @complexity O(1) syscall.534
* @alloc none.535
* @test CheatahSocket.ListenerLoopback536
*/537
long long close();539
private:540
long long fd_ = -1;541
};543
/**544
* Open a client TCP connection to @p host:@p port and return it as an owning Conn (the545
* RAII, `with`-friendly form of tcp_connect()).546
* @param host destination host (name or IP).547
* @param port destination port.548
* @return an owning Conn; on failure its is_open() is false (see last_error()).549
* @complexity O(1) + DNS resolution.550
* @alloc none beyond the Conn itself.551
* @concurrency blocks until the TCP handshake completes or fails.552
* @test CheatahSocket.ConnLoopback553
*/554
Conn open(const std::string& host, long long port);556
/**557
* Create a listening server socket bound to @p host:@p port and return it as an owning558
* Listener (the RAII, `with`-friendly form of tcp_listen()).559
* @param host interface to bind ("127.0.0.1", "0.0.0.0", …).560
* @param port TCP port (0 = let the OS pick — read it back with Listener::local_port()).561
* @param backlog pending-connection queue length.562
* @return an owning Listener; on failure its is_open() is false (see last_error()).563
* @complexity O(1) (a few syscalls).564
* @alloc none beyond the Listener itself.565
* @test CheatahSocket.ListenerLoopback566
*/567
Listener serve(const std::string& host, long long port, long long backlog);569
} // namespace cheatah::socket