cheatah
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 once
5/**
6 * @file socket.hpp
7 * @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 (from
11 * `socket.open(host, port)` / `socket.serve(host, port, backlog)`), which close their fd at
12 * 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 (an
14 * unclosed fd there is a resource leak — prefer the guards). IPv4 + TCP only; host names are
15 * 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 (the
20 * `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` and
23 * `last_error` allocate (their returned `std::string`, plus `recv`'s reused
24 * per-thread scratch buffer); the fd/status calls return a `long long` and do
25 * not allocate (the resolver's transient `getaddrinfo` list is freed in-call).
26 */
27#include <string>
29namespace 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 (sets
35 * `SO_REUSEADDR`).
36 *
37 * Performs socket()/set_reuseaddr/bind/listen in one shot; on any failure it closes the
38 * partially-created socket and returns -1, so the caller never leaks an fd. On success the
39 * 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.Loopback
47 * @crtest SocketCompileRun.TcpListen
48 * @systest StdlibE2E.Socket
49 */
50long 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; the
56 * connect blocks until the handshake completes or fails. On failure the socket is closed and
57 * -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.Loopback
65 * @crtest SocketCompileRun.TcpConnect
66 * @systest StdlibE2E.Socket
67 */
68long 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 listening
76 * fd stays open for further accepts). The returned client fd is owned by the caller and must be
77 * 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.Loopback
84 * @crtest SocketCompileRun.Accept
85 * @systest StdlibE2E.Socket
86 */
87long 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); the
93 * result is binary-safe, so a returned string may contain embedded NULs and its length is the
94 * 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 buffer
101 * up to @p bufsize on first use).
102 * @concurrency blocks until data, EOF, or the set_timeout() deadline; a shutdown() from
103 * another thread wakes it with EOF.
104 * @test CheatahSocket.Loopback
105 * @crtest SocketCompileRun.Recv
106 * @systest StdlibE2E.Socket
107 */
108std::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); the
114 * 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.Sendall
121 * @crtest SocketCompileRun.Send
122 * @systest StdlibE2E.Socket
123 */
124long 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 <= 0
131 * (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.Sendall
140 * @crtest SocketCompileRun.Sendall
141 * @systest StdlibE2E.Socket
142 */
143long 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 an
149 * already-closed or never-opened fd fails with -1 (EBADF), which is how the BadFd test exercises
150 * 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.BadFd
156 * @crtest SocketCompileRun.Close
157 * @systest StdlibE2E.Socket
158 */
159long 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 to
164 * wake a reader for a clean shutdown. The fd is still owned by the caller and must
165 * 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 — waking
171 * that reader is exactly what it is for.
172 * @test CheatahSocket.TimeoutThenShutdown
173 */
174long 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.ListenLowLevel
187 * @crtest SocketCompileRun.Socket
188 * @systest StdlibE2E.Socket
189 */
190long 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 restarted
196 * 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 the
198 * kernel's cooldown, delayed segments from a previous connection on the same
199 * 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.ListenLowLevel
205 * @crtest SocketCompileRun.SetReuseaddr
206 * @systest StdlibE2E.Socket
207 */
208long 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; a
214 * 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.ResolveFailure
222 * @crtest SocketCompileRun.Bind
223 * @systest StdlibE2E.Socket
224 */
225long 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 from
231 * 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.ListenLowLevel
238 * @crtest SocketCompileRun.Listen
239 * @systest StdlibE2E.Socket
240 */
241long 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 connection
247 * returns -1 with errno ECONNREFUSED (see the ConnectRefused test). Unlike tcp_connect() it does
248 * 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.ConnectRefused
257 * @crtest SocketCompileRun.Connect
258 * @systest StdlibE2E.Socket
259 */
260long 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.Loopback
272 * @crtest SocketCompileRun.LocalPort
273 * @systest StdlibE2E.Socket
274 */
275long long local_port(long long fd);
277/**
278 * Bound both blocking directions of @p fd by @p timeout_ms (SO_RCVTIMEO + SO_SNDTIMEO), so a
279 * silent peer cannot hang a recv/send forever. A recv that times out returns "" (check
280 * 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.TimeoutThenShutdown
288 */
289long 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 function
295 * 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.ConnectRefused
300 * @crtest SocketCompileRun.LastError
301 * @systest StdlibE2E.Socket
302 */
303std::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 a
311 * Python socket used in a `with` block. A `Conn` owns exactly one fd; when it is
312 * 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 with
314 * `with socket.open(host, port) as c { … }` cannot leak. Move-only: copying would give
315 * two owners of one fd and double-close it, so the copy operations are deleted and a
316 * moved-from `Conn` is left closed.
317 */
318class Conn {
319public:
320 /**
321 * Construct a closed connection (owns no fd).
322 * @complexity O(1).
323 * @alloc none.
324 * @test CheatahSocket.ConnDefaultIsClosed
325 */
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.ConnGuardClosesOnScopeExit
333 */
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.ConnMoveTransfersOwnership
343 */
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.ConnMoveTransfersOwnership
352 */
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.ConnGuardClosesOnScopeExit
359 */
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.ConnDefaultIsClosed
368 */
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.ConnGuardClosesOnScopeExit
376 */
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.ConnLoopback
385 */
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.ConnLoopback
394 */
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-thread
402 * scratch buffer on growth).
403 * @concurrency blocks until data, EOF, or the set_timeout() deadline.
404 * @test CheatahSocket.ConnLoopback
405 */
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.ConnLoopback
414 */
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.ConnLoopback
422 */
423 long long local_port() const;
424 /**
425 * Half-close both directions WITHOUT releasing the fd (see the free shutdown()) — wakes a
426 * 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.ConnLoopback
431 */
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.ConnGuardClosesOnScopeExit
439 */
440 long long close();
442private:
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 the
450 * listening fd for the block and closes it on exit. Each accept() returns an owning Conn, so a
451 * whole server loop leaks neither the listener nor its clients. Move-only, like Conn.
452 */
453class Listener {
454public:
455 /**
456 * Construct a closed listener (owns no fd).
457 * @complexity O(1).
458 * @alloc none.
459 * @test CheatahSocket.ListenerDefaultIsClosed
460 */
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.ListenerLoopback
468 */
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.ListenerLoopback
478 */
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.ListenerLoopback
487 */
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.ListenerLoopback
494 */
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.ListenerDefaultIsClosed
503 */
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.ListenerLoopback
511 */
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.ListenerLoopback
520 */
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.ListenerLoopback
528 */
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.ListenerLoopback
536 */
537 long long close();
539private:
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 (the
545 * 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.ConnLoopback
553 */
554Conn 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 owning
558 * 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.ListenerLoopback
566 */
567Listener serve(const std::string& host, long long port, long long backlog);
569} // namespace cheatah::socket