Module sys/tcp
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
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
| Name | Type | Description |
|---|---|---|
buf | *(u8) | The |
len | u32 | Size of |
Functions
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
| Name | Type | Notes |
|---|---|---|
domain | i32 | |
sock_type | i32 | |
protocol | i32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
addr | *u8 | |
addrlen | u32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
backlog | i32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
addr | *u8 | |
addrlen | *u32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
addr | *u8 | |
addrlen | u32 |
Returns: 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
| Name | Type | Notes | Description |
|---|---|---|---|
sockfd | i32 | ||
buf | *u8 | The | |
len | usize | Size of | |
flags | i32 |
Returns: 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
| Name | Type | Notes | Description |
|---|---|---|---|
sockfd | i32 | ||
buf | *u8 | The | |
len | usize | Size of | |
flags | i32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
how | i32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
fd | i32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
level | i32 | |
optname | i32 | |
optval | *u8 | |
optlen | u32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
sockfd | i32 | |
level | i32 | |
optname | i32 | |
optval | *u8 | |
optlen | *u32 |
Returns: IoFuture
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
| Name | Type | Notes |
|---|---|---|
ip | *u8 | |
port | u16 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
ip | *u8 | |
port | u16 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
port | u16 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
port | u16 |
Returns: SockAddr
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
| Name | Type | Notes |
|---|---|---|
addr | SockAddr |
Returns: unit
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
| Name | Type | Notes |
|---|---|---|
addr | *u8 |
Returns: u16
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
| Name | Type | Notes |
|---|---|---|
addr | *u8 |
Returns: u32
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
| Name | Type | Notes |
|---|---|---|
addr | *u8 |
Returns: u16
Read the port out of a sockaddr_in6 buffer, in host byte order.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | *u8 |
Returns: u16
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
| Name | Type | Notes |
|---|---|---|
addr | *u8 | |
out | *u8 |
Returns: unit
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
| Name | Type | Notes |
|---|---|---|
hostshort | u16 |
Returns: u16
Network to host byte order, 16-bit — ntohs(3). The inverse of htons,
and the same operation on every real platform.
Parameters
| Name | Type | Notes |
|---|---|---|
netshort | u16 |
Returns: u16
Host to network byte order, 32-bit — htonl(3). What turns
0x7F000001 into the bytes 127.0.0.1.
Parameters
| Name | Type | Notes |
|---|---|---|
hostlong | u32 |
Returns: 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
| Name | Type | Notes |
|---|---|---|
netlong | u32 |
Returns: u32