module sockets
module lib/sockets.mu
import "sockets"
sockets provides minimal socket helpers built directly on raw syscalls.
Imports
Functions
accept#
accept waits for a TCP connection on the listener, yields control via blocking syscalls, and returns a conn map describing the accepted socket.
Source lib/sockets.mu:88
fn accept(listener) {
err := helpers._require_listener(listener)
if is_error(err) {
return err
}
fd := listener["fd"]
addr_buf := buf(28)
len_buf := buf(4)
helpers._set_socklen(len_buf, 28)
conn_fd := syscall("accept", fd, addr_buf, len_buf)
if is_error(conn_fd) {
return conn_fd
}
if listener["scheme"] == "unix" {
// A dialing unix peer is unnamed; there is no address to decode.
return {"kind": "conn", "fd": conn_fd, "scheme": "unix", "family": listener["family"], "local": listener["local"], "remote": "unix://"}
}
remote := helpers._decode_sockaddr(addr_buf, len_buf, listener["scheme"])
if is_error(remote) {
syscall("close", conn_fd)
return remote
}
return {"kind": "conn", "fd": conn_fd, "scheme": listener["scheme"], "family": remote["family"], "local": listener["local"], "remote": remote["text"]}
}close#
close shuts down the socket handle via syscall("close") and is safe to call multiple times.
Source lib/sockets.mu:374
fn close(endpoint) {
if !typing.is_map(endpoint) {
return error("sockets.close: expects socket map")
}
kind := endpoint["kind"]
if kind != "listener" && kind != "conn" && kind != "sock" {
return error("sockets.close: expects listener/conn/sock")
}
if endpoint["closed"] == true {
return nil
}
fd := endpoint["fd"]
if !typing.is_int(fd) {
return error("sockets.close: socket fd must be integer")
}
result := syscall("close", fd)
if is_error(result) {
return result
}
endpoint["closed"] = true
return nil
}dial#
dial connects to the remote tcp://, udp:// or unix:// address and returns a connected socket map. UDP sockets also cache the parsed remote address in remote_parsed. A handshake that has to wait parks only the calling task (Specification §4.4), so a server may dial a backend while it keeps serving.
Source lib/sockets.mu:125
fn dial(addr) {
target := helpers._parse_socket_address(addr, true)
if is_error(target) {
return target
}
scheme := target["scheme"]
family := target["family"]
sock_type := helpers._socket_type_for_scheme(scheme)
proto := helpers._proto_for_scheme(scheme)
fd := syscall("socket", family, sock_type, proto)
if is_error(fd) {
return fd
}
sockaddr := helpers._build_sockaddr(target)
connect_result := syscall("connect", fd, sockaddr["addr"], sockaddr["length"])
if is_error(connect_result) {
syscall("close", fd)
return connect_result
}
local := helpers._get_local_address(fd, scheme)
if is_error(local) {
syscall("close", fd)
return local
}
remote_text := target["text"]
kind := "conn"
if scheme == "udp" {
kind = "sock"
}
sock := {"kind": kind, "fd": fd, "scheme": scheme, "family": family, "local": local, "remote": remote_text}
if scheme == "udp" {
sock["connected"] = true
sock["remote_parsed"] = {"scheme": scheme, "family": family, "ip": helpers._clone_buffer(target["ip"]), "port": target["port"], "is_ipv6": target["is_ipv6"]}
}
return sock
}listen#
listen binds the provided tcp://, udp:// or unix:// address via the syscall builtin and returns a handle map. Stream listeners (tcp, unix) expose kind: "listener" while UDP sockets expose kind: "sock" with connected: false. A unix path must not already exist — remove a stale socket file before binding. Errors are returned directly.
Source lib/sockets.mu:13
fn listen(addr) {
target := helpers._parse_socket_address(addr, true)
if is_error(target) {
return target
}
scheme := target["scheme"]
family := target["family"]
sock_type := helpers._socket_type_for_scheme(scheme)
proto := helpers._proto_for_scheme(scheme)
fd := syscall("socket", family, sock_type, proto)
if is_error(fd) {
return fd
}
if scheme == "tcp" {
// SO_REUSEADDR before bind, or a restarted server eats EADDRINUSE for
// the whole TIME_WAIT window of its predecessor's connections. The
// constants differ per OS: SOL_SOCKET is 1 on linux and 0xffff on
// darwin, SO_REUSEADDR is 2 and 4. A failure here is ignored on
// purpose -- the option is an amenity, and bind is the call whose
// error means something.
level := 1
option := 2
if platform() == "darwin/amd64" || platform() == "darwin/arm64" {
level = 65535
option = 4
}
one := buf(4)
one[0] = 1
syscall("setsockopt", fd, level, option, one, 4)
}
sockaddr := helpers._build_sockaddr(target)
bind_result := syscall("bind", fd, sockaddr["addr"], sockaddr["length"])
if is_error(bind_result) {
syscall("close", fd)
return bind_result
}
if scheme != "udp" {
listen_result := syscall("listen", fd, helpers._DEFAULT_BACKLOG)
if is_error(listen_result) {
syscall("close", fd)
return listen_result
}
}
local := helpers._get_local_address(fd, scheme)
if is_error(local) {
syscall("close", fd)
return local
}
if scheme == "unix" {
return {"kind": "listener", "fd": fd, "scheme": scheme, "family": family, "local": target["text"]}
}
if scheme == "tcp" {
return {"kind": "listener", "fd": fd, "scheme": scheme, "family": family, "local": local}
}
return {"kind": "sock", "fd": fd, "scheme": scheme, "family": family, "local": local, "connected": false}
}read#
read consumes up to size bytes from a TCP connection and returns the payload string. Pass nil to read into the default buffer size. Returns nil when the peer closes.
Source lib/sockets.mu:273
fn read(conn, size) {
err := helpers._require_conn(conn)
if is_error(err) {
return err
}
read_size := 4096
if size != nil {
if !typing.is_int(size) {
return error("sockets.read expects integer size, got " + type(size))
}
if size <= 0 {
return error("sockets.read size must be positive")
}
read_size = size
}
buffer := buf(read_size)
fd := conn["fd"]
if !typing.is_int(fd) {
return error("sockets.read: conn fd must be integer")
}
result := syscall("read", fd, buffer, read_size)
if type(result) == "ERROR" {
return result
}
if result == 0 {
return nil
}
return helpers._buffer_to_string(buffer, result)
}recvfrom#
recvfrom reads a single UDP datagram from the socket and returns [data, addr]. The returned address is the canonical udp:// text representation of the sender.
Source lib/sockets.mu:177
fn recvfrom(sock) {
err := helpers._require_sock(sock)
if is_error(err) {
return err
}
data_buf := buf(65536)
addr_buf := buf(28)
len_buf := buf(4)
addr_len := helpers._sockaddr_length(sock["family"])
helpers._set_socklen(len_buf, addr_len)
fd := sock["fd"]
if !typing.is_int(fd) {
return error("sockets.recvfrom: sock fd must be integer")
}
recv_result := syscall("recvfrom", fd, data_buf, len(data_buf), 0, addr_buf, len_buf)
if is_error(recv_result) {
return recv_result
}
length := recv_result
data := helpers._buffer_to_string(data_buf, length)
remote := helpers._decode_sockaddr(addr_buf, len_buf, sock["scheme"])
if is_error(remote) {
return remote
}
return [data, remote["text"]]
}sendto#
sendto transmits a UDP datagram to addr (or the connected peer when addr is nil) and returns the number of bytes written.
Source lib/sockets.mu:215
fn sendto(sock, data, addr) {
err := helpers._require_sock(sock)
if is_error(err) {
return err
}
if !typing.is_str(data) {
return error("sockets.sendto: expects string data")
}
target := nil
if addr == nil {
if !sock["connected"] {
return error("sockets.sendto: requires an address when socket is not connected")
}
target = sock["remote_parsed"]
if target == nil {
return error("sockets.sendto: missing remote address data")
}
} else {
parsed := helpers._parse_socket_address(addr, true)
if is_error(parsed) {
return parsed
}
if parsed["scheme"] != "udp" {
return error("sockets.sendto: can only target udp:// addresses")
}
target = parsed
}
sockaddr := helpers._build_sockaddr(target)
fd := sock["fd"]
if !typing.is_int(fd) {
return error("sockets.sendto: sock fd must be integer")
}
addr_ptr := sockaddr["addr"]
addr_len := sockaddr["length"]
sent := syscall("sendto", fd, data, len(data), 0, addr_ptr, addr_len)
if is_error(sent) {
return sent
}
return sent
}with_conn#
with_conn runs body(conn) and closes the socket when it returns, whatever the outcome — the defer-shaped resource guard. body's result (a value or an error) passes through unchanged; close errors are dropped, matching a deferred close. An error conn also passes straight through, so accept/dial results can be handed over unchecked.
Source lib/sockets.mu:362
fn with_conn(conn, body) {
if is_error(conn) {
return conn
}
result := body(conn)
close(conn)
return result
}write#
write flushes data to a TCP connection, retrying until every byte is delivered.
Source lib/sockets.mu:316
fn write(conn, data) {
err := helpers._require_conn(conn)
if is_error(err) {
return err
}
if !typing.is_str(data) {
return error("sockets.write: expects string data")
}
total := 0
length := len(data)
offset := 0
while offset < length {
chunk := strings.substring(data, offset, length)
fd := conn["fd"]
if !typing.is_int(fd) {
return error("sockets.write: conn fd must be integer")
}
result := syscall("write", fd, chunk, len(chunk))
if type(result) == "ERROR" {
return result
}
if result == 0 {
return error("sockets.write: returned zero bytes")
}
offset = offset + result
total = total + result
}
return total
}