Clause
← All documentation

Ports

Programs get ports as OTP defines them, and external I/O goes through them. The port table, exit rules and port tasks are in runtime/src/scheduler/ports.cpp, the builtins in runtime/src/builtins/erlang/ports.cpp, the drivers and the I/O thread in runtime/src/ports/. OTP goldens: executables_port_identities, executables_port_input, executables_port_spawn, executables_file_io, executables_sockets, executables_many_ports, executables_port_fairness, executables_busy_ports, executables_slow_owner; runtime test runtime_port_io.

Identity

Port table and ownership

Builtins and port messages

Errors follow OTP: badarg for a closed or invalid port and bad arguments, the POSIX reason (enoent, eacces) as an error for an open that fails.

Messages from a port go to its connected process: {Port, {data, Data}}, {Port, eof} (option eof), {Port, {exit_status, Status}} (option exit_status), {Port, closed} (after {Pid, close}), {Port, connected} (to the old owner after a connect).

Data modes and options

OptionEffect
stream (default), {packet, N} (N = 1, 2, 4)Bytes as they arrive, or messages framed by an N-byte big-endian length that output also gets
{line, L}{eol, Line} per line, {noeol, Part} for parts longer than L or an unterminated end
binaryData as binaries instead of byte lists
eof{Port, eof} at end of input; the port stays open until closed
exit_status{Port, {exit_status, S}} when the program exits (spawn ports)
use_stdio (default), nouse_stdio, stderr_to_stdout, in, out, hideAs in OTP (hide has no effect)
{args, List}, {arg0, A}, {env, Env}, {cd, Dir}Spawn ports (subprocesses)
{busy_limits_port, {Low, High} | disabled}Queued output bytes that make the port busy (busy ports)

Without eof, end of input closes the port with reason normal, after the exit_status message when one was asked for. Unknown options are badarg.

Drivers

A driver is a C++ object behind one port (runtime/src/ports/): it opens the resource, accepts output (port_command), answers port_control/3 when it supports control, reports input and errors as events and closes. Drivers:

The internal drivers of file, the sockets and the registry are opened with {spawn_driver, Name} under Clause names, and their port_control/3 operations are a Clause protocol: programs use the library modules, not OTP's prim_inet/efile protocols.

I/O thread

One I/O thread per runtime (detail::Reactor, runtime/src/ports/reactor.hpp) runs a Boost.Asio io_context: an I/O completion port on Windows, epoll on Linux, kqueue on macOS. The first port that needs it starts it; it serves every port kind, so a port costs no thread of its own:

runtime/src/ports/io*.cpp hold the port I/O (detail::IoService): its methods only post work to the I/O thread, where all I/O state lives. The I/O thread hands input over as it was read: raw bytes, the end of input, a read error, a program's exit status. Socket events (messages, accepted connections) are handed over the same way.

The prototype tests/prototypes/poller/ (run.py --wsl) shows the wakeup on Windows (completion port) and WSL Linux (poll()): an idle scheduler thread wakes 9–91 µs after input, and shutdown stops the I/O thread without input.

At program end every port is closed (child programs see end of input; they are not killed, as in OTP) and the I/O thread stops: it is joined outside the executor mutex, because a delivery in progress takes it; a Windows fd reader blocked in a read that cannot be cancelled is detached and delivers nothing more.

Port tasks

A port is scheduled like a process: what the I/O thread hands over waits in the port, and the port waits in the executor's port queue until a scheduler worker runs its task.

Busy ports

Output a driver queues for the I/O thread (spawned programs' pipes) counts against the port's busy limits, OTP's busy_limits_port (defaults: high 8,192 bytes, low 4,096; {busy_limits_port, {Low, High}} or disabled as an open_port/2 option, limits at least 1, a low limit above the high one lowered to it):

A port also bounds the input it holds:

Subprocesses

open_port({spawn, Command}, Options) and open_port({spawn_executable, File}, Options) start a program whose stdin and stdout are pipes of the port:

Standard I/O and files

Sockets

A socket is a port, as with OTP's inet_drv backend: is_port(Socket) is true and active-mode messages are {tcp, Socket, Data}, {tcp_closed, Socket}, {tcp_error, Socket, Reason}, {udp, Socket, Address, Port, Data}. The subset: gen_tcp:connect/2,3,4 (a host and port, or a socket address map), listen/2, accept/1,2, send/2, recv/2,3, close/1, controlling_process/2, shutdown/2; gen_udp:open/1,2, connect/2,3, send/2,3,4,5 (OTP's destination forms and ancillary data), recv/2,3, close/1; inet:setopts/2, getopts/2, getstat/1,2, info/1, i/0,1,2, monitor/1, cancel_monitor/1, port/1, peername/1, peernames/1,2, sockname/1, socknames/1,2; modes {active, true | false | once}, binary/list ({mode, M}), {packet, 0 | 1 | 2 | 4 | raw}, {backlog, N}, {ip, Address}/{ifaddr, Address}, inet/inet6; IPv4 and IPv6 addresses as tuples, host names as strings or atoms (loopback included).

Options beyond the data mode (ports/socket_options.cpp, library/kernel/clause_socket_options.erl) are driver records of a code and a value, given when a socket opens (set before it binds or connects) or by setopts/2; every option of a setopts/2 call is checked before any is set. The system's options (reuseaddr, keepalive, dontroute, linger, broadcast, sndbuf, recbuf, nodelay, tos, tclass, ipv6_v6only, multicast_ttl, multicast_loop, multicast_if, add_membership, drop_membership, exclusiveaddruse on Windows, priority and reuseport where the system has them) go to setsockopt and are read back with getsockopt; the options OTP's inet_drv keeps itself (buffer, header, deliver, exit_on_close, send_timeout, watermarks, packet_size, ...) are kept with inet_drv's defaults and not applied (differences). As inet_drv, a receive buffer raises buffer until that is set, and getopts/2 leaves out the options a socket type lacks (broadcast of a TCP socket, nodelay of a UDP one, tclass of IPv4) and refuses unknown ones with {error, einval}. An accepted connection takes over its listening socket's options. send/5 with ancillary data sets tos, tclass or ttl for that datagram alone; on Windows it is {error, einval}, as OTP.

Each socket counts its traffic as inet_drv does for getstat/1,2: packets received are reads (TCP) or datagrams, packets sent are send calls (with their packet header) or datagrams, with inet_drv's running averages; send_pend is the output queued and not written. getstatus gives inet_drv's status flags (open, connected, connecting, listen, accepting), which info/1 and i/0,1,2 print with OTP's names and module column (inet_tcp, inet6_udp, ...). Queries of a closed socket answer {error, einval}, as OTP.

The socket module (library/kernel/socket.erl) uses the same driver ports in passive binary mode. A stream socket can be bound before it listens or connects (operation 22 opens and binds the system socket, which listen turns into the acceptor or connect connects); a datagram socket is bound by udp_open, implicitly at its first send. A connection's end answers every waiting recv (also one asking for more bytes than remain). socket keeps the bytes a recv did not take in its own buffer, so recv/2,3,4 gives OTP's {error, {timeout, Data}} and {error, {closed, Data}}.

Implementation (runtime/src/ports/sockets.cpp):

The registry

win32reg opens {spawn_driver, "clause_registry"} on Windows (runtime/src/ports/registry_windows.cpp). The driver keeps OTP's registry driver's state (the access asked for, the current key and its root, initially HKEY_CLASSES_ROOT) and answers OTP's nine commands as synchronous port_control/3 operations with the replies OTP's driver sends as port data: o, e and the errno name (registry damage is eio, a deleted key einval, other Windows errors as the C runtime maps them), k and a key name, v, the type, name and data (strings without their trailing zero, DWORDs big-endian), s and the current key. Listings return every reply, each preceded by its 32-bit size. Names and strings use the ANSI code page, as OTP's driver.

Not provided

Linked-in drivers and NIFs, erlang:open_port({spawn_driver, Name}) for OTP driver names, distribution ports, port_call/3 on the provided drivers other than the library's own protocol, busy socket ports, and the overlapped_io, parallelism and busy_limits_msgq options.