The port! model
A port! is an open handle — a file, a socket, a TLS
session, an HTTP connection. All ports share one runtime shape and one ownership
discipline: borrow to read or write, consume to close.
Runtime ports carry a scheme (which outer layer: file,
tcp, tls, http, https,
dns), a layer-specific handle, an optional
inner port for layered schemes, and a close_policy
controlling whether closing the outer layer cascades to the inner one.
File ports
Open a file with file/open (wrapped in make port!).
Borrow the port with a get-word for reads and writes; close consumes it.
fp: make port! file/open %data.txt
either none? fp [print 0] [print 1]
file/read :fp ; borrow
file/write :fp "text" ; borrow
file/close fp ; consume
Pair the open with a defer so the handle is always released:
fp: make port! file/open %data.txt
defer [file/close fp]
Blocking vs non-blocking reads
Two read styles serve two needs:
read/intois async: on an idle socket it returns a would-block result rather than waiting. Use it in a reactor loop.read/blockingwaits until bytes arrive or EOF. Use it for a synchronous, line-interactive driver such as a session loop.
Networking layers
The runtime composes networking as layered ports. A TLS, HTTP, or HTTPS port wraps an inner transport and takes ownership of it:
tcp-port: make port! tcp/connect-host host 80
http-port: make port! http/open-wrap tcp-port
; tcp-port is moved into http-port — do not use it again
Construction flows from the bottom up: tcp/connect-host →
tls/wrap (or the backend-owned tls/connect-host) →
http/open-wrap / https/open-wrap. Each wrapper consumes
the port below it.
Cascade close
Close only the topmost layer; the rest cascade when
close_policy is PORT_CLOSE_INNER:
- closing
httpscloses the wrappedtls(and its transport) - closing
httpcloses the wrappedtcp - closing
tlscloses the wrappedtcponly for fd-wrap backends
Servers: listen, accept & the reactor
Open a listening TCP port on an empty host, accept connections, and let the
runtime reactor block on readiness. A server installs an awake
handler and is driven with serve:
; listen on all interfaces, port 8080
server: make port! open tcp://:8080
; accept a ready connection
client: make port! accept :server
The reactor (__recoil_reactor_wait, a select-based
wait) wakes ports when they are readable. A transport-agnostic session driver,
serve-session :port :vm, runs a line-interactive loop over the
blocking read primitive — the same driver works for console://,
tcp, and serial:// ports.
awake handler passed as :fn links only if it is
an exported module function — a non-exported function emits
an unmangled name and fails to link.
Port error taxonomy
Port failures set a runtime error of type ERR_TYPE_PORT (7) with
a numeric code. Layered wrappers preserve one level of cause-chain metadata
(runtime/last-error-code and
runtime/last-error-cause-code).
| Code | Constant | Meaning |
|---|---|---|
| 800 | ERR_PORT_INVALID | invalid port |
| 801 | ERR_PORT_CLOSED | operation on a closed port |
| 802 | ERR_PORT_TIMEOUT | timed out |
| 803 | ERR_PORT_WOULD_BLOCK | no data ready (async) |
| 804 | ERR_PORT_IO | I/O failure |
| 805 | ERR_PORT_PROTOCOL | protocol error |
| 806 | ERR_PORT_UNSUPPORTED | unknown scheme / unsupported dispatch / TLS fd-wrap unavailable |
| 810–815 | ERR_PORT_TCP_* | resolve, connect, bind, listen, accept, reset |
| 820–824 | ERR_PORT_TLS_* | handshake, verify, cert, protocol, alert |
| 830–833 | ERR_PORT_HTTP_* | status, parse, redirect, protocol |
| 840–841 | ERR_PORT_DNS_* | resolve failure, invalid endpoint host |
In the active runtime path, TCP resolve/connect, TLS handshake, and the DNS codes are emitted today; the remaining codes are reserved constants for higher-level diagnostics.
Worked example: an HTTP server
The bundled HTTP example (examples/net/) is a small but complete
server built from the language's own building blocks — the request scanner is an
FST, the protocol state is an FSM, and parsing is
zero-copy.
The parser accepts GET / HTTP/1.0 and HTTP/1.1
requests (with repeated headers and a small Content-Length body),
and rejects malformed request lines, missing \r\n\r\n terminators,
HTTP/1.1 without Host, and non-integer
Content-Length. Routing maps GET / and
GET /health to 200, unknown paths/methods to
404, and malformed requests to 400.
parsed: make parse-request-result! parse-request :raw
status: make i32! match parsed [
ok [request-line headers host] [200]
error [status-code] [status-code]
]
Request line parts, the header block, and extracted header values are all
slice! [char!] views into the original buffer (no copies). See the
repository's
HTTP server doc
and
port layering notes
for the full surface.