module sockets

module lib/sockets.mu

import "sockets"

sockets provides minimal socket helpers built directly on raw syscalls.

Imports

Functions

accept#

fn accept(listener)

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#

fn close(endpoint)

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#

fn dial(addr)

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#

fn listen(addr)

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#

fn read(conn, size)

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#

fn recvfrom(sock)

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#

fn sendto(sock, data, addr)

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#

fn with_conn(conn, body)

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#

fn write(conn, data)

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
}