| // 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 | |
| // Proactor: the central abstraction for completion-based async I/O. | |
| // | |
| // A proactor manages the submission of async operations and dispatches | |
| // completion callbacks when they finish. It is vtable-dispatched for backend | |
| // polymorphism (io_uring, kqueue, IOCP, threaded emulation). | |
| // | |
| // The proactor is caller-driven: it only makes progress when poll() is called. | |
| // All callbacks fire from within poll() on the calling thread. A utility | |
| // wrapper (iree_async_proactor_thread_t) provides optional dedicated-thread | |
| // operation. | |
| // | |
| // Threading model: | |
| // The proactor has single-thread ownership semantics for poll(). The first | |
| // thread to call poll() becomes the poll owner for the proactor's lifetime. | |
| // Only that thread may call poll(); calling from any other thread is | |
| // undefined behavior. This enables lock-free fast paths in high-performance | |
| // backends (e.g., io_uring's DEFER_TASKRUN mode). Other operations like | |
| // submit(), cancel(), wake(), import_fence(), and export_fence() are | |
| // thread-safe and may be called from any thread. | |
| // | |
| // Lifecycle: backend-specific _create functions → _retain/_release. | |
| // Destroy is at vtable offset 0 for toll-free bridging to iree_vm_ref_t. | |
| #ifndef IREE_ASYNC_PROACTOR_H_ | |
| #define IREE_ASYNC_PROACTOR_H_ | |
| #include "iree/async/operation.h" | |
| #include "iree/async/primitive.h" | |
| #include "iree/async/region.h" | |
| #include "iree/async/relay.h" | |
| #include "iree/async/slab.h" | |
| #include "iree/async/socket.h" | |
| #include "iree/async/types.h" | |
| #include "iree/base/api.h" | |
| #include "iree/base/internal/atomics.h" | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #endif // __cplusplus | |
| typedef struct iree_async_file_t iree_async_file_t; | |
| typedef struct iree_async_event_t iree_async_event_t; | |
| typedef struct iree_async_event_source_t iree_async_event_source_t; | |
| typedef struct iree_async_notification_t iree_async_notification_t; | |
| typedef struct iree_async_notification_shared_options_t | |
| iree_async_notification_shared_options_t; | |
| typedef struct iree_async_relay_t iree_async_relay_t; | |
| typedef struct iree_async_semaphore_t iree_async_semaphore_t; | |
| // Notification flags type (full definition in notification.h). | |
| typedef uint32_t iree_async_notification_flags_t; | |
| typedef struct iree_async_proactor_t iree_async_proactor_t; | |
| typedef struct iree_async_proactor_vtable_t iree_async_proactor_vtable_t; | |
| typedef struct iree_async_signal_subscription_t | |
| iree_async_signal_subscription_t; | |
| //===----------------------------------------------------------------------===// | |
| // Event source callbacks | |
| //===----------------------------------------------------------------------===// | |
| // Poll events passed to event source callbacks. | |
| // | |
| // Cross-platform abstraction over native poll events. Backends translate from | |
| // their native representation (POLLIN/POLLERR/POLLHUP on POSIX, equivalent | |
| // flags on other platforms) to these portable values. | |
| // | |
| // The callback always fires for any poll activity; use these flags to | |
| // determine what occurred and how to respond. | |
| typedef enum iree_async_poll_event_e { | |
| IREE_ASYNC_POLL_EVENT_NONE = 0, | |
| // Data available to read (POLLIN equivalent). | |
| IREE_ASYNC_POLL_EVENT_IN = 1u << 0, | |
| // Error condition on the fd (POLLERR equivalent). | |
| IREE_ASYNC_POLL_EVENT_ERR = 1u << 1, | |
| // Hang up / disconnected (POLLHUP/POLLRDHUP equivalent). | |
| IREE_ASYNC_POLL_EVENT_HUP = 1u << 2, | |
| // Ready for writing / send buffer available (POLLOUT equivalent). | |
| // Used for backpressure handling in non-blocking write loops. | |
| // kqueue: EVFILT_WRITE, IOCP: FD_WRITE. | |
| IREE_ASYNC_POLL_EVENT_OUT = 1u << 3, | |
| } iree_async_poll_event_t; | |
| // Bitmask of poll events. | |
| typedef uint32_t iree_async_poll_events_t; | |
| // Returns true if error events are present without data available. | |
| // | |
| // This indicates a fatal error condition on the fd (disconnected, device | |
| // removed, etc.). The callback should handle cleanup rather than attempting | |
| // to read from the fd. | |
| static inline bool iree_async_poll_has_error(iree_async_poll_events_t events) { | |
| return (events & (IREE_ASYNC_POLL_EVENT_ERR | IREE_ASYNC_POLL_EVENT_HUP)) && | |
| !(events & IREE_ASYNC_POLL_EVENT_IN); | |
| } | |
| // Callback invoked when an event source is signaled. | |
| // | |
| // Fires from within proactor poll() on the polling thread. The source remains | |
| // armed after the callback returns (multishot behavior). Heavy work should be | |
| // deferred to avoid stalling completion dispatch. | |
| // | |
| // Parameters: | |
| // user_data: Value from the callback struct at registration time. | |
| // source: The event source that was signaled. The source remains valid until | |
| // unregistered. Do not call unregister from within the callback. | |
| // events: Bitmask of poll events that occurred. Check with | |
| // iree_async_poll_has_error() to detect error conditions. For RDMA CQ | |
| // channels, IREE_ASYNC_POLL_EVENT_IN indicates completions are available. | |
| typedef void (*iree_async_event_source_callback_fn_t)( | |
| void* user_data, iree_async_event_source_t* source, | |
| iree_async_poll_events_t events); | |
| // Callback wrapper struct for event source notifications. | |
| // Follows the pattern of iree_async_buffer_recycle_callback_t. | |
| typedef struct iree_async_event_source_callback_t { | |
| iree_async_event_source_callback_fn_t fn; | |
| void* user_data; | |
| } iree_async_event_source_callback_t; | |
| // Returns a null event source callback (no notification). | |
| static inline iree_async_event_source_callback_t | |
| iree_async_event_source_callback_null(void) { | |
| iree_async_event_source_callback_t callback = {NULL, NULL}; | |
| return callback; | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Progress callbacks (inline poll-loop work) | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Progress callbacks run every poll() iteration, enabling hot-path subsystems | |
| // to check for work without kernel-mediated wakeups. The canonical use case is | |
| // SHM carrier SPSC ring polling: when traffic is active, a progress callback | |
| // checks the ring position with an acquire-load (~50ns) instead of waiting for | |
| // a notification signal through the kernel (~1-5us). | |
| // | |
| // Registration/unregistration are poll-thread-only operations: no | |
| // synchronization is needed because the same thread calls poll(). Callbacks | |
| // fire from within poll() before the blocking wait. If any callback returns > 0 | |
| // completions, the backend forces a non-blocking poll (timeout=0) to avoid | |
| // blocking when user-space progress is available. | |
| // | |
| // Callbacks may request their own removal by setting remove_requested=true. | |
| // The proactor removes the entry from the list after the callback returns, | |
| // avoiding list corruption during iteration. | |
| // | |
| // Typical lifecycle: | |
| // 1. Subsystem detects hot traffic (e.g., notification callback processes | |
| // data above a threshold). | |
| // 2. Subsystem registers a progress callback and stops posting kernel waits. | |
| // 3. Progress callback polls user-space state each iteration. | |
| // 4. After N consecutive empty polls, callback transitions back to kernel | |
| // waits and sets remove_requested=true. | |
| // Intrusive singly-linked list entry for per-poll-iteration progress checks. | |
| // Owned by the registrant (e.g., embedded in a carrier struct). The proactor | |
| // holds a linked list of these; all mutations happen on the poll thread only. | |
| typedef struct iree_async_progress_entry_t { | |
| struct iree_async_progress_entry_t* next; | |
| // Called each poll() iteration. Returns the number of completions processed. | |
| // Zero means no progress was made. The callback may set |remove_requested| | |
| // to request removal after returning. | |
| iree_host_size_t (*fn)(void* user_data); | |
| void* user_data; | |
| // Set by the callback to request removal after fn returns. The proactor | |
| // removes the entry from the list after the callback completes, avoiding | |
| // list corruption during iteration. | |
| bool remove_requested; | |
| // Optional callback invoked AFTER the entry is unlinked from the proactor's | |
| // list (only called when remove_requested is set). This fires after fn | |
| // returns and the entry is safely removed, so it may trigger higher-level | |
| // cleanup (e.g., deactivation completion) that could free the entry. The | |
| // proactor does not access the entry after on_remove returns. | |
| void (*on_remove)(void* user_data); | |
| } iree_async_progress_entry_t; | |
| //===----------------------------------------------------------------------===// | |
| // Proactor | |
| //===----------------------------------------------------------------------===// | |
| // Proactor capability flags. | |
| // | |
| // Use iree_async_proactor_query_capabilities() to discover which features | |
| // the current backend supports. Callers can use these to select optimal code | |
| // paths or verify that the backend supports required features. | |
| // | |
| // Capability matrix by backend: | |
| // | |
| // Capability generic | io_uring | IOCP | kqueue | |
| // ──────────────────────────────────────────────────────── | |
| // MULTISHOT emul | 5.19+ | emul | emul | |
| // FIXED_FILES emul | 5.1+ | n/a | n/a | |
| // REGISTERED_BUFFERS emul | 5.1+ | reg | emul | |
| // LINKED_OPERATIONS emul | 5.3+ | emul | emul | |
| // ZERO_COPY_SEND copy | 6.0+ | reg | copy | |
| // DMABUF n/a | 5.19+ | n/a | n/a | |
| // DEVICE_FENCE poll | poll | yes | poll | |
| // ABSOLUTE_TIMEOUT emul | 5.4+ | yes | emul | |
| // FUTEX_OPERATIONS n/a | 6.7+ | n/a | n/a | |
| // | |
| // Legend: | |
| // yes/X.Y+ = Native kernel support (optimal path). | |
| // emul = Emulated (correct behavior, per-operation overhead). | |
| // copy = Falls back to copy (flag accepted but not honored). | |
| // reg = Native only with pre-registered buffers. | |
| // poll = Polls on fd (correct, adds wake latency). | |
| // n/a = Hardware-specific; API returns IREE_STATUS_UNAVAILABLE. | |
| // | |
| enum iree_async_proactor_capability_bits_e { | |
| IREE_ASYNC_PROACTOR_CAPABILITY_NONE = 0u, | |
| // Supports multishot operations (persistent accept/recv). | |
| // When enabled, a single submit can produce multiple completions until | |
| // explicitly cancelled. Reduces syscall overhead for high-throughput | |
| // accept loops and streaming receives. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.19+ | emul | emul | |
| IREE_ASYNC_PROACTOR_CAPABILITY_MULTISHOT = 1u << 0, | |
| // Supports registered/fixed file descriptors for reduced syscall overhead. | |
| // Pre-registers sockets/files with the kernel, eliminating per-operation | |
| // fd lookup costs. Most beneficial for high-frequency operations on the | |
| // same set of connections. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.1+ | n/a | n/a | |
| IREE_ASYNC_PROACTOR_CAPABILITY_FIXED_FILES = 1u << 1, | |
| // Supports registered memory buffers for zero-copy I/O. | |
| // Pre-registers memory regions with the kernel, enabling DMA directly | |
| // to/from application buffers without intermediate copies. Critical for | |
| // achieving line-rate network throughput. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.1+ | reg | emul | |
| IREE_ASYNC_PROACTOR_CAPABILITY_REGISTERED_BUFFERS = 1u << 2, | |
| // Supports linked operations (io_uring linked SQEs) for kernel-chained | |
| // sequences without user-space round-trips between steps. Enables patterns | |
| // like "recv → process → send" to execute entirely in kernel space. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.3+ | emul | emul | |
| IREE_ASYNC_PROACTOR_CAPABILITY_LINKED_OPERATIONS = 1u << 3, | |
| // Supports zero-copy send (MSG_ZEROCOPY / io_uring SEND_ZC). | |
| // Avoids copying send data into kernel buffers. The application buffer | |
| // must remain valid until the zero-copy completion notification arrives. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // copy | 6.0+ | reg | copy | |
| IREE_ASYNC_PROACTOR_CAPABILITY_ZERO_COPY_SEND = 1u << 4, | |
| // Supports dmabuf registration for device memory I/O paths. | |
| // Enables GPU↔NIC and GPU↔NVMe zero-copy transfers without staging | |
| // through host memory. Requires compatible hardware and drivers. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // n/a | 5.19+ | n/a | n/a | |
| IREE_ASYNC_PROACTOR_CAPABILITY_DMABUF = 1u << 5, | |
| // Supports device fence import/export (sync_file ↔ semaphore bridging). | |
| // Enables GPU→proactor and proactor→GPU synchronization without CPU | |
| // involvement in the fast path. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // poll | poll | yes | poll | |
| IREE_ASYNC_PROACTOR_CAPABILITY_DEVICE_FENCE = 1u << 6, | |
| // Supports absolute timeouts (e.g., io_uring IORING_TIMEOUT_ABS on 5.4+). | |
| // When set, timer operations use absolute deadlines with no drift. | |
| // When not set, the backend converts to relative at submission time, | |
| // which introduces potential drift under scheduling delays. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.4+ | yes | emul | |
| IREE_ASYNC_PROACTOR_CAPABILITY_ABSOLUTE_TIMEOUT = 1u << 7, | |
| // Supports kernel-side futex operations (io_uring FUTEX_WAIT/WAKE on 6.7+). | |
| // When set, futex wait/wake can be submitted as io_uring operations, | |
| // enabling LINK chains like POLL_ADD → FUTEX_WAKE for efficient | |
| // semaphore-to-futex bridging without userspace round-trips. | |
| // When not set, callers must use direct syscalls for futex operations | |
| // (iree/base/threading/futex.h). | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // n/a | 6.7+ | n/a | n/a | |
| IREE_ASYNC_PROACTOR_CAPABILITY_FUTEX_OPERATIONS = 1u << 8, | |
| // Supports kernel-mediated cross-proactor messaging (io_uring | |
| // MSG_RING 5.18+). When set, iree_async_proactor_send_message() uses | |
| // zero-copy ring-to-ring communication. When not set, falls back to eventfd | |
| // wake + shared queue. LINK chain support (kernel-side I/O → message | |
| // chaining) requires this. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // n/a | 5.18+ | n/a | n/a | |
| IREE_ASYNC_PROACTOR_CAPABILITY_PROACTOR_MESSAGING = 1u << 9, | |
| // Supports kernel wait completion packets for direct Event-to-IOCP | |
| // delivery (NtAssociateWaitCompletionPacket on Windows 8.1+). When set, | |
| // event waits and shared notification wake monitoring bypass the | |
| // RegisterWaitForSingleObject threadpool, eliminating context switches | |
| // and reducing the wake path from 3 kernel transitions to 1. When not | |
| // set, falls back to RegisterWaitForSingleObject. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // n/a | n/a | Win 8.1+ | n/a | |
| IREE_ASYNC_PROACTOR_CAPABILITY_WAIT_COMPLETION_PACKET = 1u << 10, | |
| // All capabilities enabled (for allowed_capabilities default). | |
| IREE_ASYNC_PROACTOR_CAPABILITY_ALL = ~0u, | |
| }; | |
| // Backend capabilities reported by query_capabilities(). | |
| typedef uint32_t iree_async_proactor_capabilities_t; | |
| // How the proactor will be used relative to threading. | |
| typedef enum iree_async_proactor_threading_mode_e { | |
| // The caller will poll from the same thread that creates the proactor. | |
| // This is the common case for tests, benchmarks, and simple single-threaded | |
| // usage. | |
| IREE_ASYNC_PROACTOR_THREADING_SAME_THREAD = 0, | |
| // The caller will create the proactor on one thread and poll from a different | |
| // thread (e.g., a proactor pool with dedicated poll threads). The proactor | |
| // will defer any per-thread setup to the first poll() call. | |
| IREE_ASYNC_PROACTOR_THREADING_CROSS_THREAD = 1, | |
| } iree_async_proactor_threading_mode_t; | |
| // Options for proactor creation. | |
| // | |
| // All fields have sensible defaults; zero-initialize and override as needed. | |
| // Use iree_async_proactor_options_default() for canonical initialization. | |
| typedef struct iree_async_proactor_options_t { | |
| // Hint: maximum number of concurrently in-flight operations. | |
| // Backends use this to size internal structures (ring buffers, etc.). | |
| // Zero means "use a reasonable default" (typically 256-4096 depending on | |
| // the backend and available system resources). | |
| // | |
| // io_uring: Sets the ring size. Powers of 2 are most efficient. | |
| // IOCP: Ignored (IOCP is dynamically sized). | |
| // kqueue: Sets initial event list allocation. | |
| // generic: Sets internal completion queue size. | |
| iree_host_size_t max_concurrent_operations; | |
| // Debug name for tracing and diagnostics. | |
| // Copied into the proactor; the string view need not remain valid after | |
| // the create call returns. | |
| iree_string_view_t debug_name; | |
| // Capabilities to allow. The backend's detected capabilities are masked | |
| // with this value: `effective = detected & allowed_capabilities`. | |
| // Defaults to IREE_ASYNC_PROACTOR_CAPABILITY_ALL (all capabilities enabled). | |
| // | |
| // Use this to disable specific features for testing or compatibility: | |
| // options.allowed_capabilities &= | |
| // ~IREE_ASYNC_PROACTOR_CAPABILITY_MULTISHOT; | |
| iree_async_proactor_capabilities_t allowed_capabilities; | |
| // Capacity of the cross-proactor message pool. Zero means use the default | |
| // (IREE_ASYNC_MESSAGE_POOL_DEFAULT_CAPACITY = 256). Each entry is 24 bytes, | |
| // so the default uses ~6KB of trailing data in the proactor allocation. | |
| // Increase for workloads with many concurrent in-flight messages across | |
| // proactors; decrease on memory-constrained embedded targets. | |
| iree_host_size_t message_pool_capacity; | |
| // Threading model. Defaults to SAME_THREAD. | |
| iree_async_proactor_threading_mode_t threading_mode; | |
| } iree_async_proactor_options_t; | |
| // Returns default proactor options. | |
| static inline iree_async_proactor_options_t iree_async_proactor_options_default( | |
| void) { | |
| iree_async_proactor_options_t options = {0}; | |
| options.allowed_capabilities = IREE_ASYNC_PROACTOR_CAPABILITY_ALL; | |
| return options; | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Cross-proactor messaging | |
| //===----------------------------------------------------------------------===// | |
| // Callback invoked when a message is received from another proactor. | |
| // | |
| // |proactor| is the receiving proactor (from within whose poll() this fires). | |
| // |message_data| is the 64-bit payload sent by the source proactor. | |
| // |user_data| is the value set via iree_async_proactor_set_message_callback. | |
| // | |
| // Callbacks fire from within iree_async_proactor_poll() on the polling thread. | |
| // Heavy work should be deferred to avoid stalling completion dispatch. | |
| typedef void (*iree_async_proactor_message_callback_fn_t)( | |
| iree_async_proactor_t* proactor, uint64_t message_data, void* user_data); | |
| // Bundled message callback (function pointer + user data). | |
| typedef struct iree_async_proactor_message_callback_t { | |
| iree_async_proactor_message_callback_fn_t fn; | |
| void* user_data; | |
| } iree_async_proactor_message_callback_t; | |
| //===----------------------------------------------------------------------===// | |
| // Signal handling | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Signals are process-global, so only ONE proactor per process may own signal | |
| // subscriptions. The first proactor to subscribe claims ownership; subsequent | |
| // subscribe calls from other proactors return IREE_STATUS_FAILED_PRECONDITION. | |
| // | |
| // Signal subscriptions use a doubly-linked list per signal type for O(1) | |
| // removal. Subscribe/unsubscribe must be serialized with poll() (see below). | |
| // | |
| // Threading model ("serialized with poll()"): | |
| // Subscribe/unsubscribe are safe in these contexts: | |
| // - Before poll loop starts (no concurrent poll) | |
| // - After poll loop exits (poll has returned) | |
| // - From within poll() callbacks (already serialized) | |
| // - Via wake + message to defer to poll thread | |
| // The dangerous case is calling from another thread while poll() is running. | |
| // | |
| // Typical usage: | |
| // // Early init (before threads): | |
| // iree_async_signal_block_default(); | |
| // iree_async_signal_ignore_broken_pipe(); | |
| // | |
| // // After proactor creation, before poll loop: | |
| // iree_async_signal_subscription_t* sub = NULL; | |
| // iree_async_proactor_subscribe_signal(proactor, IREE_ASYNC_SIGNAL_INTERRUPT, | |
| // callback, &sub); | |
| // | |
| // // Poll loop: signal callbacks fire from within poll(). | |
| // | |
| // // After poll loop exits: | |
| // iree_async_proactor_unsubscribe_signal(proactor, sub); | |
| // Platform-abstracted signal identifiers. | |
| // | |
| // These map to common signals that server applications handle. On Windows, | |
| // only INTERRUPT and TERMINATE are meaningful (console control events). | |
| // Platform-specific signals (HANGUP, QUIT, USER1, USER2) return | |
| // IREE_STATUS_INVALID_ARGUMENT on platforms that don't support them. | |
| typedef enum iree_async_signal_e { | |
| IREE_ASYNC_SIGNAL_NONE = 0, | |
| // Interrupt request (SIGINT / Ctrl+C / CTRL_C_EVENT). | |
| // Typical use: Initiate graceful shutdown. | |
| IREE_ASYNC_SIGNAL_INTERRUPT, | |
| // Termination request (SIGTERM / CTRL_CLOSE_EVENT). | |
| // Typical use: Forced but clean shutdown (systemd, container runtime). | |
| IREE_ASYNC_SIGNAL_TERMINATE, | |
| // Hangup / terminal disconnect (SIGHUP). Unix only. | |
| // Typical use: Configuration reload. | |
| IREE_ASYNC_SIGNAL_HANGUP, | |
| // Quit with core dump (SIGQUIT). Unix only. | |
| // Note: Subscribing to QUIT via signalfd suppresses core dump generation. | |
| // Typical use: Diagnostic dump (thread stacks, memory state). | |
| IREE_ASYNC_SIGNAL_QUIT, | |
| // User-defined signal 1 (SIGUSR1). Unix only. | |
| IREE_ASYNC_SIGNAL_USER1, | |
| // User-defined signal 2 (SIGUSR2). Unix only. | |
| IREE_ASYNC_SIGNAL_USER2, | |
| IREE_ASYNC_SIGNAL_COUNT, | |
| } iree_async_signal_t; | |
| // Returns the platform name for a signal (e.g., "SIGINT", "interrupt"). | |
| // Returns "unknown" for invalid signal values. | |
| IREE_API_EXPORT iree_string_view_t | |
| iree_async_signal_name(iree_async_signal_t signal); | |
| // Callback invoked when a subscribed signal is received. The |signal| | |
| // indicates which signal fired and |user_data| is the value provided at | |
| // subscription time. | |
| // | |
| // Fires from within iree_async_proactor_poll() on the polling thread. Multiple | |
| // signals of the same type arriving between polls may be coalesced into a | |
| // single callback. The callback should be fast; defer heavy work to avoid | |
| // stalling completion dispatch. | |
| typedef void (*iree_async_signal_callback_fn_t)(void* user_data, | |
| iree_async_signal_t signal); | |
| typedef struct iree_async_signal_callback_t { | |
| iree_async_signal_callback_fn_t fn; | |
| void* user_data; | |
| } iree_async_signal_callback_t; | |
| static inline iree_async_signal_callback_t iree_async_signal_callback_null( | |
| void) { | |
| iree_async_signal_callback_t callback = {NULL, NULL}; | |
| return callback; | |
| } | |
| // Signal subscription handle. | |
| // | |
| // Subscriptions form a doubly-linked list per signal type. The proactor owns | |
| // the list heads; subscriptions are allocated by subscribe and freed by | |
| // unsubscribe. The subscription struct is exposed for inline access but | |
| // should be treated as opaque by callers. | |
| // | |
| // Lifetime: | |
| // - Created by iree_async_proactor_subscribe_signal() | |
| // - Valid until iree_async_proactor_unsubscribe_signal() or proactor destroy | |
| // - Proactor destroy automatically unsubscribes all remaining subscriptions | |
| struct iree_async_signal_subscription_t { | |
| // Intrusive doubly-linked list for O(1) removal. | |
| struct iree_async_signal_subscription_t* next; | |
| struct iree_async_signal_subscription_t* prev; | |
| // Separate link for deferred unsubscribe list. During dispatch, unsubscribes | |
| // are deferred to avoid corrupting the main list during iteration. This field | |
| // is used to build the deferred list without touching next/prev. | |
| struct iree_async_signal_subscription_t* pending_next; | |
| // Back-pointer to owning proactor (for unsubscribe validation). | |
| iree_async_proactor_t* proactor; | |
| // Which signal this subscription is for. | |
| iree_async_signal_t signal; | |
| // User callback. | |
| iree_async_signal_callback_t callback; | |
| }; | |
| //===----------------------------------------------------------------------===// | |
| // Proactor vtable | |
| //===----------------------------------------------------------------------===// | |
| typedef struct iree_async_proactor_vtable_t { | |
| // Offset 0: destroy. Required for toll-free bridging to iree_vm_ref_t. | |
| // Called when ref_count reaches zero. Must release all backend resources. | |
| void (*destroy)(iree_async_proactor_t* proactor); | |
| iree_async_proactor_capabilities_t (*query_capabilities)( | |
| iree_async_proactor_t* proactor); | |
| iree_status_t (*submit)(iree_async_proactor_t* proactor, | |
| iree_async_operation_list_t operations); | |
| iree_status_t (*poll)(iree_async_proactor_t* proactor, iree_timeout_t timeout, | |
| iree_host_size_t* out_completed_count); | |
| void (*wake)(iree_async_proactor_t* proactor); | |
| iree_status_t (*cancel)(iree_async_proactor_t* proactor, | |
| iree_async_operation_t* operation); | |
| iree_status_t (*create_socket)(iree_async_proactor_t* proactor, | |
| iree_async_socket_type_t type, | |
| iree_async_socket_options_t options, | |
| iree_async_socket_t** out_socket); | |
| iree_status_t (*import_socket)(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); | |
| void (*destroy_socket)(iree_async_proactor_t* proactor, | |
| iree_async_socket_t* socket); | |
| iree_status_t (*import_file)(iree_async_proactor_t* proactor, | |
| iree_async_primitive_t primitive, | |
| iree_async_file_t** out_file); | |
| void (*destroy_file)(iree_async_proactor_t* proactor, | |
| iree_async_file_t* file); | |
| iree_status_t (*create_event)(iree_async_proactor_t* proactor, | |
| iree_async_event_t** out_event); | |
| void (*destroy_event)(iree_async_proactor_t* proactor, | |
| iree_async_event_t* event); | |
| iree_status_t (*register_event_source)( | |
| iree_async_proactor_t* proactor, iree_async_primitive_t handle, | |
| iree_async_event_source_callback_t callback, | |
| iree_async_event_source_t** out_event_source); | |
| void (*unregister_event_source)(iree_async_proactor_t* proactor, | |
| iree_async_event_source_t* event_source); | |
| iree_status_t (*create_notification)( | |
| iree_async_proactor_t* proactor, iree_async_notification_flags_t flags, | |
| iree_async_notification_t** out_notification); | |
| iree_status_t (*create_notification_shared)( | |
| iree_async_proactor_t* proactor, | |
| const iree_async_notification_shared_options_t* options, | |
| iree_async_notification_t** out_notification); | |
| void (*destroy_notification)(iree_async_proactor_t* proactor, | |
| iree_async_notification_t* notification); | |
| void (*notification_signal)(iree_async_proactor_t* proactor, | |
| iree_async_notification_t* notification, | |
| int32_t wake_count); | |
| bool (*notification_wait)(iree_async_proactor_t* proactor, | |
| iree_async_notification_t* notification, | |
| uint32_t wait_token, iree_timeout_t timeout); | |
| iree_status_t (*register_relay)( | |
| iree_async_proactor_t* proactor, iree_async_relay_source_t source, | |
| iree_async_relay_sink_t sink, iree_async_relay_flags_t flags, | |
| iree_async_relay_error_callback_t error_callback, | |
| iree_async_relay_t** out_relay); | |
| void (*unregister_relay)(iree_async_proactor_t* proactor, | |
| iree_async_relay_t* relay); | |
| iree_status_t (*register_buffer)( | |
| iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_state_t* state, iree_byte_span_t buffer, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_buffer_registration_entry_t** out_entry); | |
| iree_status_t (*register_dmabuf)( | |
| iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_state_t* state, int dmabuf_fd, | |
| uint64_t offset, iree_host_size_t length, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_buffer_registration_entry_t** out_entry); | |
| void (*unregister_buffer)(iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_entry_t* entry, | |
| iree_async_buffer_registration_state_t* state); | |
| iree_status_t (*register_slab)(iree_async_proactor_t* proactor, | |
| iree_async_slab_t* slab, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_region_t** out_region); | |
| iree_status_t (*import_fence)(iree_async_proactor_t* proactor, | |
| iree_async_primitive_t fence, | |
| iree_async_semaphore_t* semaphore, | |
| uint64_t signal_value); | |
| iree_status_t (*export_fence)(iree_async_proactor_t* proactor, | |
| iree_async_semaphore_t* semaphore, | |
| uint64_t wait_value, | |
| iree_async_primitive_t* out_fence); | |
| // Cross-proactor messaging. NULL if backend does not support messaging. | |
| // Backends with native kernel-mediated delivery (io_uring MSG_RING, IOCP | |
| // PostQueuedCompletionStatus) implement these directly; backends without | |
| // native support (POSIX poll) use a pre-allocated message pool internally. | |
| void (*set_message_callback)(iree_async_proactor_t* proactor, | |
| iree_async_proactor_message_callback_t callback); | |
| iree_status_t (*send_message)(iree_async_proactor_t* target, | |
| uint64_t message_data); | |
| // Signal handling. NULL if platform does not support signals. | |
| // Implementation must claim process-wide signal ownership on first subscribe. | |
| iree_status_t (*subscribe_signal)( | |
| iree_async_proactor_t* proactor, iree_async_signal_t signal, | |
| iree_async_signal_callback_t callback, | |
| iree_async_signal_subscription_t** out_subscription); | |
| void (*unsubscribe_signal)(iree_async_proactor_t* proactor, | |
| iree_async_signal_subscription_t* subscription); | |
| } iree_async_proactor_vtable_t; | |
| // Base proactor structure. Backends extend this with additional fields by | |
| // embedding it as the first member of their implementation struct. | |
| typedef struct iree_async_proactor_t { | |
| iree_atomic_ref_count_t ref_count; | |
| const iree_async_proactor_vtable_t* vtable; | |
| iree_allocator_t allocator; | |
| IREE_TRACE(char debug_name[32];) | |
| // Head of the progress callback list. Poll-thread-only; no synchronization. | |
| // See "Progress callbacks" section above. | |
| iree_async_progress_entry_t* progress_list; | |
| } iree_async_proactor_t; | |
| // Initializes the base proactor fields. Called by backend create functions. | |
| IREE_API_EXPORT void iree_async_proactor_initialize( | |
| const iree_async_proactor_vtable_t* vtable, iree_string_view_t debug_name, | |
| iree_allocator_t allocator, iree_async_proactor_t* out_proactor); | |
| //===----------------------------------------------------------------------===// | |
| // Progress callback management | |
| //===----------------------------------------------------------------------===// | |
| // Registers a progress callback that runs every poll() iteration. | |
| // | |
| // The entry is owned by the caller and must remain valid until unregistered | |
| // (either explicitly or via remove_requested). The entry's fn, user_data, and | |
| // remove_requested fields must be initialized before calling this function. | |
| // | |
| // Must be called from the poll thread only (from within poll() callbacks or | |
| // before the poll loop starts). | |
| IREE_API_EXPORT void iree_async_proactor_register_progress( | |
| iree_async_proactor_t* proactor, iree_async_progress_entry_t* entry); | |
| // Unregisters a progress callback. The entry must have been previously | |
| // registered with this proactor. After this call the entry's fn will not be | |
| // called again. | |
| // | |
| // Must be called from the poll thread only. Must NOT be called from within | |
| // the entry's own fn callback — use remove_requested instead. | |
| IREE_API_EXPORT void iree_async_proactor_unregister_progress( | |
| iree_async_proactor_t* proactor, iree_async_progress_entry_t* entry); | |
| // Runs all registered progress callbacks and returns the total number of | |
| // completions they processed. Entries with remove_requested set are removed | |
| // from the list after their callback returns. | |
| // | |
| // Called by backend poll() implementations before the blocking wait. Backends | |
| // must force a non-blocking poll whenever progress callbacks are registered | |
| // (progress_list is non-NULL), regardless of whether they returned progress | |
| // this iteration. Progress callbacks exist to be polled; blocking while they | |
| // are registered would starve them until an unrelated I/O event arrives. | |
| IREE_API_EXPORT iree_host_size_t | |
| iree_async_proactor_run_progress(iree_async_proactor_t* proactor); | |
| // Retains a reference to the proactor. | |
| static inline void iree_async_proactor_retain(iree_async_proactor_t* proactor) { | |
| iree_atomic_ref_count_inc(&proactor->ref_count); | |
| } | |
| // Releases a reference to the proactor. Destroys when count reaches zero. | |
| static inline void iree_async_proactor_release( | |
| iree_async_proactor_t* proactor) { | |
| if (proactor && iree_atomic_ref_count_dec(&proactor->ref_count) == 1) { | |
| proactor->vtable->destroy(proactor); | |
| } | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Queries | |
| //===----------------------------------------------------------------------===// | |
| // Queries backend capabilities. | |
| static inline iree_async_proactor_capabilities_t | |
| iree_async_proactor_query_capabilities(iree_async_proactor_t* proactor) { | |
| return proactor->vtable->query_capabilities(proactor); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Submission | |
| //===----------------------------------------------------------------------===// | |
| // Submits a batch of operations for async execution. | |
| // | |
| // Operations are caller-owned and must remain valid until their final | |
| // callbacks fire. The operation list contents are consumed during this call; | |
| // the list struct itself need not remain valid after return. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | yes | yes | yes | |
| // | |
| // Threading model: | |
| // Thread-safe with respect to poll() and wake(). Multiple threads may | |
| // submit concurrently. However, each individual operation must be owned by | |
| // exactly one thread at submission time (no concurrent access to the same | |
| // operation struct). | |
| // | |
| // Batch semantics: | |
| // All operations in the batch are submitted atomically where possible. | |
| // io_uring: Single io_uring_submit() call. | |
| // IOCP: Individual PostQueuedCompletionStatus calls (no atomic batch). | |
| // kqueue: Single kevent() call with all changes. | |
| // | |
| // Region lifetime: | |
| // Implementations call iree_async_span_retain_region() for each span in | |
| // submitted operations at submit time, and release after the final callback | |
| // fires. This ensures registered memory regions remain valid for the | |
| // operation's in-flight window. For multishot operations, regions are | |
| // retained at submit and released only after the final callback (one | |
| // without IREE_ASYNC_COMPLETION_FLAG_MORE). | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: All operations submitted successfully. | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Submission queue full (retry after poll). | |
| // IREE_STATUS_INVALID_ARGUMENT: Malformed operation in the batch. | |
| static inline iree_status_t iree_async_proactor_submit( | |
| iree_async_proactor_t* proactor, iree_async_operation_list_t operations) { | |
| return proactor->vtable->submit(proactor, operations); | |
| } | |
| // Submits a single operation. Convenience wrapper around submit(). | |
| static inline iree_status_t iree_async_proactor_submit_one( | |
| iree_async_proactor_t* proactor, iree_async_operation_t* operation) { | |
| iree_async_operation_list_t list = {&operation, 1}; | |
| return proactor->vtable->submit(proactor, list); | |
| } | |
| // Drains completions and invokes callbacks. | |
| // | |
| // Blocks until |timeout| is reached or at least one completion is available. | |
| // All callbacks fire synchronously from within this call on the calling thread. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | yes | yes | yes | |
| // | |
| // Threading model: | |
| // Poll must be called from the proactor's poll owner thread. The first thread | |
| // to call poll() becomes the poll owner for the proactor's lifetime. Calling | |
| // poll() from any other thread is undefined behavior and may cause errors | |
| // (e.g., EEXIST on io_uring with DEFER_TASKRUN). This constraint enables | |
| // lock-free fast paths in high-performance backends. | |
| // | |
| // NOT thread-safe with respect to other poll() calls. Thread-safe with | |
| // respect to submit() and wake(), which may be called from any thread. | |
| // Typical pattern: dedicated I/O thread owns the proactor and calls poll() | |
| // in a loop while worker threads call submit(). | |
| // | |
| // Parameters: | |
| // timeout: Maximum time to block. Use iree_timeout_t (not raw duration) to | |
| // avoid drift—absolute deadlines are converted to relative only at the | |
| // syscall boundary. Use iree_make_timeout_ms(0) for non-blocking poll. | |
| // out_completed_count: Number of callbacks invoked (may be NULL). | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: One or more completions processed. | |
| // IREE_STATUS_DEADLINE_EXCEEDED: Timeout expired with no completions | |
| // (not an error—normal for polling loops). | |
| // IREE_STATUS_ABORTED: Proactor is shutting down. | |
| // | |
| // Example: | |
| // while (running) { | |
| // iree_status_t status = iree_async_proactor_poll( | |
| // proactor, iree_make_timeout_ms(100), NULL); | |
| // if (iree_status_is_deadline_exceeded(status)) { | |
| // iree_status_ignore(status); // Normal timeout, continue loop. | |
| // } else if (!iree_status_is_ok(status)) { | |
| // return status; // Actual error. | |
| // } | |
| // } | |
| static inline iree_status_t iree_async_proactor_poll( | |
| iree_async_proactor_t* proactor, iree_timeout_t timeout, | |
| iree_host_size_t* out_completed_count) { | |
| return proactor->vtable->poll(proactor, timeout, out_completed_count); | |
| } | |
| // Wakes a blocked poll() from another thread. | |
| // | |
| // Causes poll() to return immediately with IREE_STATUS_OK and zero completions | |
| // (or whatever completions were already pending). Use this to notify the poll | |
| // thread that new work has been submitted or that shutdown is requested. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | yes | yes | yes | |
| // | |
| // Threading model: | |
| // Fully thread-safe. May be called from any thread, including signal | |
| // handlers (async-signal-safe on POSIX). Idempotent—multiple concurrent | |
| // wake() calls coalesce into a single poll() wakeup. | |
| // | |
| // Implementation: | |
| // io_uring: IORING_OP_NOP or eventfd write. | |
| // IOCP: PostQueuedCompletionStatus with NULL overlapped. | |
| // kqueue: EVFILT_USER trigger. | |
| // generic: eventfd/pipe write or condition variable signal. | |
| static inline void iree_async_proactor_wake(iree_async_proactor_t* proactor) { | |
| proactor->vtable->wake(proactor); | |
| } | |
| // Requests cancellation of a pending operation. | |
| // | |
| // Cancellation is asynchronous: the request is submitted to the kernel and | |
| // the operation's callback will eventually fire with IREE_STATUS_CANCELLED. | |
| // The caller must not access the operation struct until the callback fires. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | 5.5+ | yes | yes | |
| // | |
| // Threading model: | |
| // Thread-safe. May be called from any thread. The cancellation completion | |
| // is delivered via the normal poll() path on the poll thread. | |
| // | |
| // Callback expectations: | |
| // The cancelled operation's callback fires with: | |
| // - status: IREE_STATUS_CANCELLED | |
| // - flags: Does NOT include IREE_ASYNC_COMPLETION_FLAG_MORE | |
| // For multishot operations, cancellation terminates the operation entirely; | |
| // no further completions are delivered after the cancelled callback. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Cancellation request submitted (callback will fire). | |
| // IREE_STATUS_NOT_FOUND: Operation not pending (already completed or | |
| // never submitted). The callback will NOT fire in this case. | |
| // | |
| // Note: Even if cancel() returns OK, the operation may complete successfully | |
| // before the kernel processes the cancellation request. Check the callback | |
| // status to determine the actual outcome. | |
| static inline iree_status_t iree_async_proactor_cancel( | |
| iree_async_proactor_t* proactor, iree_async_operation_t* operation) { | |
| return proactor->vtable->cancel(proactor, operation); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Buffer registration | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Ownership model: | |
| // The proactor ALLOCATES the entry (including any backend-specific | |
| // trailing data) during register_buffer/register_dmabuf. The entry is | |
| // linked into the caller's |state| list and returned via |out_entry|. | |
| // | |
| // The entry holds a reference to its iree_async_region_t and sets a | |
| // cleanup_fn that releases the region reference and frees the entry's | |
| // memory. Cleanup happens in one of two ways: | |
| // - Explicit: caller calls unregister_buffer, which removes the entry | |
| // from |state| and invokes cleanup_fn. | |
| // - Automatic: buffer destruction calls | |
| // iree_async_buffer_registration_state_deinitialize(), which walks the | |
| // list and invokes each entry's cleanup_fn. | |
| // | |
| // The caller MUST NOT free entry memory directly — always go through | |
| // unregister_buffer or state_deinitialize. | |
| // | |
| // The proactor MUST outlive all registrations. If the proactor is | |
| // destroyed before entries are cleaned up, cleanup_fn receives a stale | |
| // proactor pointer (undefined behavior). In practice: unregister or | |
| // destroy all registered buffers before releasing the proactor. | |
| // | |
| // Thread safety: | |
| // Registration/unregistration must be serialized with respect to | |
| // |state| (typically single-threaded during setup). The entry's region | |
| // and backend handles are immutable after registration, so I/O | |
| // operations can read them concurrently from any thread. | |
| // | |
| // See iree/async/types.h for the entry and state type definitions. | |
| // Registers host memory for zero-copy I/O. | |
| // | |
| // Pre-registering buffers with the kernel enables DMA directly to/from | |
| // application memory, eliminating copy overhead. This is the critical path | |
| // for achieving line-rate network throughput and NVMe bandwidth. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.1+ | reg | emul | |
| // | |
| // Registration limits: | |
| // io_uring: Limited by IORING_MAX_FIXED_BUFFERS (typically 32768). | |
| // IOCP: No hard limit; each registration pins pages. | |
| // Others: Emulated; no kernel-side benefit but API is consistent. | |
| // | |
| // Buffer lifetime: | |
| // The memory referenced by |buffer| must remain valid and at a stable | |
| // address for the lifetime of the registration. The proactor does not | |
| // copy or own the memory—it just registers the address range with the | |
| // kernel for DMA access. | |
| // | |
| // Ownership: | |
| // The proactor allocates the entry (including backend-specific trailing | |
| // data), links it into |state|, and returns it via |out_entry|. The | |
| // entry's cleanup_fn handles deallocation when unregistered or when | |
| // the owning buffer is destroyed via state_cleanup(). | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Buffer registered successfully. | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Registration table full. | |
| // IREE_STATUS_INVALID_ARGUMENT: Buffer address not suitably aligned. | |
| // | |
| // See also: iree_async_proactor_unregister_buffer() | |
| static inline iree_status_t iree_async_proactor_register_buffer( | |
| iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_state_t* state, iree_byte_span_t buffer, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_buffer_registration_entry_t** out_entry) { | |
| return proactor->vtable->register_buffer(proactor, state, buffer, | |
| access_flags, out_entry); | |
| } | |
| // Registers device memory exported as a dmabuf for zero-copy I/O. | |
| // | |
| // Enables toll-free GPU↔NIC and GPU↔NVMe data paths: the proactor can | |
| // issue RDMA sends, TCP zero-copy sends (devmem TCP), or file writes | |
| // directly from device memory without staging through host buffers. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // n/a | 5.19+ | n/a | n/a | |
| // | |
| // This is a Linux-specific feature requiring: | |
| // - Kernel 5.19+ with IORING_REGISTER_BUFFERS2 | |
| // - GPU driver that exports dmabuf (AMD, NVIDIA, Intel) | |
| // - Compatible NIC for network zero-copy (Mellanox, Intel E810) | |
| // | |
| // Parameters: | |
| // dmabuf_fd: DMA-BUF file descriptor exported by the GPU driver. | |
| // offset: Byte offset within the dmabuf to start registration. | |
| // length: Number of bytes to register starting from offset. | |
| // access_flags: READ for send paths, WRITE for receive paths. | |
| // | |
| // Ownership: | |
| // Same model as register_buffer: the proactor allocates the entry, sets | |
| // cleanup_fn, and the entry is freed via unregister or state_deinitialize. | |
| // The proactor does NOT take ownership of dmabuf_fd—caller manages its | |
| // lifetime and must keep it open until unregistration. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: dmabuf registered successfully. | |
| // IREE_STATUS_UNAVAILABLE: Backend does not support dmabuf (use | |
| // IREE_ASYNC_PROACTOR_CAPABILITY_DMABUF to check beforehand). | |
| // IREE_STATUS_INVALID_ARGUMENT: Invalid fd or offset/length out of range. | |
| static inline iree_status_t iree_async_proactor_register_dmabuf( | |
| iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_state_t* state, int dmabuf_fd, | |
| uint64_t offset, iree_host_size_t length, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_buffer_registration_entry_t** out_entry) { | |
| return proactor->vtable->register_dmabuf(proactor, state, dmabuf_fd, offset, | |
| length, access_flags, out_entry); | |
| } | |
| // Unregisters a previously registered buffer (host or dmabuf). | |
| // | |
| // Removes |entry| from |state|, invokes entry->cleanup_fn (which releases | |
| // the region reference and frees the entry memory), and releases any | |
| // backend resources (RDMA MR deregistration, io_uring buffer table slot). | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | yes | yes | yes | |
| // | |
| // In-flight safety: | |
| // Must not be called while I/O operations referencing this entry's region | |
| // are in flight. While operations retain the region via span (so the memory | |
| // is safe), the backend may need to deregister the buffer table slot, | |
| // which can corrupt in-flight DMA transfers. | |
| // | |
| // Safe pattern: Cancel all operations using this buffer, wait for their | |
| // cancellation callbacks, then unregister. | |
| // | |
| // Post-call state: | |
| // The entry pointer is invalid after this call. Do not access it. | |
| // The underlying memory (for host buffers) is not freed—only the | |
| // registration is removed. The caller may continue using the memory | |
| // for non-registered I/O or free it. | |
| static inline void iree_async_proactor_unregister_buffer( | |
| iree_async_proactor_t* proactor, | |
| iree_async_buffer_registration_entry_t* entry, | |
| iree_async_buffer_registration_state_t* state) { | |
| proactor->vtable->unregister_buffer(proactor, entry, state); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Slab registration (indexed zero-copy) | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Singleton constraint for the fixed buffer table: | |
| // Only one send-path (READ access) slab registration may be active per | |
| // proactor. This is a backend limitation (io_uring supports a single | |
| // fixed buffer table per ring) exposed as a public API constraint for | |
| // portability. Attempting to register a second READ slab returns | |
| // IREE_STATUS_ALREADY_EXISTS. | |
| // | |
| // Multiple recv-path (WRITE access) registrations are allowed (each | |
| // gets its own provided buffer ring with a unique group ID). | |
| // | |
| // Ownership: | |
| // The caller owns the returned region. The region holds a retained | |
| // reference to the slab. Deregistration happens automatically when the | |
| // region's ref count reaches zero (via the region's destroy_fn callback). | |
| // No explicit unregister call is needed. | |
| // | |
| // Thread safety: | |
| // Slab registration must be serialized with respect to the proactor. | |
| // Typically performed during initialization before starting I/O threads. | |
| // Registers a slab for indexed zero-copy I/O. | |
| // | |
| // Creates a region with backend-specific handles that enable zero-copy | |
| // operations across all buffers in the slab. This is more efficient than | |
| // registering individual buffers when using a slab-based allocation pattern. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // emul | 5.1+ | emul | emul | |
| // | |
| // Singleton constraint (io_uring fixed buffer table): | |
| // Only one send-path (READ access) slab registration may be active per | |
| // proactor. This is a backend limitation (io_uring supports a single | |
| // fixed buffer table per ring) exposed as a public API constraint for | |
| // portability. Attempting to register a second READ slab returns | |
| // IREE_STATUS_ALREADY_EXISTS. | |
| // | |
| // Multiple recv-path (WRITE access) registrations are allowed (each | |
| // gets its own provided buffer ring with a unique group ID). | |
| // | |
| // Backend behavior: | |
| // io_uring (READ): Uses IORING_REGISTER_BUFFERS. SEND_ZC operations | |
| // derive buf_index from span offset at fill time. | |
| // io_uring (WRITE): Creates a provided buffer ring (PBUF_RING) for | |
| // kernel-managed buffer selection in multishot recv. | |
| // Others: Emulated; slab structure tracked but no kernel registration. | |
| // | |
| // Ownership: | |
| // The caller owns the returned region. The region holds a retained | |
| // reference to the slab. Deregistration happens automatically when the | |
| // region's ref count reaches zero (via the region's destroy_fn callback). | |
| // No explicit unregister call is needed. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Slab registered successfully. | |
| // IREE_STATUS_ALREADY_EXISTS: READ slab already registered (io_uring). | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Too many slabs registered. | |
| static inline iree_status_t iree_async_proactor_register_slab( | |
| iree_async_proactor_t* proactor, iree_async_slab_t* slab, | |
| iree_async_buffer_access_flags_t access_flags, | |
| iree_async_region_t** out_region) { | |
| return proactor->vtable->register_slab(proactor, slab, access_flags, | |
| out_region); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Event source registration (persistent handle monitoring) | |
| //===----------------------------------------------------------------------===// | |
| // | |
| // Event sources provide persistent monitoring of external handles (file | |
| // descriptors, RDMA completion queue channels, etc.) with callback-based | |
| // notification. Unlike one-shot poll operations, event sources remain armed | |
| // after each callback and continue monitoring until explicitly unregistered. | |
| // | |
| // Common use cases: | |
| // - RDMA CQ channel fd monitoring (notify on completion queue events) | |
| // - External device fd monitoring (GPUs, accelerators) | |
| // - Integration with external event loops | |
| // | |
| // Ownership model: | |
| // The handle is NOT owned by the event source. The caller retains ownership | |
| // and must ensure the handle outlives the event source registration. The | |
| // proactor allocates internal tracking structures during registration and | |
| // frees them during unregistration. | |
| // | |
| // Thread safety: | |
| // Registration and unregistration must be serialized with respect to | |
| // poll(). Callbacks fire from within poll() on the polling thread. | |
| // Do NOT call unregister_event_source from within the callback. | |
| // Registers an external handle for persistent event monitoring. | |
| // | |
| // The handle is monitored for readability (POLLIN). When the handle becomes | |
| // readable, the callback fires from within poll() on the polling thread. The | |
| // event source remains armed after the callback returns (multishot behavior); | |
| // no re-registration is needed for subsequent events. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // poll | 5.19+ | poll | poll | |
| // | |
| // io_uring implementation uses multishot POLL_ADD for efficient persistent | |
| // monitoring without per-event syscalls. | |
| // | |
| // Parameters: | |
| // handle: The external handle to monitor (must be a valid fd). | |
| // callback: Function to invoke when the handle is signaled. The callback | |
| // receives poll events and should drain the handle (e.g., ibv_poll_cq for | |
| // RDMA CQ channels) and re-arm if needed (e.g., ibv_req_notify_cq). | |
| // out_event_source: Receives the event source handle for later | |
| // unregistration. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Event source registered successfully. | |
| // IREE_STATUS_INVALID_ARGUMENT: Invalid handle or NULL callback. | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Too many event sources registered. | |
| static inline iree_status_t iree_async_proactor_register_event_source( | |
| iree_async_proactor_t* proactor, iree_async_primitive_t handle, | |
| iree_async_event_source_callback_t callback, | |
| iree_async_event_source_t** out_event_source) { | |
| return proactor->vtable->register_event_source(proactor, handle, callback, | |
| out_event_source); | |
| } | |
| // Unregisters an event source and stops monitoring. | |
| // | |
| // After this call returns, the callback will not fire again. The event source | |
| // handle becomes invalid and must not be used after this call. | |
| // | |
| // Must NOT be called from within the event source's callback. If you need to | |
| // unregister from a callback, defer the unregistration to the next poll() | |
| // iteration (e.g., via a flag check in your poll loop). | |
| // | |
| // Thread safety: | |
| // Must be called from the proactor's poll thread (same thread that calls | |
| // poll()). Not thread-safe with respect to poll(). | |
| static inline void iree_async_proactor_unregister_event_source( | |
| iree_async_proactor_t* proactor, iree_async_event_source_t* event_source) { | |
| proactor->vtable->unregister_event_source(proactor, event_source); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Relay registration (event-to-event bridging) | |
| //===----------------------------------------------------------------------===// | |
| // Registers a relay that triggers |sink| when |source| is signaled. | |
| // | |
| // The relay is active immediately after registration. The proactor monitors | |
| // the source and fires the sink from within poll() when triggered. | |
| // | |
| // For PERSISTENT relays, the source is re-armed after each trigger and the | |
| // relay remains active until iree_async_proactor_unregister_relay() is called. | |
| // For one-shot relays (no PERSISTENT flag), the relay auto-cleans up after | |
| // firing once. | |
| // | |
| // Error handling: | |
| // If a persistent relay fails to re-arm (e.g., syscall error), the | |
| // |error_callback| is invoked with the error code. The relay transitions | |
| // to a faulted state and should be unregistered. Pass | |
| // iree_async_relay_error_callback_none() if errors can be ignored. | |
| // | |
| // Availability: | |
| // generic | io_uring | IOCP | kqueue | |
| // yes | yes | yes | yes | |
| // | |
| // io_uring optimizations: | |
| // When LINK chains are supported, certain source/sink combinations can | |
| // execute entirely in kernel space without userspace round-trips: | |
| // - PRIMITIVE → SIGNAL_PRIMITIVE: POLL_ADD → LINK → WRITE | |
| // - PRIMITIVE → SIGNAL_NOTIFICATION (futex): POLL_ADD → LINK → FUTEX_WAKE | |
| // - NOTIFICATION (futex) → SIGNAL_NOTIFICATION (futex): kernel chain | |
| // | |
| // Parameters: | |
| // proactor: The proactor to register with. | |
| // source: Event source specification. | |
| // sink: Event sink specification. | |
| // flags: Behavioral flags (PERSISTENT, OWN_SOURCE_PRIMITIVE, | |
| // ERROR_SENSITIVE). | |
| // error_callback: Called if the relay fails to re-arm. May be _none(). | |
| // out_relay: Receives the relay handle for later unregistration. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Relay registered successfully. | |
| // IREE_STATUS_INVALID_ARGUMENT: Invalid source or sink specification. | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Too many relays or SQ full. | |
| static inline iree_status_t iree_async_proactor_register_relay( | |
| iree_async_proactor_t* proactor, iree_async_relay_source_t source, | |
| iree_async_relay_sink_t sink, iree_async_relay_flags_t flags, | |
| iree_async_relay_error_callback_t error_callback, | |
| iree_async_relay_t** out_relay) { | |
| return proactor->vtable->register_relay(proactor, source, sink, flags, | |
| error_callback, out_relay); | |
| } | |
| // Unregisters a relay and stops monitoring. | |
| // | |
| // After this call returns, the sink will not fire again for this relay. | |
| // If OWN_SOURCE_PRIMITIVE was set, the source fd is closed. | |
| // | |
| // Must NOT be called from within a relay sink's callback (the sink fires | |
| // from poll(), which holds internal state). Defer unregistration if needed. | |
| // | |
| // For persistent relays, this is the only way to stop monitoring. | |
| // For one-shot relays, this can be used to cancel before the relay fires. | |
| // | |
| // Thread safety: | |
| // Must be called from the proactor's poll thread. | |
| static inline void iree_async_proactor_unregister_relay( | |
| iree_async_proactor_t* proactor, iree_async_relay_t* relay) { | |
| if (!relay) return; | |
| proactor->vtable->unregister_relay(proactor, relay); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Cross-proactor messaging | |
| //===----------------------------------------------------------------------===// | |
| // Sets the callback invoked when a message arrives from another proactor. | |
| // | |
| // Cross-proactor messaging enables efficient communication between proactors | |
| // running on different threads. Messages can arrive via backend-specific paths: | |
| // - io_uring MSG_RING: Kernel posts CQE directly to target ring (5.18+). | |
| // - IOCP PostQueuedCompletionStatus: Kernel posts to target port. | |
| // - Software fallback: Pre-allocated message pool + wake (POSIX poll). | |
| // | |
| // All paths deliver messages through this callback during poll(). | |
| // | |
| // Only one callback may be registered at a time; subsequent calls replace the | |
| // previous callback. Pass a callback with NULL fn to disable message delivery. | |
| // | |
| // Must be called before the poll loop starts or from within a poll() callback | |
| // (not thread-safe with poll). | |
| static inline void iree_async_proactor_set_message_callback( | |
| iree_async_proactor_t* proactor, | |
| iree_async_proactor_message_callback_t callback) { | |
| proactor->vtable->set_message_callback(proactor, callback); | |
| } | |
| // Sends a message to a proactor from any thread. | |
| // | |
| // This is a thread-safe, fire-and-forget message send that can be called from | |
| // any context (proactor thread, worker thread, signal handler). The target | |
| // proactor's registered message callback will be invoked during its next | |
| // poll() call. | |
| // | |
| // Unlike the operation-based MESSAGE submit (which supports LINK chains and | |
| // completion callbacks), this function is optimized for simplicity: | |
| // - No completion callback (fire-and-forget semantics). | |
| // - No LINK chain support. | |
| // - Minimal overhead: backend-specific (kernel CQE, pool+wake, etc.). | |
| // | |
| // Use cases: | |
| // - Shutdown signals from main thread to I/O thread. | |
| // - Work injection from worker threads. | |
| // - Cross-thread event notification. | |
| // | |
| // Threading model: | |
| // Fully thread-safe. May be called from any thread, including from within | |
| // completion callbacks. Multiple concurrent sends are safe. | |
| // | |
| // Parameters: | |
| // target: The proactor to receive the message. Must remain valid until the | |
| // message is delivered (i.e., until target's poll() processes it). | |
| // message_data: Arbitrary 64-bit payload delivered to the callback. | |
| // | |
| // Returns: | |
| // IREE_STATUS_OK: Message queued for delivery. | |
| // IREE_STATUS_RESOURCE_EXHAUSTED: Backend-specific resource limit reached. | |
| // Retry after the target proactor's next poll() drains pending messages. | |
| static inline iree_status_t iree_async_proactor_send_message( | |
| iree_async_proactor_t* target, uint64_t message_data) { | |
| return target->vtable->send_message(target, message_data); | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Signal subscription | |
| //===----------------------------------------------------------------------===// | |
| // Returns true if |proactor| supports signal handling on this platform. | |
| // All current backends (POSIX, io_uring, IOCP) support signal handling, | |
| // though with different signal type availability (Windows only supports | |
| // INTERRUPT and TERMINATE; POSIX additionally supports HANGUP, QUIT, USER1, | |
| // USER2). Use this to check if signal-based functionality will actually work | |
| // before relying on it. | |
| static inline bool iree_async_proactor_supports_signals( | |
| iree_async_proactor_t* proactor) { | |
| return proactor->vtable->subscribe_signal != NULL; | |
| } | |
| // Subscribes to |signal| on |proactor|, invoking |callback| each time the | |
| // signal is received. The |out_subscription| handle must eventually be passed | |
| // to iree_async_proactor_unsubscribe_signal() or released implicitly when the | |
| // proactor is destroyed. | |
| // | |
| // Only one proactor per process may own signal subscriptions. The first | |
| // proactor to call this function claims ownership; calls from other proactors | |
| // return IREE_STATUS_FAILED_PRECONDITION. Ownership persists for the lifetime | |
| // of the process (never released). | |
| // | |
| // Multiple subscriptions to the same signal are allowed; all callbacks fire in | |
| // registration order. Callbacks fire from within poll() on the polling thread. | |
| // | |
| // Must be serialized with poll(). Safe to call before the poll loop starts, | |
| // after it exits, or from within poll() callbacks. Not safe to call from | |
| // another thread while poll() is running. | |
| // | |
| // Returns IREE_STATUS_FAILED_PRECONDITION if another proactor owns signals, or | |
| // IREE_STATUS_INVALID_ARGUMENT if |signal| is out of range. If the platform | |
| // does not support signal handling this is a no-op that returns OK with | |
| // |*out_subscription| set to NULL. | |
| static inline iree_status_t iree_async_proactor_subscribe_signal( | |
| iree_async_proactor_t* proactor, iree_async_signal_t signal, | |
| iree_async_signal_callback_t callback, | |
| iree_async_signal_subscription_t** out_subscription) { | |
| if (!proactor->vtable->subscribe_signal) { | |
| *out_subscription = NULL; | |
| return iree_ok_status(); | |
| } | |
| return proactor->vtable->subscribe_signal(proactor, signal, callback, | |
| out_subscription); | |
| } | |
| // Unsubscribes |subscription| from signal delivery on |proactor|. After this | |
| // call the callback will not fire again and the subscription handle becomes | |
| // invalid. | |
| // | |
| // Safe to call with NULL |subscription| (no-op), allowing cleanup code to | |
| // unconditionally unsubscribe without checking whether subscribe succeeded. | |
| // | |
| // Must be serialized with poll(). When called from within a signal callback, | |
| // the unsubscription is deferred until dispatch completes to avoid corrupting | |
| // the subscription list during iteration. | |
| static inline void iree_async_proactor_unsubscribe_signal( | |
| iree_async_proactor_t* proactor, | |
| iree_async_signal_subscription_t* subscription) { | |
| if (!subscription) return; // NULL-safe no-op. | |
| if (proactor->vtable->unsubscribe_signal) { | |
| proactor->vtable->unsubscribe_signal(proactor, subscription); | |
| } | |
| } | |
| //===----------------------------------------------------------------------===// | |
| // Process-wide signal utilities | |
| //===----------------------------------------------------------------------===// | |
| // Blocks signals that will be handled via proactor subscriptions. | |
| // | |
| // Call BEFORE creating any threads so child threads inherit the blocked mask. | |
| // This prevents signals from being delivered with default handlers (which | |
| // terminate the process) before proactor signal handling is set up. | |
| // | |
| // Blocks: SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGUSR1, SIGUSR2. | |
| // On Windows this is a no-op (console events don't use signal masks). | |
| IREE_API_EXPORT iree_status_t iree_async_signal_block_default(void); | |
| // Ignores SIGPIPE globally. | |
| // | |
| // Call once at startup to prevent SIGPIPE from terminating the process when | |
| // writing to a closed socket/pipe. Essential for any network server—the | |
| // correct way to detect broken pipes is via EPIPE return from write(). | |
| // On Windows this is a no-op (no SIGPIPE equivalent). | |
| IREE_API_EXPORT iree_status_t iree_async_signal_ignore_broken_pipe(void); | |
| #ifdef __cplusplus | |
| } // extern "C" | |
| #endif // __cplusplus | |
| #endif // IREE_ASYNC_PROACTOR_H_ |