blob: 84b47a5506cfe9c7de3e924165385c6b566afa56 [file]
// Copyright 2026 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// Async socket abstraction.
//
// An iree_async_socket_t wraps a platform socket (TCP, UDP, Unix) and is bound
// to a specific proactor for async I/O operations. Sockets are created via
// iree_async_socket_create() / iree_async_socket_import() and used as targets
// for async accept, connect, recv, and send operations.
//
// Sockets are ref-counted. The proactor retains a reference while operations
// are in flight. The caller's reference controls logical ownership; releasing
// it after all operations complete (or are cancelled) triggers cleanup.
//
// Sticky failure: once a socket encounters an error, it enters a permanently
// failed state. Subsequent operations on a failed socket complete immediately
// with the recorded failure status.
#ifndef IREE_ASYNC_SOCKET_H_
#define IREE_ASYNC_SOCKET_H_
#include "iree/async/address.h"
#include "iree/async/primitive.h"
#include "iree/base/api.h"
#include "iree/base/internal/atomics.h"
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
typedef struct iree_async_proactor_t iree_async_proactor_t;
//===----------------------------------------------------------------------===//
// Socket
//===----------------------------------------------------------------------===//
// The kind of socket to create.
// IPv4 and IPv6 variants are separate types (no dual-stack) to avoid
// platform-specific IPV6_V6ONLY default behavior differences.
enum iree_async_socket_type_e {
IREE_ASYNC_SOCKET_TYPE_TCP = 0u,
IREE_ASYNC_SOCKET_TYPE_UDP,
IREE_ASYNC_SOCKET_TYPE_UNIX_STREAM,
IREE_ASYNC_SOCKET_TYPE_UNIX_DGRAM,
// IPv6 variants use AF_INET6 exclusively.
IREE_ASYNC_SOCKET_TYPE_TCP6,
IREE_ASYNC_SOCKET_TYPE_UDP6,
};
typedef uint8_t iree_async_socket_type_t;
// Socket options set at creation time. Immutable after the socket is created.
// The proactor applies these to the underlying platform handle during
// create_socket and may use them to select backend-specific optimizations
// (e.g., enabling kernel zero-copy paths, configuring io_uring features).
//
// For imported sockets (import_socket), options are set to NONE — the caller
// is responsible for pre-configuring the platform handle before import.
//
// Queried via iree_async_socket_query_options() after creation.
enum iree_async_socket_option_bits_e {
IREE_ASYNC_SOCKET_OPTION_NONE = 0u,
// SO_REUSEADDR: allows binding to a port that was recently in use.
// Required for servers that restart without waiting for TIME_WAIT to expire.
IREE_ASYNC_SOCKET_OPTION_REUSE_ADDR = 1u << 0,
// SO_REUSEPORT: allows multiple sockets to bind to the same port.
// The kernel load-balances incoming connections across them. Useful for
// multi-proactor server architectures where each proactor thread has its own
// listening socket on the same port.
IREE_ASYNC_SOCKET_OPTION_REUSE_PORT = 1u << 1,
// TCP_NODELAY: disables Nagle's algorithm. Sends data immediately without
// waiting to coalesce small writes. Required for low-latency protocols.
IREE_ASYNC_SOCKET_OPTION_NO_DELAY = 1u << 2,
// SO_KEEPALIVE: enables TCP keepalive probes. Detects dead connections
// without application-level heartbeats. The kernel sends probes after an
// idle period and closes the connection if the peer is unreachable.
IREE_ASYNC_SOCKET_OPTION_KEEP_ALIVE = 1u << 3,
// Hint to use kernel zero-copy send path when available.
//
// On Linux, this calls setsockopt(SO_ZEROCOPY) on the socket. The send path
// then uses zero-copy (SEND_ZC on io_uring 6.0+) or falls back to regular
// send transparently based on whether the proactor has
// IREE_ASYNC_PROACTOR_CAPABILITY_ZERO_COPY_SEND. On platforms without
// SO_ZEROCOPY (Windows, macOS), the option is accepted silently and sends
// always use the regular copy path.
//
// Accepted sockets inherit this option from their listening socket. The
// proactor calls setsockopt(SO_ZEROCOPY) on each accepted fd since the
// kernel does not inherit this socket option across accept().
IREE_ASYNC_SOCKET_OPTION_ZERO_COPY = 1u << 4,
// SO_LINGER with timeout=0: forces immediate RST on close instead of
// graceful FIN. When set, closing a socket discards any unsent data and
// sends RST to the peer, avoiding TIME_WAIT state. Useful for:
// - Aborting connections that are known to be broken
// - Avoiding TIME_WAIT accumulation in high-churn scenarios
// - Testing RST handling paths
IREE_ASYNC_SOCKET_OPTION_LINGER_ZERO = 1u << 5,
};
typedef uint32_t iree_async_socket_options_t;
// Runtime behavior flags stored on the socket.
// Unlike options (which configure the socket via setsockopt at creation and
// are then discarded), flags control proactor behavior for operations on
// this socket.
//
// For create_socket: flags are derived from options (e.g., ZERO_COPY option
// sets ZERO_COPY flag).
// For import_socket: caller declares which flags apply to the imported socket.
enum iree_async_socket_flag_bits_e {
IREE_ASYNC_SOCKET_FLAG_NONE = 0u,
// Send operations prefer zero-copy path (SEND_ZC on io_uring 6.0+) when the
// proactor has IREE_ASYNC_PROACTOR_CAPABILITY_ZERO_COPY_SEND; otherwise sends
// use the regular copy path transparently.
// For create_socket: set when IREE_ASYNC_SOCKET_OPTION_ZERO_COPY is
// requested. For import_socket: caller must set this if they configured
// SO_ZEROCOPY.
IREE_ASYNC_SOCKET_FLAG_ZERO_COPY = 1u << 0,
};
typedef uint32_t iree_async_socket_flags_t;
// Diagnostic connection state. This tracks the socket's logical lifecycle but
// is not used for protocol logic — operations determine actual behavior.
enum iree_async_socket_state_e {
IREE_ASYNC_SOCKET_STATE_CREATED = 0u,
IREE_ASYNC_SOCKET_STATE_CONNECTING,
IREE_ASYNC_SOCKET_STATE_CONNECTED,
IREE_ASYNC_SOCKET_STATE_LISTENING,
IREE_ASYNC_SOCKET_STATE_CLOSING,
IREE_ASYNC_SOCKET_STATE_CLOSED,
};
typedef uint8_t iree_async_socket_state_t;
// A proactor-managed socket. Created via iree_async_socket_create() or
// iree_async_socket_import().
typedef struct iree_async_socket_t {
iree_atomic_ref_count_t ref_count;
// The proactor this socket is bound to. All operations on this socket must
// be submitted to this proactor. Not retained (proactor outlives sockets).
iree_async_proactor_t* proactor;
// Underlying platform handle.
iree_async_primitive_t primitive;
// io_uring fixed file index for reduced syscall overhead (-1 if not
// registered). Backend-specific optimization; ignored on other platforms.
int32_t fixed_file_index;
// Socket type (TCP, UDP, Unix stream/dgram).
iree_async_socket_type_t type;
// Current diagnostic state.
iree_async_socket_state_t state;
// Runtime behavior flags. Immutable after creation.
// Controls proactor behavior for operations on this socket.
iree_async_socket_flags_t flags;
// Sticky failure status. Once set, all operations on this socket fail with
// this status. Stored as an atomic for thread-safe access.
iree_atomic_intptr_t failure_status;
IREE_TRACE(char debug_label[64];)
} iree_async_socket_t;
// Creates a new socket of the given type with the specified options.
//
// The socket is bound to |proactor| and must be used only with it for all async
// operations. The returned socket has one reference; caller must release it
// when done (or submit a close operation, which consumes the ref).
//
// Availability:
// generic | io_uring | IOCP | kqueue
// yes | yes | yes | yes
//
// Options consumption:
// |options| are applied to the platform handle during creation via setsockopt
// and are not stored—they are consumed and discarded. The socket does not
// remember what options were requested.
//
// Options are best-effort:
// Options that require kernel support (e.g., ZERO_COPY requiring
// SO_ZEROCOPY) are applied via setsockopt when available. On platforms
// without the underlying support, the option is accepted silently and the
// corresponding behavior degrades to the regular path. The ZERO_COPY flag
// is still recorded on the socket so the send path can attempt zero-copy
// when the proactor has IREE_ASYNC_PROACTOR_CAPABILITY_ZERO_COPY_SEND.
//
// Returns:
// IREE_STATUS_OK: Socket created successfully.
// IREE_STATUS_RESOURCE_EXHAUSTED: System socket limit reached.
IREE_API_EXPORT iree_status_t iree_async_socket_create(
iree_async_proactor_t* proactor, iree_async_socket_type_t type,
iree_async_socket_options_t options, iree_async_socket_t** out_socket);
// Imports an existing platform handle as a proactor-managed socket.
//
// Use this to bring externally-created sockets (e.g., from accept() or
// inherited from a parent process) under proactor management.
//
// Availability:
// generic | io_uring | IOCP | kqueue
// yes | yes | yes | yes
//
// Ownership:
// The proactor takes logical ownership of the handle and will close it when
// the socket is released or a close operation completes. The caller must not
// close the handle after a successful import.
//
// No mutation:
// The proactor does NOT modify the socket (no setsockopt, no fcntl). The
// caller is responsible for pre-configuring nonblocking mode, socket options,
// etc. before import. This allows importing sockets with non-default
// configurations that the proactor doesn't know about.
//
// Flags parameter:
// The |flags| parameter declares runtime behavior for the imported socket.
// For zero-copy: caller must have set SO_ZEROCOPY via setsockopt before
// import and pass IREE_ASYNC_SOCKET_FLAG_ZERO_COPY in |flags|.
//
// Platform handles:
// - POSIX: int fd (socket file descriptor)
// - Windows: SOCKET (cast to uintptr_t in primitive.handle)
//
// Returns:
// IREE_STATUS_OK: Socket imported successfully.
// IREE_STATUS_INVALID_ARGUMENT: Invalid handle or type mismatch.
IREE_API_EXPORT iree_status_t iree_async_socket_import(
iree_async_proactor_t* proactor, iree_async_primitive_t primitive,
iree_async_socket_type_t type, iree_async_socket_flags_t flags,
iree_async_socket_t** out_socket);
// Increments the reference count.
IREE_API_EXPORT void iree_async_socket_retain(iree_async_socket_t* socket);
// Decrements the reference count and destroys if it reaches zero.
IREE_API_EXPORT void iree_async_socket_release(iree_async_socket_t* socket);
// Returns the socket's current diagnostic state.
static inline iree_async_socket_state_t iree_async_socket_query_state(
const iree_async_socket_t* socket) {
return socket->state;
}
// Returns the sticky failure status, or iree_ok_status() if not failed.
// The returned status is NOT owned by the caller (peek semantics).
static inline iree_status_t iree_async_socket_query_failure(
const iree_async_socket_t* socket) {
intptr_t value =
iree_atomic_load((iree_atomic_intptr_t*)&socket->failure_status,
iree_memory_order_acquire);
return (iree_status_t)value;
}
// Sets the socket's sticky failure status if not already failed.
// Uses atomic compare-exchange so the first error wins: if the socket already
// has a failure, |status| is discarded and the original failure preserved.
//
// Only the status code is stored (no storage allocation). The full error
// context is delivered to the caller via the operation's completion callback;
// the sticky failure is a persistent "is this socket still usable?" indicator
// that supports peek semantics in iree_async_socket_query_failure().
static inline void iree_async_socket_set_failure(iree_async_socket_t* socket,
iree_status_t status) {
if (iree_status_is_ok(status)) return;
intptr_t failure_code =
(intptr_t)iree_status_from_code(iree_status_code(status));
intptr_t expected = (intptr_t)iree_ok_status();
iree_atomic_compare_exchange_strong(&socket->failure_status, &expected,
failure_code, iree_memory_order_acq_rel,
iree_memory_order_acquire);
}
//===----------------------------------------------------------------------===//
// Socket lifecycle (synchronous, pre-async-I/O setup)
//===----------------------------------------------------------------------===//
// Synchronous configuration calls that prepare a socket for async I/O.
// These operate directly on the platform handle and do not involve the
// proactor's event loop. They are called once during socket setup — the socket
// is then ready for async operations (accept, connect, recv, send).
//
// Server setup:
// iree_async_socket_create(proactor, TCP, options, &socket);
// iree_async_socket_bind(socket, &address);
// iree_async_socket_listen(socket, backlog);
// // Submit multishot accept operations via the proactor.
//
// Client setup:
// iree_async_socket_create(proactor, TCP, options, &socket);
// // Submit async connect operation via the proactor.
// Binds the socket to a local address. Must be called before listen.
// Returns IREE_STATUS_ALREADY_EXISTS if the address is in use (and
// REUSE_ADDR/REUSE_PORT were not enabled at creation time).
IREE_API_EXPORT iree_status_t iree_async_socket_bind(
iree_async_socket_t* socket, const iree_async_address_t* address);
// Puts the socket into listening state with the given backlog queue depth.
// Must be called after bind. The socket's diagnostic state transitions to
// LISTENING. |backlog| is a hint for the kernel's accept queue size; 0 uses
// the system default (typically 128 on Linux).
IREE_API_EXPORT iree_status_t
iree_async_socket_listen(iree_async_socket_t* socket, iree_host_size_t backlog);
// Queries the local address the socket is bound to (getsockname).
// Useful after binding to port 0 (ephemeral) to discover the assigned port.
// Returns IREE_STATUS_FAILED_PRECONDITION if the socket is not yet bound.
IREE_API_EXPORT iree_status_t iree_async_socket_query_local_address(
const iree_async_socket_t* socket, iree_async_address_t* out_address);
// Shutdown mode for iree_async_socket_shutdown().
enum iree_async_socket_shutdown_mode_e {
// Disables further receive operations. Data already in the receive buffer
// can still be read. The peer's subsequent sends may fail with
// EPIPE/ECONNRESET.
IREE_ASYNC_SOCKET_SHUTDOWN_READ = 0,
// Disables further send operations and sends FIN to the peer (half-close).
// The peer's recv will return 0 (EOF) after reading any buffered data.
// The socket can still receive data until the peer also closes.
IREE_ASYNC_SOCKET_SHUTDOWN_WRITE = 1,
// Disables both send and receive. Equivalent to calling shutdown with both
// modes, but the socket remains valid for querying state (unlike close).
IREE_ASYNC_SOCKET_SHUTDOWN_BOTH = 2,
};
typedef uint8_t iree_async_socket_shutdown_mode_t;
// Shuts down part or all of the socket's communication.
// This is a synchronous operation that affects the underlying socket
// immediately. The socket remains valid after shutdown; use release to destroy
// it.
//
// Shutdown modes:
// SHUTDOWN_READ: Peer's sends will fail; local recv returns 0 or error.
// SHUTDOWN_WRITE: Sends FIN; peer recv returns 0 (EOF). Local can still recv.
// SHUTDOWN_BOTH: Disables both directions; socket still valid for queries.
//
// Returns IREE_STATUS_FAILED_PRECONDITION if the socket is not connected.
IREE_API_EXPORT iree_status_t iree_async_socket_shutdown(
iree_async_socket_t* socket, iree_async_socket_shutdown_mode_t mode);
// Queries available send buffer space for backpressure tracking.
//
// Returns an estimate of how many bytes can be sent without blocking or hitting
// flow control. This is useful for implementing application-level backpressure.
//
// Platform behavior:
// - Linux: Uses ioctl(SIOCOUTQ) to query unsent bytes in the send queue,
// then returns SO_SNDBUF - unsent. This is accurate but queries current
// state (not a guarantee of future capacity).
// - macOS/Windows: No equivalent to SIOCOUTQ exists. Returns
// IREE_HOST_SIZE_MAX
// (unknown). Callers must rely on send() return codes (EAGAIN/EWOULDBLOCK)
// for backpressure.
//
// On success, |out_space| is set to:
// - Estimated available send space in bytes on Linux.
// - IREE_HOST_SIZE_MAX if the query is not supported on this platform.
// - 0 if the send buffer is currently full.
//
// Returns an error if the socket is in a failed state.
IREE_API_EXPORT iree_status_t iree_async_socket_query_send_space(
iree_async_socket_t* socket, iree_host_size_t* out_space);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
#endif // IREE_ASYNC_SOCKET_H_