Clause
← All documentation

Ports

Decision of plan 11 step 57A (2026-10-08). It replaces the step-53 decision (no ports, processes): programs get ports as OTP defines them, and external I/O goes through them. Steps 57B–57F implement it; each section names its step. This contract settles the representation, the driver model and the I/O thread before any source can open a port.

Implemented: identities, the port table, the port builtins and messages, links, monitors, names and exit signals of ports, and output-only fd ports (step 57B, runtime/src/scheduler/ports.cpp, runtime/src/builtins/ports.cpp, runtime/src/ports/; OTP golden executables_port_identities); the I/O thread and fd port input with stream, packet and line framing (step 57C, runtime/src/ports/io*.cpp; OTP golden executables_port_input); subprocess ports, os:type/0, os:getenv/1 and os:cmd/1 (step 57D, runtime/src/ports/spawn*.cpp, library/stdlib/os.erl; OTP golden executables_port_spawn); the file driver, the library file subset and standard input through io:get_line/io:get_chars (step 57E, runtime/src/ports/file.cpp, library/stdlib/{file,io}.erl; OTP golden executables_file_io); sockets (step 57F, OTP golden executables_sockets); one event-driven I/O thread for every port kind (step 57G1, runtime/src/ports/reactor.cpp; OTP golden executables_many_ports, runtime test runtime_port_io); port tasks on the scheduler workers (step 57G2, runtime/src/scheduler/ports.cpp; OTP golden executables_port_fairness); busy ports and bounded input (step 57G3; OTP goldens executables_busy_ports, executables_slow_owner).

Identity

Port table and ownership

Builtins and port messages

Builtin or messageStep
is_port/1 true for ports, port_to_list/1, list_to_port/1, ports/057B
port_info/1,2 (name, links, id, connected, input, output, os_pid, monitors, monitored_by, registered_name)57B
port_close/1, port_connect/2, port_command/2,357B
Port ! {Pid, {command, Data}}, {Pid, close}, {Pid, {connect, New}}57B
link/1, unlink/1, monitor(port, P), exit/2 on ports; register/2 of a port57B
port_control/3, port_call/357B (badarg for drivers without control); used by the 57E/57F drivers
open_port({fd, In, Out}, Opts)57B output; input from 57C
open_port({spawn, Command} | {spawn_executable, File}, Opts)57D
Internal drivers of the project library (file, sockets)57E, 57F

The builtins replace the step-53 [ports] notimpl diagnostics; feature ports becomes implemented in 57B. 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 (57D)
{busy_limits_port, {Low, High} | disabled}Queued output bytes that make the port busy (busy ports, 57G3)

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 and the sockets 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, step 57G1) 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.

Port tasks

Step 57G2. 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.

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.

Busy ports

Step 57G3. 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

Step 57D. 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

Step 57E.

Sockets (57F)

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/3,4, listen/2, accept/1,2, send/2, recv/2,3, close/1, controlling_process/2, shutdown/2; gen_udp:open/1,2, send/4, recv/2,3, close/1; inet:setopts/2, inet:port/1, inet:peername/1, inet:sockname/1; modes {active, true | false | once}, binary/list, {packet, 0 | 1 | 2 | 4 | raw}, {reuseaddr, Bool}, {backlog, N}, {ip, Address}/{ifaddr, Address}, inet/inet6; IPv4 and IPv6 addresses as tuples, host names as strings or atoms (loopback included). The tuning options nodelay, keepalive, send_timeout, send_timeout_close, delay_send and exit_on_close are accepted and not applied; any other option is exit(badarg), as for an invalid one in OTP.

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

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.