Module sys/tcp

sys/tcp
Stability: unstable — the address representation is what has to change first. `SockAddr` is a malloc'd byte buffer plus a length that the CALLER must free, with the field offsets baked into `make_sockaddr_in` (`buf.add(4)` for `sin_addr`, `buf.add(8)` for `sin6_addr`) rather than asked of the platform, and `make_sockaddr_in` silently ignores a failed `inet_pton` so a malformed IP string yields a zero address instead of an error. On top of that the error channel is two different vocabularies (errno and WSA) and the byte-order helpers are exported raw. Freezing needs an owning address type with a fallible parser and one error vocabulary; `std/net/tcp` already provides both, and is the surface to build on. — stable modules only change additively; this one may still change.

Async TCP sockets — the raw syscall boundary.

The socket calls plus the sockaddr_in/sockaddr_in6 builders that every other socket module here reuses (std/sys/udp and std/sys/unix both import from this one). std/net/tcp.yo is the public surface — TcpStream, TcpListener and the IpAddr parsing this layer does not do.

Every operation returns an IoFuture resolving to a non-negative value on success (an fd for socket and accept, a byte count for send/recv, 0 for the rest) or a NEGATIVE error code. On POSIX that code is an errno; on Windows the socket paths return a negated WSA code (-10061 rather than -ECONNREFUSED), which IoError.from_errno will not classify.

How much of this is genuinely asynchronous differs per platform, and it matters for latency, not just for tidiness: socket, bind, listen, shutdown, setsockopt and getsockopt are synchronous everywhere and merely wrapped in an already-completed future, while accept, connect, send and recv are real io_uring submissions on Linux, kqueue registrations on macOS (which also sets O_NONBLOCK on every socket it creates) and IOCP operations on Windows — except connect, which is still a BLOCKING connect() on the event-loop thread there.

tcp :: import("std/sys/tcp");
{ AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR } :: import("std/sys/socket");

fd := io.await(tcp.socket(AF_INET, SOCK_STREAM, i32(0)), io);
addr := tcp.make_sockaddr_in_loopback(u16(8080));
io.await(tcp.bind(fd, addr.buf, addr.len), io);
io.await(tcp.listen(fd, i32(128)), io);
client := io.await(tcp.accept(fd, addr.buf, ...), io);
tcp.free_sockaddr(addr);

Stability

unstable — the address representation is what has to change first. SockAddr is a malloc'd byte buffer plus a length that the CALLER must free, with the field offsets baked into make_sockaddr_in (buf.add(4) for sin_addr, buf.add(8) for sin6_addr) rather than asked of the platform, and make_sockaddr_in silently ignores a failed inet_pton so a malformed IP string yields a zero address instead of an error. On top of that the error channel is two different vocabularies (errno and WSA) and the byte-order helpers are exported raw. Freezing needs an owning address type with a fallible parser and one error vocabulary; std/net/tcp already provides both, and is the surface to build on.

Types

SockAddr struct
SockAddr

A heap-allocated socket address buffer and its length — what the bind, connect and accept calls below take.

It OWNS its allocation but has no Dispose: every make_sockaddr_* must be paired with a free_sockaddr, or the buffer leaks. The len is the size of the platform's sockaddr_in/sockaddr_in6, not the number of bytes meaningfully filled.

Fields

NameTypeDescription
buf*(u8)

The sockaddr bytes. Read them with get_family first, then with the get_*_in or get_*_in6 accessors for that family.

lenu32

Size of buf in bytes — the addrlen every socket call wants.

Functions

socket function
fn(domain : i32, sock_type : i32, protocol : i32) -> IoFuture

Create a socket — POSIX socket(2). Resolves to the new fd, or a negative error code. domain is AF_INET or AF_INET6, sock_type is normally SOCK_STREAM, and protocol 0 lets the kernel pick (TCP for SOCK_STREAM).

Synchronous underneath on every platform. On macOS the runtime also sets O_NONBLOCK on the new descriptor, because its kqueue backend requires it — so a socket from here behaves differently under a blocking std/sys/iov read on macOS than on Linux.

Parameters

NameTypeNotes
domaini32
sock_typei32
protocoli32

Returns: IoFuture

bind function
fn(sockfd : i32, addr : *u8, addrlen : u32) -> IoFuture

Bind the socket to a local address — POSIX bind(2). Resolves to 0, or a negative error code (-EADDRINUSE when the port is taken, which is what SO_REUSEADDR is for). Binding to port 0 asks the kernel to choose; read the choice back with std/sys/sockinfo's getsockname.

Parameters

NameTypeNotes
sockfdi32
addr*u8
addrlenu32

Returns: IoFuture

listen function
fn(sockfd : i32, backlog : i32) -> IoFuture

Mark the socket as accepting connections — POSIX listen(2). Resolves to 0, or a negative error code. backlog bounds the queue of connections the kernel completes before accept has taken them; on Linux it is capped by somaxconn, so a large value is a request rather than a guarantee.

Parameters

NameTypeNotes
sockfdi32
backlogi32

Returns: IoFuture

accept function
fn(sockfd : i32, addr : *u8, addrlen : *u32) -> IoFuture

Take the next completed connection off a listening socket's queue — POSIX accept(2). Resolves to the NEW connection's fd, or a negative error code; the listening socket stays open.

addr/addrlen receive the peer's address, with addrlen in/out as in getsockname (set it to the buffer capacity first). This is one of the four genuinely asynchronous operations: io_uring on Linux, kqueue on macOS, and AcceptEx on Windows — which is an AF_INET/AF_INET6 extension, so a Unix-domain listener there takes a different path.

Parameters

NameTypeNotes
sockfdi32
addr*u8
addrlen*u32

Returns: IoFuture

connect function
fn(sockfd : i32, addr : *u8, addrlen : u32) -> IoFuture

Connect to a remote address — POSIX connect(2). Resolves to 0, or a negative error code (-ECONNREFUSED, -ETIMEDOUT).

Asynchronous on Linux (io_uring) and macOS (kqueue), but on Windows it is still a BLOCKING connect() on the event-loop thread, so a slow or unreachable peer stalls every other task in the program there. There is also no timeout parameter: the kernel's is the only one, and std/async's timeout is the way to bound it.

Parameters

NameTypeNotes
sockfdi32
addr*u8
addrlenu32

Returns: IoFuture

send function
fn(sockfd : i32, buf : *u8, len : usize, flags : i32) -> IoFuture

Send from buf on a connected socket — POSIX send(2). Resolves to the number of bytes ACCEPTED, which may be fewer than len (loop, or use std/io's write_all through std/net), or a negative error code.

flags is passed straight to the syscall. Note that nothing in the generated runtime ignores SIGPIPE, so on POSIX a send to a peer that has closed will KILL the process unless you either pass MSG_NOSIGNAL (Linux), set SO_NOSIGPIPE (macOS) or install a handler with std/sys/signal.

Parameters

NameTypeNotesDescription
sockfdi32
buf*u8

The sockaddr bytes. Read them with get_family first, then with the get_*_in or get_*_in6 accessors for that family.

lenusize

Size of buf in bytes — the addrlen every socket call wants.

flagsi32

Returns: IoFuture

recv function
fn(sockfd : i32, buf : *u8, len : usize, flags : i32) -> IoFuture

Receive into buf on a connected socket — POSIX recv(2). Resolves to the number of bytes read, or a negative error code. 0 means the peer performed an orderly shutdown, not "nothing available yet" — this runtime never resolves a recv with EAGAIN, it suspends instead. flags is normally 0.

Parameters

NameTypeNotesDescription
sockfdi32
buf*u8

The sockaddr bytes. Read them with get_family first, then with the get_*_in or get_*_in6 accessors for that family.

lenusize

Size of buf in bytes — the addrlen every socket call wants.

flagsi32

Returns: IoFuture

shutdown function
fn(sockfd : i32, how : i32) -> IoFuture

Shut down one or both directions of a connection — POSIX shutdown(2). Resolves to 0, or a negative error code. how is SHUT_RD (0), SHUT_WR (1) or SHUT_RDWR (2).

SHUT_WR is the one worth knowing: it sends FIN so the peer sees end-of-stream, while still letting you read their reply — which close cannot do. Unlike close it affects the CONNECTION, not the descriptor, so a duped fd is shut down too.

Parameters

NameTypeNotes
sockfdi32
howi32

Returns: IoFuture

close function
fn(fd : i32) -> IoFuture

Close the descriptor — POSIX close(2). Resolves to 0, or a negative error code. Do not retry on -EINTR: the descriptor is gone either way, and retrying can close an unrelated fd that has since taken the number.

Parameters

NameTypeNotes
fdi32

Returns: IoFuture

setsockopt function
fn(sockfd : i32, level : i32, optname : i32, optval : *u8, optlen : u32) -> IoFuture

Set a socket option from raw bytes — POSIX setsockopt(2), wrapped as a future for use inside an async block (std/sys/sockinfo has the synchronous form of the same syscall). Resolves to 0, or a negative error code. level and optname come from std/sys/socket; optval/optlen must match what the option expects, and nothing here checks that.

Parameters

NameTypeNotes
sockfdi32
leveli32
optnamei32
optval*u8
optlenu32

Returns: IoFuture

getsockopt function
fn(sockfd : i32, level : i32, optname : i32, optval : *u8, optlen : *u32) -> IoFuture

Read a socket option into raw bytes — POSIX getsockopt(2), the future-returning form. Resolves to 0, or a negative error code. optlen is in/out: set it to the buffer capacity and read back the size written.

Parameters

NameTypeNotes
sockfdi32
leveli32
optnamei32
optval*u8
optlen*u32

Returns: IoFuture

make_sockaddr_in function
fn(ip : *u8, port : u16) -> SockAddr

Build a sockaddr_in from a dotted-quad IP string and a HOST-order port (the helper does the htons). ip must be a NUL-terminated C string like "127.0.0.1". The caller must free_sockaddr the result.

A malformed ip is NOT reported: the underlying inet_pton failure is discarded and the address field stays zero, which binds to INADDR_ANY — so validate the string first, or use std/net's IpAddr.parse, which returns a Result.

Parameters

NameTypeNotes
ip*u8
portu16

Returns: SockAddr

fn(ip : *u8, port : u16) -> SockAddr

Build a sockaddr_in6 from an IPv6 string (e.g. "::1") and a host-order port. Caller frees with free_sockaddr. Same silent-inet_pton caveat as make_sockaddr_in, and note that sin6_scope_id is left zero, so a link-local address (fe80::…%en0) cannot be expressed here.

Parameters

NameTypeNotes
ip*u8
portu16

Returns: SockAddr

fn(port : u16) -> SockAddr

Build a sockaddr_in for INADDR_ANY (0.0.0.0) on port — bind to every IPv4 interface. This is the wildcard a server wants; note it does NOT also cover IPv6, which needs a separate AF_INET6 socket (or a dual-stack one via IPV6_V6ONLY). Caller frees with free_sockaddr.

Parameters

NameTypeNotes
portu16

Returns: SockAddr

fn(port : u16) -> SockAddr

Build a sockaddr_in for 127.0.0.1 on port — reachable only from this machine, which is what a test or a local-only service wants. Caller frees with free_sockaddr.

Parameters

NameTypeNotes
portu16

Returns: SockAddr

free_sockaddr function
fn(addr : SockAddr) -> unit

Release a SockAddr's buffer. Required after every make_sockaddr_*, and safe to call only once — there is no Dispose doing it for you.

Parameters

NameTypeNotes
addrSockAddr

Returns: unit

get_port_in function
fn(addr : *u8) -> u16

Read the port out of a sockaddr_in buffer, in HOST byte order (the accessor does the ntohs). Use it on the buffer accept or getsockname filled.

Parameters

NameTypeNotes
addr*u8

Returns: u16

get_addr_in function
fn(addr : *u8) -> u32

Read the IPv4 address out of a sockaddr_in buffer as a u32 in NETWORK byte order — unlike get_port_in, this one is NOT converted. Pass it through ntohl before comparing against a host-order literal.

Parameters

NameTypeNotes
addr*u8

Returns: u32

get_family function
fn(addr : *u8) -> u16

Read the address family (AF_INET, AF_INET6, AF_UNIX) out of a sockaddr buffer. This is the first thing to read from an address the kernel filled, since it decides which of the accessors above is valid.

Parameters

NameTypeNotes
addr*u8

Returns: u16

get_port_in6 function
fn(addr : *u8) -> u16

Read the port out of a sockaddr_in6 buffer, in host byte order.

Parameters

NameTypeNotes
addr*u8

Returns: u16

get_addr_in6 function
fn(addr : *u8, out : *u8) -> unit

Copy the 16 bytes of an IPv6 address out of a sockaddr_in6 buffer into out, which must have room for 16 bytes. They stay in NETWORK byte order, which for IPv6 is simply the canonical big-endian order the address is written in.

Parameters

NameTypeNotes
addr*u8
out*u8

Returns: unit

htons function
fn(hostshort : u16) -> u16

Host to network byte order, 16-bit — htons(3). Ports on the wire are big-endian; on a big-endian host this is the identity.

Parameters

NameTypeNotes
hostshortu16

Returns: u16

ntohs function
fn(netshort : u16) -> u16

Network to host byte order, 16-bit — ntohs(3). The inverse of htons, and the same operation on every real platform.

Parameters

NameTypeNotes
netshortu16

Returns: u16

htonl function
fn(hostlong : u32) -> u32

Host to network byte order, 32-bit — htonl(3). What turns 0x7F000001 into the bytes 127.0.0.1.

Parameters

NameTypeNotes
hostlongu32

Returns: u32

ntohl function
fn(netlong : u32) -> u32

Network to host byte order, 32-bit — ntohl(3). Apply it to get_addr_in's result before comparing against a host-order literal.

Parameters

NameTypeNotes
netlongu32

Returns: u32