Native socket listeners
Scoped nonblocking TCP and pathname-Unix listeners with owned accepted connections.
silk.native_socket creates one affine listening descriptor and admits one independently owned
connection at a time. The actor is ordinary selected Silk source: it is available on aarch64 Darwin
with Apple system libc and on aarch64 or x86-64 GNU/Linux with glibc. It is absent on Windows, musl,
no-libc profiles, and WebAssembly.
Use Listener.boundAddress after listen to observe the actual numeric endpoint. This matters when
the requested port is zero: the kernel-assigned nonzero port is published only after bind,
listen, and getsockname all succeed.
import silk.native_socket {Accepted, AcceptedContext, AcceptedView, BoundAddress, ListenOptions, Listener, NativeSocketError, listen, withAcceptedContext}
import silk.network_address {Endpoint, IpAddress, Ipv4Address, Port}
fn loopbackListener() -> Endpoint {
return Endpoint.make(
IpAddress.V4 {value: Ipv4Address.fromOctets([127, 0, 0, 1])},
Port.fromU16(0),
)
}
effect fn openLoopback() -> Listener ! NativeSocketError {
let listener = run listen(loopbackListener(), ListenOptions.defaults())
let bound = Listener.boundAddress(&listener)
match move bound {
BoundAddress.Tcp {endpoint} => {
if Port.value(&Endpoint.port(&endpoint)) == 0 { fail NativeSocketError.InvalidNativeAddress {family: 2, length: 16} }
}
_ => {}
}
return move listener
}
struct RequestContext {requestId: i32}
impl RequestContext {
effect<'call> fn use<'call>(
context: Self,
view: &'call mut AcceptedView<'call>,
) -> i32 {
drop view
return context.requestId
}
}
impl AcceptedContext<i32, never, never ? never> for RequestContext {
use: RequestContext.use
}
effect fn useAccepted(accepted: Accepted, requestId: i32) -> i32 {
return run withAcceptedContext(move accepted, RequestContext {requestId: requestId})
}Options
ListenOptions.make(kernelBacklog, reuseAddress, pollIntervalNanoseconds) is pure. Backlog must be
1 through 65535 and the polling interval must be 1 through 1,000,000,000 nanoseconds. defaults()
selects backlog 128, disables reuse, and polls at one-millisecond intervals. Reuse controls only
SO_REUSEADDR; the actor never enables SO_REUSEPORT or creates an application queue.
IPv6 listeners are explicitly IPv6-only and own one descriptor. IPv4-mapped IPv6 values therefore remain IPv6 identities rather than being silently converted to IPv4.
Serial acceptance and ownership
accept(&mut listener, deadline) holds an exclusive listener borrow and returns one affine
Accepted. A second accept or close cannot overlap that borrow. withAccepted consumes the result,
lends &mut Connection plus &PeerAddress, and closes the connection after success, typed failure,
or structured cancellation. The connection remains usable if its listener is closed first, and
closing it never closes the listener.
withListener is the matching higher-ranked scope for an already acquired listener. It consumes
the owner, lends one exclusive listener reference to the callback, and attempts terminal close on
every structured exit. Both scopes preserve the callback outcome when close itself reports an
error. Use explicit Listener.close when the accepted connection must be transferred before the
listener scope ends, as in a serial server loop.
Use AcceptedContext<A, E, ?R, ?Q> and withAcceptedContext when the protected operation must
consume owned or once-callable state. R preserves caller-selected requirements and Q records
additional requirements introduced by the concrete context; the protected operation exposes their
union. Interface selection is compile-time only. AcceptedContext.use receives the owned context
independently from its higher-ranked AcceptedView loan, so neither the view nor its connection can
escape. The context is consumed once; the accepted connection closes once after success, typed
failure, or structured cancellation, and a close error does not replace the protected result. As
with withAccepted, neither row's union can contain ByteDuplex; use the explicitly loaned
connection instead.
Acceptance always tries the nonblocking syscall before parking. After temporary unavailability, a
readable zero-time poll permits exactly one immediate accept retry. If that retry also reports
temporary unavailability, the operation takes a positive MonotonicClock wait before trying again;
interrupted syscalls and GNU's documented pending-network errors also wait positively and never
spin. The same absolute deadline is checked before native work and after every wait. None
intentionally permits indefinite cooperative waiting; structured cancellation releases the parked
timer authority before the enclosing listener scope closes its descriptor. Concurrent cross-thread
close is not a supported cancellation mechanism.
TCP peers are owned numeric Endpoint values. Unix peers distinguish Unnamed from an owned
pathname spelling. An unbound peer is recognized from GNU's family-header-only length or Darwin's
family-bearing padded result with a leading-NUL pathname; Darwin has no abstract Unix namespace,
while GNU's leading-NUL form remains unsupported. A zero-length form is accepted only for a known
Unix listener. Native returned lengths are checked before any family field, port, address, or
pathname byte is read.
Abstract Unix peers are rejected as UnsupportedPeerAddress, and malformed families or lengths as
InvalidNativeAddress; only the provisional accepted descriptor is closed.
Unix pathname ownership
listenUnix accepts a nonempty absolute NUL-free byte pathname that fits the platform
sockaddr_un, including its terminal NUL. It never removes a stale entry before bind and never
unlinks a successful entry during close or finalization. The caller must create a private directory,
remove any entry under its own policy, and explicitly unlink the socket entry after the listener is
closed. An occupied or stale pathname reports AddressInUse without destructive cleanup.
Listener and connection close are terminal, idempotent, and nonparking. They do not flush, drain, wait for peers, retry an interrupted close, or mutate the Unix namespace. Structured finalizers attempt exactly one close while preserving the protected callback outcome.
Failures
Pure option construction reports ListenOptionsError with the rejected field and value. Local
pathname admission reports InvalidBindAddress or PathTooLong before socket creation. Native
listener setup distinguishes only AddressInUse, PermissionDenied, FamilyUnsupported, and
SystemResources; every other listener failure retains its exact NativeSocketOperation and
signed errno. In particular, connection-oriented errno values are not collapsed while creating or
accepting a listener.
Acceptance additionally reports Timeout, Closed, InvalidNativeAddress, or
UnsupportedPeerAddress. Once an Accepted value transfers, byte I/O continues to use the
existing ByteIoError contract rather than widening it with listener failures.
The accept loop retries only temporary unavailability, interruption, readiness races, and the documented GNU pending-network errno set. Resource, permission, invalid-descriptor, and unknown errors are terminal. No retry renews the supplied deadline, and a deadline already equal to the current monotonic mark performs no accept or poll call.