From 2ce60d1440a5bac3b52a7615451251d938aede31 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 18 Oct 2026 16:35:35 +1300 Subject: [PATCH 1/2] Split socket capabilities into individual operation traits. --- context/design.md | 2 +- context/gaps.md | 2 +- context/implementation.md | 6 +- crates/executor/design.md | 96 ++++++++++-- crates/executor/examples/interface_probe.rs | 25 ++- .../examples/interface_probe/simulated.rs | 49 +++++- .../tests/address_resolution.rs | 1 + .../interface_probe/tests/socket_lifecycle.rs | 35 ++++- .../interface_probe/tests/socket_messages.rs | 36 ++++- .../interface_probe/tests/socket_options.rs | 16 +- crates/executor/examples/socket_messages.rs | 10 +- crates/executor/examples/socket_options.rs | 7 +- crates/executor/examples/socket_shutdown.rs | 6 +- crates/executor/readme.md | 19 ++- crates/executor/src/lib.rs | 7 +- crates/executor/src/scheduler.rs | 51 ++++++ .../executor/src/scheduler/cleanup/tests.rs | 12 +- .../src/scheduler/selector/io_uring.rs | 48 +++++- .../scheduler/selector/io_uring/message.rs | 7 +- .../src/scheduler/selector/readiness.rs | 55 ++++++- .../executor/src/scheduler/socket_accept.rs | 16 ++ crates/executor/src/scheduler/socket_bind.rs | 17 ++ crates/executor/src/scheduler/socket_close.rs | 17 ++ .../executor/src/scheduler/socket_connect.rs | 17 ++ .../src/scheduler/socket_get_option.rs | 41 +++++ .../executor/src/scheduler/socket_listen.rs | 17 ++ .../src/scheduler/socket_local_addr.rs | 10 ++ .../executor/src/scheduler/socket_messages.rs | 29 +--- crates/executor/src/scheduler/socket_open.rs | 18 +++ .../src/scheduler/socket_operation.rs | 23 +++ .../executor/src/scheduler/socket_options.rs | 30 ++-- .../src/scheduler/socket_peer_addr.rs | 10 ++ crates/executor/src/scheduler/socket_read.rs | 35 +++++ .../executor/src/scheduler/socket_recv_msg.rs | 33 ++++ .../src/scheduler/socket_registration.rs | 7 +- .../src/scheduler/socket_scheduler.rs | 147 ++++++------------ .../executor/src/scheduler/socket_send_msg.rs | 26 ++++ .../src/scheduler/socket_set_option.rs | 18 +++ .../executor/src/scheduler/socket_shutdown.rs | 24 +++ crates/executor/src/scheduler/socket_wait.rs | 16 ++ crates/executor/src/scheduler/socket_write.rs | 17 ++ .../src/scheduler/socketry/operations.rs | 111 +++++++++++-- .../scheduler/socketry/operations/tests.rs | 1 + crates/executor/src/scheduler/tokio.rs | 97 ++++++++++-- crates/executor/src/socket_option.rs | 2 +- crates/executor/tests/contracts.rs | 1 + crates/executor/tests/io.rs | 4 + crates/executor/tests/socket_capabilities.rs | 119 ++++++++++++++ crates/executor/tests/socket_lifecycle.rs | 1 + crates/executor/tests/socket_messages.rs | 7 +- crates/executor/tests/socket_options.rs | 5 +- examples/echo/client.rs | 9 +- examples/echo/server.rs | 10 +- readme.md | 6 +- releases.md | 2 + src/lib.rs | 11 +- 56 files changed, 1181 insertions(+), 263 deletions(-) create mode 100644 crates/executor/src/scheduler/socket_accept.rs create mode 100644 crates/executor/src/scheduler/socket_bind.rs create mode 100644 crates/executor/src/scheduler/socket_close.rs create mode 100644 crates/executor/src/scheduler/socket_connect.rs create mode 100644 crates/executor/src/scheduler/socket_get_option.rs create mode 100644 crates/executor/src/scheduler/socket_listen.rs create mode 100644 crates/executor/src/scheduler/socket_local_addr.rs create mode 100644 crates/executor/src/scheduler/socket_open.rs create mode 100644 crates/executor/src/scheduler/socket_operation.rs create mode 100644 crates/executor/src/scheduler/socket_peer_addr.rs create mode 100644 crates/executor/src/scheduler/socket_read.rs create mode 100644 crates/executor/src/scheduler/socket_recv_msg.rs create mode 100644 crates/executor/src/scheduler/socket_send_msg.rs create mode 100644 crates/executor/src/scheduler/socket_set_option.rs create mode 100644 crates/executor/src/scheduler/socket_shutdown.rs create mode 100644 crates/executor/src/scheduler/socket_wait.rs create mode 100644 crates/executor/src/scheduler/socket_write.rs create mode 100644 crates/executor/tests/socket_capabilities.rs diff --git a/context/design.md b/context/design.md index ba1785a..c92f37e 100644 --- a/context/design.md +++ b/context/design.md @@ -139,7 +139,7 @@ The implementations live under `scheduler/selector/`, selected with Cargo featur `FileStat::file_stat` queries an open descriptor and returns an owned associated metadata type. `FileMetadata` defines portable optional accessors for type, size, and access/modification/creation times, allowing native-backed results and simple simulated records. Readiness/Tokio wrap `std::fs::Metadata`; io_uring retains Linux `statx` directly in the request allocation, with native fields exposed on the concrete type. Avoid eager field normalization and additional output allocations. Returning by value keeps the interface simple; copy elimination is an optimization rather than a contract. The file and output survive waiter destruction until backend access ends. Metadata does not change the cursor or order concurrent modifications, and path-based lookup remains separate. -File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. The corresponding `SocketOperation` split is agreed follow-up work; `SocketScheduler` still declares its operations directly. +File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. Socket operation traits share `SocketOperation::Socket`, and `SocketScheduler` is their automatic core aggregate. `SocketMessages` aggregates `SocketSendMsg`/`SocketRecvMsg`, and `SocketOptions` aggregates `SocketGetOption`/`SocketSetOption`. Each individual extension and `SocketRegistration` depend only on `SocketOperation`; consumers explicitly require any additional setup, transfer, or cleanup operations. Import operation traits for concrete method calls and use `::Socket` for qualified resource paths. The Linux `io-uring` feature selects a dedicated ring for socket/file reads and writes, scatter/gather message sends/receives, and `file_sync`, plus descriptor-based `file_stat` using required `Statx` support. `file_truncate` uses the ring when its opcode is available, with a blocking-file fallback on older kernels. Optional `FileAllocate` adds Linux range reservation with `AllocationMode::Extend` or `KeepSize`, using a probed `Fallocate` request or the blocking syscall fallback. Other platforms currently omit that capability. Synchronization, allocation, and native truncation retain the file without a payload buffer and use the same original-completion retirement contract as transfers. Connect, accept, explicit readiness waits and sleep still use async-io. Initialization probes required opcodes and reliable completion overflow; it returns an error if unavailable rather than silently changing implementation. Shutdown cancels and drains original completions. Operation identifiers are never reused, and cancellation completions cannot release original buffers. Unexpected selector failure retains resources whose kernel lifetime cannot be established and panics their waiting operations. Normal cancellation does not retain completed resources. diff --git a/context/gaps.md b/context/gaps.md index 9dc2a75..5517ad2 100644 --- a/context/gaps.md +++ b/context/gaps.md @@ -18,7 +18,7 @@ The executor does not expose a Barrier or require a group/owner argument. Librar ## 3. The I/O boundary is still narrow -`SocketScheduler` uses one associated resource type for unconnected, bound, listening, connected, and datagram sockets. It exposes `socket_open`, `socket_bind`, syscall-like `socket_listen`, `socket_connect`, `socket_accept`, address inspection, reads, writes, and readiness. The initial API supports IPv4/IPv6 and stream/datagram sockets with optional TCP/UDP protocol selection. OS-resource import is a separate `SocketRegistration` capability. Socket close/shutdown and their admission-only variants are implemented; typed option get/set supports address reuse, keepalive, and TCP no-delay; `SocketMessages` implements owned scatter/gather payloads, addresses, peeking, routing flags, and receive truncation; ancillary data and further options remain future work. `FileScheduler` opens associated resources and borrows them for seeking, truncation, synchronization, shared-cursor and positioned I/O. Explicit close/submission now consumes an exclusively owned handle, and optional `FileAllocate` supplies Linux allocation and `FileAdvice` supplies hints with completion/submission forms. `file_stat` returns owned associated metadata with native storage and portable accessors; path-based metadata remains future work. File/socket operations support cancellation and owned buffer views. `Clock` supplies monotonic time and cancellable absolute and relative waits. +`SocketOperation` supplies one associated resource type for unconnected, bound, listening, connected, and datagram sockets. Individual operation traits, aggregated by `SocketScheduler`, expose `socket_open`, `socket_bind`, syscall-like `socket_listen`, `socket_connect`, `socket_accept`, address inspection, reads, writes, and readiness. The initial API supports IPv4/IPv6 and stream/datagram sockets with optional TCP/UDP protocol selection. OS-resource import is a separate `SocketRegistration` capability. Socket close/shutdown and their admission-only variants are implemented; typed option get/set supports address reuse, keepalive, and TCP no-delay; `SocketMessages` implements owned scatter/gather payloads, addresses, peeking, routing flags, and receive truncation; ancillary data and further options remain future work. `FileScheduler` opens associated resources and borrows them for seeking, truncation, synchronization, shared-cursor and positioned I/O. Explicit close/submission now consumes an exclusively owned handle, and optional `FileAllocate` supplies Linux allocation and `FileAdvice` supplies hints with completion/submission forms. `file_stat` returns owned associated metadata with native storage and portable accessors; path-based metadata remains future work. File/socket operations support cancellation and owned buffer views. `Clock` supplies monotonic time and cancellable absolute and relative waits. `Blocking` now adds bounded asynchronous admission for owned synchronous closures, implemented through dedicated native and Tokio pools. `AddressResolution` uses the same bounded pools for system hostname lookup and returns owned address/type/protocol candidates with numeric or named services and native resolver hints; the simulator uses an explicit table. Started system lookups finish independently of cancellation. Existing file fallbacks still use backend pools. Ancillary data, further socket options, randomness, and processes remain outside the portable boundary. Deadline policies can now compose `Clock` and cancellation; higher-level timeout helpers remain separate. Ordinary synchronization uses futures and wakers above scheduler primitives. Expand capabilities as real consumers need them, keeping the application's implementation choice explicit. A broader boundary would also allow more operating-system behavior to be substituted in deterministic tests. diff --git a/context/implementation.md b/context/implementation.md index 3096de8..8041842 100644 --- a/context/implementation.md +++ b/context/implementation.md @@ -58,7 +58,7 @@ This guide describes the current implementation and its boundaries. Read [the de ## I/O and runtime boundaries - `scheduler.rs` re-exports SocketScheduler, FileScheduler, Interest and Clock from `scheduler/socket_scheduler.rs`, `file_scheduler.rs`, `interest.rs` and `clock.rs`. Operations return concrete Send futures; portable consumers receive the required capabilities. -- File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. The corresponding `SocketOperation` split is agreed follow-up work; `SocketScheduler` still declares its operations directly. +- File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. Socket operation traits share `SocketOperation::Socket`, and `SocketScheduler` is their automatic core aggregate. `SocketMessages` aggregates `SocketSendMsg`/`SocketRecvMsg`, and `SocketOptions` aggregates `SocketGetOption`/`SocketSetOption`. Each individual extension and `SocketRegistration` depend only on `SocketOperation`; consumers explicitly require any additional setup, transfer, or cleanup operations. Import operation traits for concrete method calls and use `::Socket` for qualified resource paths. - `FileOperation` declares an associated resource and `FileOpen` opens it with `FileOptions`; native/Tokio choose `Arc` internally. `SocketScheduler` creates IP socket descriptors with explicit bind/listen/connect and address inspection; standard-library resource import belongs to `SocketRegistration`. Blocking truncation, synchronization, shared-cursor, and positioned file helpers live in `scheduler/file_io.rs`. `file_seek` accepts `std::io::SeekFrom` and returns the resulting offset through a cancellable blocking fallback on all current native backends and the Tokio runtime pool; await it and other cursor operations in order. `file_read`/`file_write` advance the OS cursor with no extra per-file state or locking; io\_uring represents current-position requests with `None` internally and encodes them as offset `-1`. Await cursor operations sequentially when ordering matters, and retain buffers through terminal completion as for positioned I/O. - `FileAllocate: FileOperation` optionally reserves space with `AllocationMode::Extend`/`KeepSize`. Native readiness and Tokio implement it on Linux using `fallocate64` on their file pools; io\_uring probes optional `Fallocate` and falls back when absent. Other targets currently omit the implementation. Positive length and a range end within `i64` are required; data and cursor are preserved, and filesystem support errors remain runtime results. No resize/zero-write emulation, unsharing, or durability promise. The simulator reserves vector capacity within its 1 MiB bound. - `FileTruncate::file_truncate` changes the logical length with zero-filled extension, requiring write access and preserving the cursor. Lengths must fit in `i64`. Readiness and Tokio use `set_len` on their file pools; io\_uring probes optional `Ftruncate` support and falls back to the blocking implementation when absent. Buffer-free ring requests share resource retention and cancellation with sync. Await related operations sequentially; truncation provides neither space reservation nor durability. The simulator uses its 1 MiB bound. @@ -114,7 +114,7 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Typed socket options -- `SocketOption` describes an identifier's associated `Value`; `SocketOptions: SocketScheduler` adds lazy cancellable `socket_get_option` and `socket_set_option`. Consumers require each supported option through trait bounds. The core `SocketScheduler` trait is unchanged. +- `SocketOption` describes an identifier's associated `Value`; `SocketGetOption` and `SocketSetOption`, aggregated as `SocketOptions`, add lazy cancellable `socket_get_option` and `socket_set_option`. Consumers require each supported option through trait bounds. The core `SocketScheduler` trait is unchanged. - `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` use boolean values and preserve their OS meanings. Keepalive timing, additional options, and read-only/write-only controls remain separate extensions. Invalid resource protocols or states return OS errors even when the capability exists. - Native mappings live privately in `scheduler/native_socket_option.rs`, using socket2's typed accessors. Readiness and Tokio execute them directly during a poll; io_uring delegates to readiness with its open check. Scheduler handles preserve shutdown and Tokio runtime validation. There is no readiness wait, pool dispatch, raw public ABI, or per-socket locking for these controls. - Cancellation before execution prevents the syscall. Once executed, its result wins cancellation; an unpolled setter has no effect. Await configuration before dependent operations. Address reuse and accepted-socket inheritance vary by OS; configure accepted resources explicitly. @@ -122,7 +122,7 @@ Coverage measures source regions for native and Tokio schedulers on each support ## Message/address I/O -- `SocketMessages: SocketScheduler` adds `socket_sendmsg` and `socket_recvmsg`. `SocketMessage` owns scatter/gather buffers and an optional destination; receive metadata returns copied bytes, sender address when available, and payload/control truncation. Buffer owners return on success, error, or cancellation, and initialized lengths remain unchanged. No `Sync` bound is added to `IoBuf`. +- `SocketSendMsg` and `SocketRecvMsg`, aggregated as `SocketMessages`, add `socket_sendmsg` and `socket_recvmsg`. `SocketMessage` owns scatter/gather buffers and an optional destination; receive metadata returns copied bytes, sender address when available, and payload/control truncation. Buffer owners return on success, error, or cancellation, and initialized lengths remain unchanged. No `Sync` bound is added to `IoBuf`. - `scheduler/message_io.rs` owns native metadata and validates representable lengths. Readiness/Tokio execute nonblocking syscalls; Windows uses vectored Winsock payload/address calls and normalizes message-too-large to truncation. Zero-capacity receives preserve OS differences; use nonempty storage for portable empty-message reception. `peek` and `dont_route` are the initial portable input flags. - `selector/io_uring/message.rs` submits native message opcodes through the existing completion/cancellation machinery. Stable payload, header, address, and vector allocations survive until the original terminal CQE. Descriptor retention ends before completion notification. Close still requires completion of outstanding operations. - Ancillary data is not supported by this capability. Unix reports control truncation; the Windows fallback cannot observe control metadata. Do not silently extend that fallback to ancillary-capable operations. The stream-only simulator has no `SocketMessages` implementation; shared message contracts run against the native backends and Tokio. diff --git a/crates/executor/design.md b/crates/executor/design.md index e82fb2a..50d74e6 100644 --- a/crates/executor/design.md +++ b/crates/executor/design.md @@ -40,9 +40,9 @@ Dedicated acceptors may reduce scheduling interference, and blocking or multisho ## Capability traits and static dispatch -`FileScheduler`, `SocketScheduler`, and `Clock` name scheduler capabilities. `Spawn` provides primitive task dispatch and returns an owning task handle. `Blocking` adds optional execution of owned synchronous closures with bounded asynchronous admission. `AddressResolution` adds hostname lookup independently of `SocketScheduler`. `FileOperation::File` and `SocketScheduler::Socket` are associated resource types chosen by each implementation. Native and Tokio use concrete `FileDescriptor` and `SocketDescriptor` types. +`FileScheduler`, `SocketScheduler`, and `Clock` name scheduler capabilities. `Spawn` provides primitive task dispatch and returns an owning task handle. `Blocking` adds optional execution of owned synchronous closures with bounded asynchronous admission. `AddressResolution` adds hostname lookup independently of `SocketScheduler`. `FileOperation::File` and `SocketOperation::Socket` are associated resource types chosen by each implementation. Native and Tokio use concrete `FileDescriptor` and `SocketDescriptor` types. -Use the `Scheduler` suffix for the broad file and socket interfaces: `FileScheduler` and `SocketScheduler` replace the former `File` and `Socket` trait names. This identifies the scheduler providing those capabilities and distinguishes it from the resource and individual operation traits. `FileScheduler` is already a blanket aggregate; `SocketScheduler` still declares its methods directly until the operation-level socket split. Individual capabilities retain names such as `FileRead`, `Clock`, and `Spawn`. +Use the `Scheduler` suffix for the broad file and socket interfaces: `FileScheduler` and `SocketScheduler` replace the former `File` and `Socket` trait names. This identifies the scheduler providing those capabilities and distinguishes it from the resource and individual operation traits. Both are blanket aggregates of their core operation traits. Individual capabilities retain names such as `FileRead`, `Clock`, and `Spawn`. Prefer generic trait bounds, concrete resource types, and concrete future types. These allow compile-time capability checking, monomorphization, and inlining without requiring a boxed future for each operation. The compiler decides which calls to inline; the interface should make that optimization possible. @@ -50,11 +50,11 @@ Keep core traits small and dependable. Implementing a core trait requires workin Additional capabilities belong in extension traits where their semantics or platform availability differ. A consumer requiring an extension declares that requirement through its trait bounds. An incompatible scheduler then fails at compile time. Implemented examples include `S: FileAllocate` for allocation and `S: SocketOptions` for a particular socket option. -Prefer operation-level capability traits so consumers can require just the operations they use. Keep completion and `_submit` variants of the same operation together, and keep inherently platform-specific types and flags in platform extensions. The file capability split is implemented; the equivalent socket refactor remains follow-up work. +Prefer operation-level capability traits so consumers can require just the operations they use. Keep completion and `_submit` variants of the same operation together, and keep inherently platform-specific types and flags in platform extensions. The file and socket capability splits are implemented. ### Operation-level capabilities -`FileOperation` declares the scheduler's associated `File` resource type and its existing `Send + Sync` bounds. It is implemented on the scheduler, not the descriptor. Every file operation on that scheduler shares this one type. This is a scheduler capability, not a common descriptor hierarchy, and it does not prescribe `Arc`, dynamic dispatch, or a concrete resource representation. The corresponding socket base trait will be named `SocketOperation`; it is not implemented yet. Existing `FileType` and `SocketType` enums retain their classification meanings, and concrete native resources remain `FileDescriptor` and `SocketDescriptor`. +`FileOperation` declares the scheduler's associated `File` resource type and its existing `Send + Sync` bounds. It is implemented on the scheduler, not the descriptor. Every file operation on that scheduler shares this one type. This is a scheduler capability, not a common descriptor hierarchy, and it does not prescribe `Arc`, dynamic dispatch, or a concrete resource representation. `SocketOperation` plays the same role for the associated `Socket` resource type. Existing `FileType` and `SocketType` enums retain their classification meanings, and concrete native resources remain `FileDescriptor` and `SocketDescriptor`. The implemented core file capabilities are `FileOpen`, `FileClose`, `FileStat`, `FileTruncate`, `FileSync`, `FileSeek`, `FileRead`, `FileWrite`, `FileReadAt`, and `FileWriteAt`. Each depends only on `FileOperation`. Optional `FileAdvice` and `FileAllocate` use that same base, without requiring unrelated open, read, write, and close operations. `FileStat` declares the operation-specific `Metadata: FileMetadata` output, so read-only implementations do not need a metadata representation. @@ -75,7 +75,7 @@ async fn read_chunk( This function needs neither file creation nor writing, metadata, or close. A copy loop using existing files can require `S: FileRead + FileWrite`. Partial and simulated backends can implement those capabilities without unrelated methods or `Unsupported` stubs. The owned-buffer, cancellation, resource-retention, and concrete `Send` future contracts are preserved. -`FileClose` keeps `file_close` and `file_close_submit` together, and `FileAdvice` keeps both advice forms together. Each pair describes the same underlying operation with different completion boundaries. Retain this grouping for the socket equivalents. Audit remaining grouped capabilities, including typed socket get/set options, for independent consumer requirements; retain small cohesive groups where their shared contract is useful, such as a clock's instant type and monotonic deadline operations. +`FileClose` keeps `file_close` and `file_close_submit` together, and `FileAdvice` keeps both advice forms together. Each pair describes the same underlying operation with different completion boundaries. `SocketClose` and `SocketShutdown` follow the same grouping. Typed socket get/set options and message send/receive have separate operation traits with optional convenience aggregates. Keep cohesive capabilities such as a clock's instant type and monotonic deadline operations together. `FileScheduler` is a convenience aggregate of all core file operation traits, with a blanket implementation for any scheduler satisfying those bounds. It declares no methods or associated types itself. Optional allocation and advice remain outside the aggregate. Generic `S: FileScheduler` code can use all core methods; narrower consumers select individual capabilities. This uses ordinary supertraits and a blanket implementation, not unstable trait aliases. @@ -83,19 +83,25 @@ These bounds are explicit capability requirements, not inferred effect tracking. Native, Tokio, and simulated implementations use the file operation traits. Shared file conformance tests declare their required setup, operation, and observation capabilities. A minimal read-only implementation exercises a read-only consumer, and compile-fail documentation checks reject missing operations and optional extensions. The copy failure adapter implements only file creation and positioned reads/writes. -This is a source-breaking implementor refactor. Replace the former `impl File` with `impl FileOperation` for `type File`, `impl FileStat` for `type Metadata` and `file_stat`, and an implementation of each supported operation trait. Do not implement the aggregate explicitly. Fully qualified associated paths become `::File` and `::Metadata`; method paths become, for example, `FileRead::file_read`. Concrete callers import the individual operation traits for method lookup; importing only `FileScheduler` does not bring those methods into scope. Generic callers replace `S: File` with `S: FileScheduler` or narrow their bounds. Consumers of `FileAdvice` or `FileAllocate` must now explicitly require any additional operations they use. Socket implementations and consumers rename `Socket` to `SocketScheduler`, including `::Socket`; their operation requirements are unchanged. +This is a source-breaking implementor refactor. Replace the former `impl File` with `impl FileOperation` for `type File`, `impl FileStat` for `type Metadata` and `file_stat`, and an implementation of each supported operation trait. Do not implement the aggregate explicitly. Fully qualified associated paths become `::File` and `::Metadata`; method paths become, for example, `FileRead::file_read`. Concrete callers import the individual operation traits for method lookup; importing only `FileScheduler` does not bring those methods into scope. Generic callers replace `S: File` with `S: FileScheduler` or narrow their bounds. Consumers of `FileAdvice` or `FileAllocate` must now explicitly require any additional operations they use. The socket implementor migration is described below. -### Follow-up: socket operation capabilities +### Socket operation capabilities -Extract `SocketOperation` and split the current `SocketScheduler` operations into capabilities such as `SocketOpen`, `SocketBind`, `SocketListen`, `SocketAccept`, `SocketRead`, and `SocketWrite`. Keep trait names aligned with current method names: single-buffer operations are `socket_read` and `socket_write`, while message operations are `socket_recvmsg` and `socket_sendmsg`. Split `SocketMessages` into receive and send capabilities so a reader need not implement sending. This refactor does not rename the operations or add a new `socket_recv` method. +`SocketOperation` supplies the shared `Socket` associated type. Each core operation trait depends only on that base: `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddr`, `SocketPeerAddr`, `SocketShutdown`, and `SocketClose`. Keep the existing syscall-shaped method names; this split adds no new runtime behavior or allocations. Close and shutdown retain their ordinary and `_submit` forms together. -Make each socket operation and extension depend on `SocketOperation`, and retain `SocketScheduler` as a convenience aggregate. Migrate all backends, exports, examples, and conformance tests together. Include partial-backend and missing-capability checks, and document associated-path and concrete-method-import migration as for files. +`SocketScheduler` is implemented automatically for schedulers providing all core operations. It declares no methods or associated types. Existing-connection protocol code can require only `S: SocketRead + SocketWrite`, and a send loop can require only `S: SocketWrite`, without socket creation, listening, or accepting. The same resource type is shared across these bounds. + +Optional `SocketSendMsg` and `SocketRecvMsg` supply `socket_sendmsg` and `socket_recvmsg`; `SocketMessages` is their blanket aggregate. `SocketGetOption` and `SocketSetOption` independently supply typed option get/set; `SocketOptions` is their blanket aggregate. Each individual capability and `SocketRegistration` depend only on `SocketOperation`, so an extension-only implementation needs no core operations. Message, option, and registration capabilities remain outside `SocketScheduler`. + +Implementors move `type Socket` from `impl SocketScheduler` into `impl SocketOperation` and move each method into its operation trait implementation. Replace explicit `SocketMessages` and `SocketOptions` implementations with the respective send/receive and get/set implementations. Do not implement the three aggregates explicitly. Fully qualified resource paths become `::Socket`; method paths name the operation trait, such as `SocketRead::socket_read`. Concrete callers import individual operation traits; importing only an aggregate no longer exposes methods on a concrete scheduler. Generic `S: SocketScheduler`, `S: SocketMessages`, and `S: SocketOptions` bounds retain their grouped methods. Consumers that previously relied on optional capabilities implying core socket operations must now add every setup, I/O, or cleanup capability they use. + +Native readiness, io_uring, Tokio, and the in-memory simulator implement the split interfaces. Shared conformance tests state their setup and observation requirements. A descriptor-free read/write backend exercises a protocol without other socket capabilities, while positive and compile-fail examples verify extension-only adapters, absent operations, typed options, and the aggregate boundaries. Runtime operation bodies and cancellation, buffer ownership, and cleanup contracts are unchanged. ### Common scheduler dependency and resource creation Generic code takes a scheduler such as `&S`, with bounds describing the portion of the interface it uses. A task group may require `S: Spawn`; a timed primitive may additionally require `Clock`; an HTTP server may require `Spawn + SocketScheduler`. These bounds describe the same chosen scheduler implementation. A named aggregate trait can make a standard bundle convenient once its required capabilities are settled; it should not force a task-only primitive to require file or socket support. The current public `Scheduler` is the concrete native implementation, not an aggregate scheduler trait. -Resource creation belongs inside the corresponding capability. `FileOperation` declares an associated file type, and `FileOpen` returns that type. `SocketScheduler` declares one associated socket type for unconnected, bound, listening, connected, and datagram sockets. Its `socket_open`, `socket_bind`, `socket_listen`, and `socket_connect` operations follow the corresponding system-call boundaries; `socket_accept` returns another value of that same type. `socket_listen` takes an existing socket and backlog and leaves bind behavior to the operating system, including implicit binding where supported. Generic consumers use these associated resources rather than creating a `std::fs::File` or `std::net::TcpListener` themselves. +Resource creation belongs inside the corresponding capability. `FileOperation` declares an associated file type, and `FileOpen` returns that type. `SocketOperation` declares one associated socket type for unconnected, bound, listening, connected, and datagram sockets. Its `socket_open`, `socket_bind`, `socket_listen`, and `socket_connect` operations follow the corresponding system-call boundaries; `socket_accept` returns another value of that same type. `socket_listen` takes an existing socket and backlog and leaves bind behavior to the operating system, including implicit binding where supported. Generic consumers use these associated resources rather than creating a `std::fs::File` or `std::net::TcpListener` themselves. The initial socket arguments select IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Address parameters use `std::net::SocketAddr`. `SocketOptions` adds typed configuration without extending the required `SocketScheduler` surface. `SocketMessages` adds owned scatter/gather payloads, IP addresses, and portable flags. Non-IP families and ancillary data remain future extensions. File and socket descriptors remain distinct; no common descriptor trait is required. Existing std TCP resources can still enter through the optional `SocketRegistration` interoperability trait. @@ -105,7 +111,7 @@ These resource contracts are implemented. `FileOpen::file_open` accepts public ` ### Typed socket options -`SocketOption` associates an option identifier with its `Value` type; `SocketOptions: SocketScheduler` supplies `socket_get_option(socket, option, cancellation)` and `socket_set_option(socket, option, value, cancellation)`. Both return concrete `Send` futures with `OperationResult` outputs. The getter returns `O::Value`; the setter returns `()`. Generic code requires exactly the options it uses: +`SocketOption` associates an option identifier with its `Value` type; `SocketGetOption` and `SocketSetOption` supply `socket_get_option(socket, option, cancellation)` and `socket_set_option(socket, option, value, cancellation)`. Both return concrete `Send` futures with `OperationResult` outputs. The getter returns `O::Value`; the setter returns `()`. Generic code requires exactly the options it uses: ```rust,ignore async fn configure + SocketOptions>( @@ -129,7 +135,7 @@ Shared tests exercise round trips, cancellation, unpolled setters, configuration ### Message/address I/O -`SocketMessages: SocketScheduler` is an optional capability; existing implementations of `SocketScheduler` gain no required methods. The native readiness selectors, native scheduler/handle, io_uring selector, and Tokio scheduler/handle implement it. The stream-only example simulator does not currently implement this capability or UDP. +`SocketMessages` aggregates the optional `SocketSendMsg` and `SocketRecvMsg` capabilities; existing implementations of `SocketScheduler` gain no required methods. The native readiness selectors, native scheduler/handle, io_uring selector, and Tokio scheduler/handle implement it. The stream-only example simulator does not currently implement this capability or UDP. ```rust,ignore let message = SocketMessage { @@ -567,9 +573,11 @@ pub trait FileScheduler: { } -pub trait SocketScheduler: Send + Sync { +pub trait SocketOperation: Send + Sync { type Socket: Send + Sync; +} +pub trait SocketOpen: SocketOperation { fn socket_open( &self, domain: SocketDomain, @@ -577,51 +585,70 @@ pub trait SocketScheduler: Send + Sync { protocol: Option, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketBind: SocketOperation { fn socket_bind( &self, socket: &Self::Socket, address: SocketAddr, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketListen: SocketOperation { fn socket_listen( &self, socket: &Self::Socket, backlog: i32, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketLocalAddr: SocketOperation { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result; +} + +pub trait SocketPeerAddr: SocketOperation { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result; +} +pub trait SocketConnect: SocketOperation { fn socket_connect( &self, socket: &Self::Socket, address: SocketAddr, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketAccept: SocketOperation { fn socket_accept( &self, socket: &Self::Socket, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketRead: SocketOperation { fn socket_read( &self, socket: &Self::Socket, buffer: B, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketWrite: SocketOperation { fn socket_write( &self, socket: &Self::Socket, buffer: B, cancellation: &Cancellation, ) -> impl Future> + Send; +} +pub trait SocketWait: SocketOperation { fn socket_wait( &self, socket: &Self::Socket, @@ -630,6 +657,43 @@ pub trait SocketScheduler: Send + Sync { ) -> impl Future> + Send; } +pub trait SocketShutdown: SocketOperation { + fn socket_shutdown( + &self, + socket: &Self::Socket, + how: Shutdown, + ) -> impl Future> + Send; + fn socket_shutdown_submit( + &self, + socket: &Self::Socket, + how: Shutdown, + ) -> impl Future> + Send; +} + +pub trait SocketClose: SocketOperation { + fn socket_close(&self, socket: Self::Socket) -> impl Future> + Send; + fn socket_close_submit( + &self, + socket: Self::Socket, + ) -> impl Future> + Send; +} + +pub trait SocketScheduler: + SocketClose + + SocketShutdown + + SocketOpen + + SocketBind + + SocketListen + + SocketLocalAddr + + SocketPeerAddr + + SocketConnect + + SocketAccept + + SocketRead + + SocketWrite + + SocketWait +{ +} + pub trait Clock: Send + Sync { fn now(&self) -> Instant; @@ -697,7 +761,7 @@ Dropping an in-flight io_uring operation requests cancellation, but its file and File close initially uses a bounded blocking cleanup pool for native and Tokio schedulers, including the io_uring scheduler. Both variants use the same path. This pool reuses the internal blocking-pool implementation but has separate workers from arbitrary `spawn_blocking` closures, so shutdown draining does not depend on a user closure that might itself destroy the scheduler. Both pools start lazily and each uses the configured `BlockingOptions` limits. Accepted close jobs survive waiter and scheduler destruction. Native shutdown/destruction outside future and blocking-pool workers waits for cleanup; native destruction on either kind of worker only requests shutdown. Explicit synchronous shutdown panics on either kind of worker to avoid waiting for its own completion. Tokio destruction requests shutdown, and its awaited `shutdown` drains cleanup. Standalone io_uring selectors own a cleanup pool drained by their shutdown; standalone readiness selectors use a process-wide bounded fallback, like their other file operations. The simulator completes close immediately. `_submit` execution errors are currently discarded; use ordinary close for diagnostics. Native close/shutdown SQEs and CQE suppression remain later optimizations. -`SocketScheduler::socket_shutdown` takes `std::net::Shutdown::{Read, Write, Both}` and borrows the descriptor. Await preceding writes before shutting down `Write`; the other direction can remain usable for the response. Repeated shutdown and I/O on disabled directions retain OS-specific outcomes. `socket_close` consumes the descriptor and rejects retained backend use before admission; it never coordinates or interrupts other operations. Deregistration and close run together on the cleanup worker. Deregistration errors are execution results where the backend exposes them; Tokio's Unix `AsyncFd::into_inner()` discards them internally. The ordinary future reports available execution errors; `_submit` discards them. +`SocketShutdown::socket_shutdown` takes `std::net::Shutdown::{Read, Write, Both}` and borrows the descriptor. Await preceding writes before shutting down `Write`; the other direction can remain usable for the response. Repeated shutdown and I/O on disabled directions retain OS-specific outcomes. `socket_close` consumes the descriptor and rejects retained backend use before admission; it never coordinates or interrupts other operations. Deregistration and close run together on the cleanup worker. Deregistration errors are execution results where the backend exposes them; Tokio's Unix `AsyncFd::into_inner()` discards them internally. The ordinary future reports available execution errors; `_submit` discards them. Native and Tokio socket descriptors privately share their persistent registration through `Arc`. Ordinary readiness operations borrow it without cloning; io_uring and accepted shutdown jobs retain references until completion. Cleanup releases those references before notifying completion, allowing an immediately following close. `_submit` alone cannot establish this ordering: use ordinary shutdown when subsequent explicit close depends on its completion. The in-memory backend completes immediately while preserving admission-only error reporting for `_submit`. @@ -720,7 +784,7 @@ Keep `_submit` exceptional rather than adding a submission-only variant to every A pool receive returns a selected filled lease, rather than accepting an already-selected buffer: ```rust -pub trait PoolReceive: SocketScheduler { +pub trait PoolReceive: SocketOperation { type Pool: Send + Sync; type Lease: IoBufMut; @@ -1106,7 +1170,7 @@ The simulator has bounded in-memory file sizes and unbounded byte queues. It doe | Cancellation | `Cancellation`, `Cancelled`, `defer_cancel`, and cancellation-aware file/socket operations implemented. Application shutdown is cooperative. | | Signal notifications | `Notify`, `Signals`, and optional Unix registration guards implemented for native and Tokio; each guard owns its dispatch task. | | Runtime lifetime | Application waits are explicit. Runtime shutdown abandons futures; backend retention protects submitted I/O. Accepted file-close and socket-close/shutdown submissions survive abandonment and are drained by graceful shutdown. | -| Capabilities | Public `FileScheduler`, `SocketScheduler`, `SocketOptions`, `SocketMessages`, `SocketRegistration`, `AddressResolution`, `Clock`, `Spawn`, and `Blocking`; associated resources and initial file/socket creation implemented. File operation traits share `FileOperation`; `FileScheduler` is a convenience aggregate. The equivalent `SocketOperation` split remains follow-up work. | +| Capabilities | Public `FileScheduler`, `SocketScheduler`, `SocketOptions`, `SocketMessages`, `SocketRegistration`, `AddressResolution`, `Clock`, `Spawn`, and `Blocking`; associated resources and initial file/socket creation implemented. File operation traits share `FileOperation`; `FileScheduler` is a convenience aggregate. Socket operation traits share `SocketOperation`; `SocketScheduler`, `SocketMessages`, and `SocketOptions` are convenience aggregates. | | Blocking execution | Dedicated native/Tokio pools, configurable worker and admission limits, queued cancellation, and owning completion handles implemented. Started closures finish independently; graceful completion requires awaiting them. | | Linked operations | Prepared chains and `Linked` remain proposed; native linking, whole-chain blocking jobs, and readiness sequencing are not implemented. | | File operations | `file_open`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at` implemented with owned buffers. Optional `FileAllocate` implemented on Linux with `Extend` and `KeepSize`; optional `FileAdvice` supplies six hints with completion/submission forms. `file_close`/`file_close_submit` implemented with exclusive ownership and bounded cleanup. `file_stat` returns associated native-backed metadata; path-based metadata remains separate. | diff --git a/crates/executor/examples/interface_probe.rs b/crates/executor/examples/interface_probe.rs index 3c49c7f..137f4ef 100644 --- a/crates/executor/examples/interface_probe.rs +++ b/crates/executor/examples/interface_probe.rs @@ -12,6 +12,10 @@ use socketry_executor::{ FileReadAt, FileScheduler, FileWriteAt, IoBuf, OperationResult, SocketDomain, SocketProtocol, SocketScheduler, SocketType, }; +use socketry_executor::{ + SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, SocketRead, + SocketWrite, +}; use std::{io, path::Path}; static NEVER: Cancellation = Cancellation::never(); @@ -63,7 +67,7 @@ async fn copy( } } -async fn send_all( +async fn send_all( scheduler: &S, socket: &S::Socket, mut buffer: Vec, @@ -85,7 +89,10 @@ async fn send_all( } /// Serve one connection until peer EOF, echoing each received prefix. -async fn serve(scheduler: &S, listener: &S::Socket) -> OperationResult { +async fn serve( + scheduler: &S, + listener: &S::Socket, +) -> OperationResult { let (socket, _) = scheduler.socket_accept(listener, &NEVER).await?; let mut total = 0; loop { @@ -102,7 +109,19 @@ async fn serve(scheduler: &S, listener: &S::Socket) -> Opera } } -async fn exchange(scheduler: &S) -> OperationResult<()> { +async fn exchange< + S: SocketOpen + + SocketBind + + SocketListen + + SocketLocalAddr + + SocketConnect + + SocketAccept + + SocketRead + + SocketWrite + + AddressResolution, +>( + scheduler: &S, +) -> OperationResult<()> { let address = "127.0.0.1:0".parse().unwrap(); let listener = scheduler .socket_open( diff --git a/crates/executor/examples/interface_probe/simulated.rs b/crates/executor/examples/interface_probe/simulated.rs index 6192c5a..f718c15 100644 --- a/crates/executor/examples/interface_probe/simulated.rs +++ b/crates/executor/examples/interface_probe/simulated.rs @@ -3,7 +3,7 @@ //! In-memory files and duplex byte streams. No OS resources or native runtime. //! This is an interface probe, not a model of TCP scheduling or durability. -use super::{Cancellation, FileOptions, OperationResult, SocketScheduler}; +use super::{Cancellation, FileOptions, OperationResult}; use event_listener::Event; use socketry_executor::{AllocationMode, BufferResult, FileAllocate, SyncMode}; use socketry_executor::{ @@ -11,6 +11,11 @@ use socketry_executor::{ FileTruncate, FileWrite, FileWriteAt, }; use socketry_executor::{Interest, IoBuf, IoBufMut, SocketDomain, SocketProtocol, SocketType}; +use socketry_executor::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, + SocketWrite, +}; use std::collections::{HashMap, VecDeque}; use std::io::{self, SeekFrom}; use std::net::SocketAddr; @@ -174,7 +179,7 @@ impl Scheduler { } } -impl socketry_executor::SocketOptions for Scheduler { +impl socketry_executor::SocketGetOption for Scheduler { async fn socket_get_option( &self, socket: &Self::Socket, @@ -184,6 +189,9 @@ impl socketry_executor::SocketOptions for cancellation.check()?; Ok(socket.options.lock().unwrap().reuse_address) } +} + +impl socketry_executor::SocketSetOption for Scheduler { async fn socket_set_option( &self, socket: &Self::Socket, @@ -197,7 +205,7 @@ impl socketry_executor::SocketOptions for } } -impl socketry_executor::SocketOptions for Scheduler { +impl socketry_executor::SocketGetOption for Scheduler { async fn socket_get_option( &self, socket: &Self::Socket, @@ -207,6 +215,9 @@ impl socketry_executor::SocketOptions for Sc cancellation.check()?; Ok(socket.options.lock().unwrap().keepalive) } +} + +impl socketry_executor::SocketSetOption for Scheduler { async fn socket_set_option( &self, socket: &Self::Socket, @@ -220,7 +231,7 @@ impl socketry_executor::SocketOptions for Sc } } -impl socketry_executor::SocketOptions for Scheduler { +impl socketry_executor::SocketGetOption for Scheduler { async fn socket_get_option( &self, socket: &Self::Socket, @@ -230,6 +241,9 @@ impl socketry_executor::SocketOptions for Schedul cancellation.check()?; Ok(socket.options.lock().unwrap().no_delay) } +} + +impl socketry_executor::SocketSetOption for Scheduler { async fn socket_set_option( &self, socket: &Self::Socket, @@ -500,8 +514,11 @@ impl FileWriteAt for Scheduler { } } -impl SocketScheduler for Scheduler { +impl SocketOperation for Scheduler { type Socket = SocketDescriptor; +} + +impl SocketClose for Scheduler { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { drop(socket); Ok(()) @@ -510,7 +527,9 @@ impl SocketScheduler for Scheduler { async fn socket_close_submit(&self, socket: Self::Socket) -> io::Result<()> { self.socket_close(socket).await } +} +impl SocketShutdown for Scheduler { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -545,7 +564,9 @@ impl SocketScheduler for Scheduler { let _ = self.socket_shutdown(socket, how).await; Ok(()) } +} +impl SocketOpen for Scheduler { async fn socket_open( &self, domain: SocketDomain, @@ -575,7 +596,9 @@ impl SocketScheduler for Scheduler { }), }) } +} +impl SocketBind for Scheduler { async fn socket_bind( &self, socket: &SocketDescriptor, @@ -604,7 +627,9 @@ impl SocketScheduler for Scheduler { *reservation = Some(binding); Ok(()) } +} +impl SocketListen for Scheduler { async fn socket_listen( &self, socket: &SocketDescriptor, @@ -648,7 +673,9 @@ impl SocketScheduler for Scheduler { }; Ok(()) } +} +impl SocketLocalAddr for Scheduler { fn socket_local_addr(&self, socket: &SocketDescriptor) -> io::Result { match &*socket.state.lock().unwrap() { SocketState::Open { local, .. } => Ok(*local), @@ -656,14 +683,18 @@ impl SocketScheduler for Scheduler { SocketState::Connected(connection) => Ok(connection.local), } } +} +impl SocketPeerAddr for Scheduler { fn socket_peer_addr(&self, socket: &SocketDescriptor) -> io::Result { match &*socket.state.lock().unwrap() { SocketState::Connected(connection) => Ok(connection.peer), _ => Err(io::Error::from(io::ErrorKind::NotConnected)), } } +} +impl SocketConnect for Scheduler { async fn socket_connect( &self, socket: &SocketDescriptor, @@ -730,7 +761,9 @@ impl SocketScheduler for Scheduler { }); Ok(()) } +} +impl SocketAccept for Scheduler { async fn socket_accept( &self, socket: &SocketDescriptor, @@ -751,7 +784,9 @@ impl SocketScheduler for Scheduler { wait(ready, cancellation).await?; } } +} +impl SocketWait for Scheduler { async fn socket_wait( &self, socket: &SocketDescriptor, @@ -795,7 +830,9 @@ impl SocketScheduler for Scheduler { } } } +} +impl SocketRead for Scheduler { async fn socket_read( &self, socket: &SocketDescriptor, @@ -839,7 +876,9 @@ impl SocketScheduler for Scheduler { } } } +} +impl SocketWrite for Scheduler { async fn socket_write( &self, socket: &SocketDescriptor, diff --git a/crates/executor/examples/interface_probe/tests/address_resolution.rs b/crates/executor/examples/interface_probe/tests/address_resolution.rs index bb037f3..92ebb5d 100644 --- a/crates/executor/examples/interface_probe/tests/address_resolution.rs +++ b/crates/executor/examples/interface_probe/tests/address_resolution.rs @@ -5,6 +5,7 @@ use socketry_executor::{ AddressHints, AddressInfo, AddressResolution, AddressService, Cancellation, OperationError, SocketDomain, SocketProtocol, SocketType, }; + use std::{io, net::SocketAddr}; pub async fn check(scheduler: &impl AddressResolution) { diff --git a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs index 40423c8..bc11b03 100644 --- a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs +++ b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs @@ -1,10 +1,14 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use socketry_executor::{Cancellation, OperationError, SocketDomain, SocketScheduler, SocketType}; +use socketry_executor::{Cancellation, OperationError, SocketDomain, SocketType}; +use socketry_executor::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketRead, SocketShutdown, SocketWrite, +}; use std::net::Shutdown; -async fn open(scheduler: &S) -> S::Socket { +async fn open(scheduler: &S) -> S::Socket { scheduler .socket_open( SocketDomain::Ipv4, @@ -16,7 +20,17 @@ async fn open(scheduler: &S) -> S::Socket { .unwrap() } -async fn pair(scheduler: &S) -> (S::Socket, S::Socket) { +async fn pair< + S: SocketOpen + + SocketBind + + SocketListen + + SocketConnect + + SocketAccept + + SocketLocalAddr + + SocketClose, +>( + scheduler: &S, +) -> (S::Socket, S::Socket) { let never = Cancellation::never(); let listener = open(scheduler).await; scheduler @@ -41,7 +55,20 @@ async fn pair(scheduler: &S) -> (S::Socket, S::Socket) { (client, server) } -pub async fn check(scheduler: &S) { +pub async fn check< + S: SocketOpen + + SocketBind + + SocketListen + + SocketConnect + + SocketAccept + + SocketLocalAddr + + SocketClose + + SocketRead + + SocketWrite + + SocketShutdown, +>( + scheduler: &S, +) { let never = Cancellation::never(); let (client, server) = pair(scheduler).await; // An unpolled shutdown must have no effect; the following write still succeeds. diff --git a/crates/executor/examples/interface_probe/tests/socket_messages.rs b/crates/executor/examples/interface_probe/tests/socket_messages.rs index 64fe8e9..f200a38 100644 --- a/crates/executor/examples/interface_probe/tests/socket_messages.rs +++ b/crates/executor/examples/interface_probe/tests/socket_messages.rs @@ -5,7 +5,11 @@ use socketry_executor::{ Cancellation, IoBuf, IoBufMut, OperationError, SocketDomain, SocketMessage, SocketMessages, - SocketRecvFlags, SocketScheduler, SocketSendFlags, SocketType, + SocketRecvFlags, SocketSendFlags, SocketType, +}; +use socketry_executor::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketShutdown, }; use std::{ future::{Future, poll_fn}, @@ -15,7 +19,7 @@ use std::{ const NEVER: Cancellation = Cancellation::never(); -async fn bound( +async fn bound( scheduler: &S, address: SocketAddr, kind: SocketType, @@ -40,7 +44,19 @@ async fn pending(future: std::pin::Pin<&mut impl Future>) { .await; } -pub async fn check(scheduler: &S) { +pub async fn check< + S: SocketMessages + + SocketOpen + + SocketBind + + SocketConnect + + SocketLocalAddr + + SocketClose + + SocketListen + + SocketAccept + + SocketShutdown, +>( + scheduler: &S, +) { for local in ["127.0.0.1:0", "[::1]:0"] { let receiver = bound(scheduler, local.parse().unwrap(), SocketType::Datagram).await; let sender = bound(scheduler, local.parse().unwrap(), SocketType::Datagram).await; @@ -244,7 +260,19 @@ pub async fn check(scheduler: &S) { stream(scheduler).await; } -async fn stream(scheduler: &S) { +async fn stream< + S: SocketMessages + + SocketOpen + + SocketBind + + SocketConnect + + SocketLocalAddr + + SocketClose + + SocketListen + + SocketAccept + + SocketShutdown, +>( + scheduler: &S, +) { let listener = bound( scheduler, "127.0.0.1:0".parse().unwrap(), diff --git a/crates/executor/examples/interface_probe/tests/socket_options.rs b/crates/executor/examples/interface_probe/tests/socket_options.rs index dc95066..a18aad5 100644 --- a/crates/executor/examples/interface_probe/tests/socket_options.rs +++ b/crates/executor/examples/interface_probe/tests/socket_options.rs @@ -3,7 +3,10 @@ use socketry_executor::{ Cancellation, OperationError, SocketDomain, SocketKeepalive, SocketOption, SocketOptions, - SocketReuseAddress, SocketScheduler, SocketType, TcpNoDelay, + SocketReuseAddress, SocketType, TcpNoDelay, +}; +use socketry_executor::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, }; async fn round_trip(scheduler: &S, socket: &S::Socket, option: O) @@ -50,7 +53,7 @@ where ); } -async fn open(scheduler: &S) -> S::Socket { +async fn open(scheduler: &S) -> S::Socket { scheduler .socket_open( SocketDomain::Ipv4, @@ -66,7 +69,14 @@ pub async fn check(scheduler: &S) where S: SocketOptions + SocketOptions - + SocketOptions, + + SocketOptions + + SocketOpen + + SocketBind + + SocketListen + + SocketAccept + + SocketConnect + + SocketLocalAddr + + SocketClose, { let never = Cancellation::never(); let listener = open(scheduler).await; diff --git a/crates/executor/examples/socket_messages.rs b/crates/executor/examples/socket_messages.rs index ea369e2..414bab6 100644 --- a/crates/executor/examples/socket_messages.rs +++ b/crates/executor/examples/socket_messages.rs @@ -6,7 +6,15 @@ use std::io; #[cfg(any(feature = "native", feature = "tokio"))] -async fn exchange(scheduler: &S) -> io::Result<()> { +async fn exchange< + S: socketry_executor::SocketMessages + + socketry_executor::SocketOpen + + socketry_executor::SocketBind + + socketry_executor::SocketLocalAddr + + socketry_executor::SocketClose, +>( + scheduler: &S, +) -> io::Result<()> { use socketry_executor::{ Cancellation, SocketDomain, SocketMessage, SocketRecvFlags, SocketSendFlags, SocketType, }; diff --git a/crates/executor/examples/socket_options.rs b/crates/executor/examples/socket_options.rs index d1b9250..158d3fb 100644 --- a/crates/executor/examples/socket_options.rs +++ b/crates/executor/examples/socket_options.rs @@ -8,7 +8,12 @@ use std::io; #[cfg(any(feature = "native", feature = "tokio"))] async fn listener(scheduler: &S) -> io::Result<()> where - S: socketry_executor::SocketOptions, + S: socketry_executor::SocketOptions + + socketry_executor::SocketOpen + + socketry_executor::SocketBind + + socketry_executor::SocketListen + + socketry_executor::SocketLocalAddr + + socketry_executor::SocketClose, { use socketry_executor::{Cancellation, SocketDomain, SocketReuseAddress, SocketType}; diff --git a/crates/executor/examples/socket_shutdown.rs b/crates/executor/examples/socket_shutdown.rs index 928fa12..0e417a8 100644 --- a/crates/executor/examples/socket_shutdown.rs +++ b/crates/executor/examples/socket_shutdown.rs @@ -5,12 +5,14 @@ #[cfg(any(feature = "native", feature = "tokio"))] use socketry_executor::{Cancellation, IoBuf, SocketDomain, SocketScheduler, SocketType}; +#[cfg(any(feature = "native", feature = "tokio"))] +use socketry_executor::{SocketRead, SocketWrite}; use std::io; #[cfg(any(feature = "native", feature = "tokio"))] use std::net::Shutdown; #[cfg(any(feature = "native", feature = "tokio"))] -async fn send_all( +async fn send_all( scheduler: &S, socket: &S::Socket, mut buffer: Vec, @@ -33,7 +35,7 @@ async fn send_all( } #[cfg(any(feature = "native", feature = "tokio"))] -async fn receive_all( +async fn receive_all( scheduler: &S, socket: &S::Socket, cancellation: &Cancellation, diff --git a/crates/executor/readme.md b/crates/executor/readme.md index 71e7f59..e97a81e 100644 --- a/crates/executor/readme.md +++ b/crates/executor/readme.md @@ -191,7 +191,11 @@ File capabilities are split by operation: `FileOperation` selects the associated When migrating an implementation, move `type File` from the former `impl File` into `impl FileOperation`, move `type Metadata` and `file_stat` into `impl FileStat`, and move each remaining method into its operation trait implementation. Do not implement `FileScheduler` explicitly. Fully qualified paths become `::File`, `::Metadata`, and operation paths such as `FileRead::file_read`. For method calls on concrete schedulers, import the relevant operation traits; importing `FileScheduler` alone is insufficient. Generic functions bounded by `S: FileScheduler` still see every core method. Bounds on `FileAdvice` or `FileAllocate` now require explicit additional capabilities when the function also opens, reads, or closes files. -The broad file and socket traits are named `FileScheduler` and `SocketScheduler`, replacing `File` and `Socket`. Update aggregate bounds and imports accordingly. Socket implementations use `impl SocketScheduler` and `::Socket`; the operation-level socket split remains follow-up work, so this trait still declares its methods directly. Individual operation traits and resource names are unchanged. +Socket capabilities are split by operation too. `SocketOperation` selects the shared associated resource, and `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddr`, `SocketPeerAddr`, `SocketShutdown`, and `SocketClose` supply core operations. `SocketScheduler` is their automatic aggregate. Existing-connection protocols can require just `SocketRead + SocketWrite`. + +Optional message I/O uses `SocketSendMsg` and `SocketRecvMsg`, with `SocketMessages` as their automatic aggregate. Options use `SocketGetOption` and `SocketSetOption`, with `SocketOptions` as their automatic aggregate. Each individual capability and `SocketRegistration` depend only on `SocketOperation`. An implementation may support only receiving messages or inspecting an option, without opening or writing sockets. + +To migrate, move `type Socket` into `impl SocketOperation` and methods into their operation trait implementations. Replace explicit implementations of `SocketScheduler`, `SocketMessages`, and `SocketOptions`; the aggregates now follow automatically. Fully qualified resource paths become `::Socket`, and methods use paths such as `SocketRead::socket_read`. Concrete callers import individual operation traits; importing an aggregate alone is insufficient. Generic aggregate bounds retain their grouped operations. Add explicit setup, I/O, and cleanup bounds where optional capabilities previously implied them. The broad names `FileScheduler` and `SocketScheduler` replace the former `File` and `Socket` names. The public `scheduler` module contains portable `SocketScheduler`, `FileScheduler`, and `Clock` traits, the Socketry implementation in `socketry.rs`, the optional Tokio adapter in `tokio.rs`, and native implementations under `selector/`. @@ -200,7 +204,7 @@ The public `scheduler` module contains portable `SocketScheduler`, `FileSchedule - `tokio` adds an adapter using a supplied `tokio::runtime::Handle`. It uses Tokio `AsyncFd` on Unix. Windows uses async-io's process-wide reactor for generic socket readiness because Tokio has no matching generic socket wrapper; this fallback is not driven by Tokio's I/O driver. The adapter does not construct or drive a runtime. Enable that runtime's I/O and time facilities for the operations that use it. - With default features disabled, the executor and public contracts still build. Enabling only `tokio` omits the native blocking and io\_uring backends; async-io remains a dependency for Tokio's generic Windows socket-readiness fallback. -`Scheduler` and `SchedulerHandle` implement the traits. `file_open` returns `FileOperation::File`; `socket_open` returns the backend's single associated socket resource, `SocketScheduler::Socket`. Call `socket_bind` and then `socket_listen(socket, backlog, cancellation)` to listen, or `socket_connect(socket, address, cancellation)` to connect an opened socket. `socket_accept` returns another resource of the same type. Native and Tokio use non-`Clone` `SocketDescriptor` values. Importing existing standard-library TCP resources uses the separate optional `SocketRegistration` trait. Returned operation futures are Send. Registrations remain with their original reactor as tasks migrate; they are not re-created per poll. Tokio resources passed to an adapter for another runtime return InvalidInput. +`Scheduler` and `SchedulerHandle` implement the traits. `file_open` returns `FileOperation::File`; `socket_open` returns the backend's single associated socket resource, `SocketOperation::Socket`. Call `socket_bind` and then `socket_listen(socket, backlog, cancellation)` to listen, or `socket_connect(socket, address, cancellation)` to connect an opened socket. `socket_accept` returns another resource of the same type. Native and Tokio use non-`Clone` `SocketDescriptor` values. Importing existing standard-library TCP resources uses the separate optional `SocketRegistration` trait. Returned operation futures are Send. Registrations remain with their original reactor as tasks migrate; they are not re-created per poll. Tokio resources passed to an adapter for another runtime return InvalidInput. The initial API selects IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Connected datagram reads and writes preserve message boundaries. The higher-level `SocketScheduler` trait deliberately does not provide protocol buffering: line, frame, and message semantics belong in protocol adapters built above it. Generic static dispatch is the default, with dynamic dispatch available to consumers that need runtime selection or type erasure. @@ -352,22 +356,21 @@ The [commented half-close example](examples/socket_shutdown.rs) demonstrates a r cargo run -p socketry-executor --example socket_shutdown --features tokio ``` -External `SocketScheduler` implementations must provide `socket_shutdown`, `socket_shutdown_submit`, `socket_close`, and `socket_close_submit`. +`SocketShutdown` provides `socket_shutdown` and `socket_shutdown_submit`; `SocketClose` provides `socket_close` and `socket_close_submit`. Both extend `SocketOperation` and are included in `SocketScheduler`. ### Typed socket options -Require `SocketOptions` for each option your generic code needs. `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` are typed identifiers with boolean values, implemented by native and Tokio schedulers/handles and the native selectors. This is an extension of `SocketScheduler`; existing implementations need not support every option. +Require `SocketGetOption` or `SocketSetOption` for each option operation your generic code needs, or `SocketOptions` for both. `SocketReuseAddress`, `SocketKeepalive`, and `TcpNoDelay` are typed identifiers with boolean values, implemented by native and Tokio schedulers/handles and the native selectors. These operations extend `SocketOperation` independently of the core aggregate; implementations need not support every option or both operations. ```rust,no_run -use socketry_executor::{Cancellation, OperationResult, SocketOptions, TcpNoDelay}; +use socketry_executor::{Cancellation, OperationResult, SocketSetOption, TcpNoDelay}; -async fn configure>( +async fn configure>( scheduler: &S, socket: &S::Socket, cancellation: &Cancellation, ) -> OperationResult<()> { scheduler.socket_set_option(socket, TcpNoDelay, true, cancellation).await?; - assert!(scheduler.socket_get_option(socket, TcpNoDelay, cancellation).await?); Ok(()) } ``` @@ -376,7 +379,7 @@ Both operations are lazy and cancellable before execution. Await setters before Run the [commented listener configuration example](examples/socket_options.rs) with `cargo run -p socketry-executor --example socket_options --features tokio`. -The optional `SocketMessages` capability provides `socket_sendmsg`/`socket_recvmsg` with owned scatter/gather buffers, IP addresses, portable flags, and receive truncation metadata. See the [commented UDP message example](examples/socket_messages.rs). Ancillary data remains a separate extension. +The optional `SocketSendMsg` and `SocketRecvMsg` capabilities provide `socket_sendmsg` and `socket_recvmsg`; `SocketMessages` aggregates both with owned scatter/gather buffers, IP addresses, portable flags, and receive truncation metadata. See the [commented UDP message example](examples/socket_messages.rs). Ancillary data remains a separate extension. ### Cancellation and shutdown diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs index 5c9e37c..3082535 100644 --- a/crates/executor/src/lib.rs +++ b/crates/executor/src/lib.rs @@ -79,8 +79,11 @@ pub use operation_error::{OperationError, OperationResult}; pub use scheduler::{ AddressResolution, BufferResult, Clock, FileAdvice, FileAllocate, FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, - FileWrite, FileWriteAt, Interest, Scheduler, SchedulerHandle, Signals, SocketMessages, - SocketOptions, SocketRegistration, SocketScheduler, + FileWrite, FileWriteAt, Interest, Scheduler, SchedulerHandle, Signals, SocketAccept, + SocketBind, SocketClose, SocketConnect, SocketGetOption, SocketListen, SocketLocalAddr, + SocketMessages, SocketOpen, SocketOperation, SocketOptions, SocketPeerAddr, SocketRead, + SocketRecvMsg, SocketRegistration, SocketScheduler, SocketSendMsg, SocketSetOption, + SocketShutdown, SocketWait, SocketWrite, }; #[cfg(all(unix, feature = "signals"))] pub use signal_hook::consts::signal; diff --git a/crates/executor/src/scheduler.rs b/crates/executor/src/scheduler.rs index 117e6e8..99a7b61 100644 --- a/crates/executor/src/scheduler.rs +++ b/crates/executor/src/scheduler.rs @@ -40,6 +40,57 @@ pub use socket_registration::SocketRegistration; mod interest; pub use interest::Interest; +mod socket_operation; +pub use socket_operation::SocketOperation; + +mod socket_close; +pub use socket_close::SocketClose; + +mod socket_shutdown; +pub use socket_shutdown::SocketShutdown; + +mod socket_open; +pub use socket_open::SocketOpen; + +mod socket_bind; +pub use socket_bind::SocketBind; + +mod socket_listen; +pub use socket_listen::SocketListen; + +mod socket_local_addr; +pub use socket_local_addr::SocketLocalAddr; + +mod socket_peer_addr; +pub use socket_peer_addr::SocketPeerAddr; + +mod socket_connect; +pub use socket_connect::SocketConnect; + +mod socket_accept; +pub use socket_accept::SocketAccept; + +mod socket_read; +pub use socket_read::SocketRead; + +mod socket_write; +pub use socket_write::SocketWrite; + +mod socket_wait; +pub use socket_wait::SocketWait; + +mod socket_send_msg; +pub use socket_send_msg::SocketSendMsg; + +mod socket_recv_msg; +pub use socket_recv_msg::SocketRecvMsg; + +mod socket_get_option; +pub use socket_get_option::SocketGetOption; + +mod socket_set_option; +pub use socket_set_option::SocketSetOption; + mod socket_scheduler; pub use socket_scheduler::SocketScheduler; diff --git a/crates/executor/src/scheduler/cleanup/tests.rs b/crates/executor/src/scheduler/cleanup/tests.rs index 23ec19a..8706799 100644 --- a/crates/executor/src/scheduler/cleanup/tests.rs +++ b/crates/executor/src/scheduler/cleanup/tests.rs @@ -2,7 +2,7 @@ // Copyright, 2026, by Samuel Williams. use super::*; -use crate::SocketRegistration; +use crate::{SocketClose, SocketRegistration, SocketShutdown, SocketWrite}; use futures_lite::future::poll_once; use std::{ io::Read, @@ -22,7 +22,7 @@ fn pair(scheduler: &S) -> (S::Socket, TcpStream) { } // A blocked worker makes admission and execution observably different. Each caller supplies a pool with one worker and capacity two. -pub(crate) async fn accepted_socket_jobs( +pub(crate) async fn accepted_socket_jobs( scheduler: &S, pool: &BlockingPool, mode: u8, @@ -77,7 +77,9 @@ pub(crate) async fn accepted_socket_jobs( assert_eq!(peer.read(&mut [0]).unwrap(), 0); } -pub(crate) async fn saturated_socket_admission( +pub(crate) async fn saturated_socket_admission< + S: SocketRegistration + SocketClose + SocketShutdown + SocketWrite, +>( scheduler: &S, pool: &BlockingPool, ) { @@ -109,7 +111,9 @@ pub(crate) async fn saturated_socket_admission( scheduler.socket_close(socket).await.unwrap(); } -pub(crate) async fn closed_socket_admission( +pub(crate) async fn closed_socket_admission< + S: SocketRegistration + SocketClose + SocketShutdown, +>( scheduler: &S, pool: &BlockingPool, ) { diff --git a/crates/executor/src/scheduler/selector/io_uring.rs b/crates/executor/src/scheduler/selector/io_uring.rs index f4c4fae..f59d355 100644 --- a/crates/executor/src/scheduler/selector/io_uring.rs +++ b/crates/executor/src/scheduler/selector/io_uring.rs @@ -15,13 +15,18 @@ //! //! Allocation and truncation use native requests when their opcodes are available, otherwise the blocking file fallback preserves the same public contract. File advice uses bounded cleanup workers so accepted jobs survive ring shutdown; native advice SQEs remain future work. use super::readiness::{self, SocketDescriptor}; -use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; +use crate::scheduler::{BufferResult, Clock, Interest}; use crate::{AllocationMode, FileAllocate, FileDescriptor, SyncMode}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; use crate::{ FileClose, FileOpen, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, FileWrite, FileWriteAt, }; +use crate::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, + SocketWrite, +}; use event_listener::{Event as CompletionEvent, Listener as _}; use futures_channel::oneshot; use io_uring::{IoUring, opcode, squeue, types}; @@ -671,7 +676,7 @@ fn operation_result(result: io::Result, cancellation: &Cancellation) -> Op result => result.map_err(Into::into), } } -impl crate::SocketOptions +impl crate::SocketGetOption for Selector { async fn socket_get_option( @@ -685,7 +690,11 @@ impl crate::Socke .socket_get_option(socket, option, cancellation) .await } +} +impl crate::SocketSetOption + for Selector +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -710,9 +719,11 @@ impl SocketRegistration for Selector { readiness::Selector.register_listener(listener) } } -impl SocketScheduler for Selector { +impl SocketOperation for Selector { type Socket = SocketDescriptor; +} +impl SocketClose for Selector { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.check_open()?; crate::scheduler::cleanup::run(&self.controller.cleanup, socket.into_close()).await @@ -722,7 +733,9 @@ impl SocketScheduler for Selector { self.check_open()?; crate::scheduler::cleanup::submit_only(&self.controller.cleanup, socket.into_close()).await } +} +impl SocketShutdown for Selector { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -748,7 +761,9 @@ impl SocketScheduler for Selector { }) .await } +} +impl SocketOpen for Selector { async fn socket_open( &self, domain: crate::SocketDomain, @@ -761,6 +776,9 @@ impl SocketScheduler for Selector { .socket_open(domain, kind, protocol, cancellation) .await } +} + +impl SocketBind for Selector { async fn socket_bind( &self, socket: &Self::Socket, @@ -772,6 +790,9 @@ impl SocketScheduler for Selector { .socket_bind(socket, address, cancellation) .await } +} + +impl SocketListen for Selector { async fn socket_listen( &self, socket: &Self::Socket, @@ -783,14 +804,23 @@ impl SocketScheduler for Selector { .socket_listen(socket, backlog, cancellation) .await } +} + +impl SocketLocalAddr for Selector { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { self.check_open()?; readiness::Selector.socket_local_addr(socket) } +} + +impl SocketPeerAddr for Selector { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { self.check_open()?; readiness::Selector.socket_peer_addr(socket) } +} + +impl SocketConnect for Selector { async fn socket_connect( &self, socket: &Self::Socket, @@ -802,6 +832,9 @@ impl SocketScheduler for Selector { .socket_connect(socket, address, cancellation) .await } +} + +impl SocketAccept for Selector { async fn socket_accept( &self, socket: &Self::Socket, @@ -812,6 +845,9 @@ impl SocketScheduler for Selector { .socket_accept(socket, cancellation) .await } +} + +impl SocketRead for Selector { async fn socket_read( &self, socket: &SocketDescriptor, @@ -840,6 +876,9 @@ impl SocketScheduler for Selector { .await; (operation_result(result, cancellation), buffer) } +} + +impl SocketWrite for Selector { async fn socket_write( &self, socket: &SocketDescriptor, @@ -868,6 +907,9 @@ impl SocketScheduler for Selector { .await; (operation_result(result, cancellation), buffer) } +} + +impl SocketWait for Selector { async fn socket_wait( &self, socket: &SocketDescriptor, diff --git a/crates/executor/src/scheduler/selector/io_uring/message.rs b/crates/executor/src/scheduler/selector/io_uring/message.rs index 695ce10..a137e91 100644 --- a/crates/executor/src/scheduler/selector/io_uring/message.rs +++ b/crates/executor/src/scheduler/selector/io_uring/message.rs @@ -3,7 +3,8 @@ use super::*; use crate::scheduler::message_io::{self, Message}; -use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; +use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; +use crate::{SocketRecvMsg, SocketSendMsg, SocketWait}; #[derive(Clone, Copy)] enum MessageOperation { @@ -119,7 +120,7 @@ impl Selector { } } -impl SocketMessages for Selector { +impl SocketSendMsg for Selector { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -158,7 +159,9 @@ impl SocketMessages for Selector { }, ) } +} +impl SocketRecvMsg for Selector { async fn socket_recvmsg( &self, socket: &Self::Socket, diff --git a/crates/executor/src/scheduler/selector/readiness.rs b/crates/executor/src/scheduler/selector/readiness.rs index 758a2c1..8add98b 100644 --- a/crates/executor/src/scheduler/selector/readiness.rs +++ b/crates/executor/src/scheduler/selector/readiness.rs @@ -9,7 +9,7 @@ //! coroutine stacks and may be polled on different workers. use crate::scheduler::file_io::{read_at, write_at}; use crate::scheduler::operation::cancellable; -use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; +use crate::scheduler::{BufferResult, Clock, Interest}; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; @@ -18,7 +18,12 @@ use crate::{ FileTruncate, FileWrite, FileWriteAt, }; use crate::{FileDescriptor, SyncMode}; -use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; +use crate::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketShutdown, SocketWait, SocketWrite, +}; +use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use std::io::{self, SeekFrom}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::Path; @@ -61,7 +66,7 @@ impl Selector { } } -impl crate::SocketOptions +impl crate::SocketGetOption for Selector { async fn socket_get_option( @@ -73,7 +78,11 @@ impl crate::Socke cancellation.check()?; Ok(option.get(socket.get_ref())?) } +} +impl crate::SocketSetOption + for Selector +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -95,9 +104,11 @@ impl SocketRegistration for Selector { } } -impl SocketScheduler for Selector { +impl SocketOperation for Selector { type Socket = SocketDescriptor; +} +impl SocketClose for Selector { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { crate::scheduler::cleanup::run( crate::scheduler::cleanup::standalone_pool(), @@ -113,7 +124,9 @@ impl SocketScheduler for Selector { ) .await } +} +impl SocketShutdown for Selector { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -138,7 +151,9 @@ impl SocketScheduler for Selector { ) .await } +} +impl SocketOpen for Selector { async fn socket_open( &self, domain: crate::SocketDomain, @@ -155,6 +170,9 @@ impl SocketScheduler for Selector { SocketDescriptor::new, ) } +} + +impl SocketBind for Selector { async fn socket_bind( &self, socket: &SocketDescriptor, @@ -164,6 +182,9 @@ impl SocketScheduler for Selector { cancellation.check()?; Ok(socket.get_ref().bind(&address.into())?) } +} + +impl SocketListen for Selector { async fn socket_listen( &self, socket: &SocketDescriptor, @@ -173,12 +194,21 @@ impl SocketScheduler for Selector { cancellation.check()?; Ok(socket.get_ref().listen(backlog)?) } +} + +impl SocketLocalAddr for Selector { fn socket_local_addr(&self, socket: &SocketDescriptor) -> io::Result { socket.local_addr() } +} + +impl SocketPeerAddr for Selector { fn socket_peer_addr(&self, socket: &SocketDescriptor) -> io::Result { socket.peer_addr() } +} + +impl SocketConnect for Selector { async fn socket_connect( &self, socket: &SocketDescriptor, @@ -198,6 +228,9 @@ impl SocketScheduler for Selector { ) .await } +} + +impl SocketAccept for Selector { async fn socket_accept( &self, socket: &SocketDescriptor, @@ -211,6 +244,9 @@ impl SocketScheduler for Selector { ) .await } +} + +impl SocketRead for Selector { async fn socket_read( &self, socket: &SocketDescriptor, @@ -226,6 +262,9 @@ impl SocketScheduler for Selector { .await; (result, buffer) } +} + +impl SocketWrite for Selector { async fn socket_write( &self, socket: &SocketDescriptor, @@ -242,6 +281,9 @@ impl SocketScheduler for Selector { .await; (result, buffer) } +} + +impl SocketWait for Selector { async fn socket_wait( &self, socket: &SocketDescriptor, @@ -568,7 +610,7 @@ impl Clock for Selector { #[cfg(test)] mod tests; -impl SocketMessages for Selector { +impl SocketSendMsg for Selector { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -602,6 +644,9 @@ impl SocketMessages for Selector { }, ) } +} + +impl SocketRecvMsg for Selector { async fn socket_recvmsg( &self, socket: &Self::Socket, diff --git a/crates/executor/src/scheduler/socket_accept.rs b/crates/executor/src/scheduler/socket_accept.rs new file mode 100644 index 0000000..91802cc --- /dev/null +++ b/crates/executor/src/scheduler/socket_accept.rs @@ -0,0 +1,16 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult}; +use std::{future::Future, net::SocketAddr}; + +/// Accept one connection from a listening socket. +pub trait SocketAccept: SocketOperation { + /// Accept one connection from a listening socket, returning an owned nonblocking socket and its peer address. + fn socket_accept( + &self, + socket: &Self::Socket, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_bind.rs b/crates/executor/src/scheduler/socket_bind.rs new file mode 100644 index 0000000..5786f7b --- /dev/null +++ b/crates/executor/src/scheduler/socket_bind.rs @@ -0,0 +1,17 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult}; +use std::{future::Future, net::SocketAddr}; + +/// Bind an existing socket to a local IP address. +pub trait SocketBind: SocketOperation { + /// Bind an existing socket to a local address. Port zero requests an OS-selected port, available through `socket_local_addr`. + fn socket_bind( + &self, + socket: &Self::Socket, + address: SocketAddr, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_close.rs b/crates/executor/src/scheduler/socket_close.rs new file mode 100644 index 0000000..2250e87 --- /dev/null +++ b/crates/executor/src/scheduler/socket_close.rs @@ -0,0 +1,17 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use std::{future::Future, io}; + +/// Consume and close an exclusively owned socket through the scheduler. +pub trait SocketClose: SocketOperation { + /// Consume and close an exclusively owned socket, awaiting bounded admission and OS completion. Finish all other operations first: close never cancels or joins them, and retained backend use may panic. Dropping an unpolled close releases the owned descriptor through its ordinary destructor. + fn socket_close(&self, socket: Self::Socket) -> impl Future> + Send; + + /// Consume the socket and await bounded cleanup admission only. Accepted close jobs survive waiter abandonment and are drained during graceful scheduler shutdown. Execution errors are discarded; use `socket_close` to observe them. The same exclusive-ownership requirement applies. + fn socket_close_submit( + &self, + socket: Self::Socket, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_connect.rs b/crates/executor/src/scheduler/socket_connect.rs new file mode 100644 index 0000000..f08ebc9 --- /dev/null +++ b/crates/executor/src/scheduler/socket_connect.rs @@ -0,0 +1,17 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult}; +use std::{future::Future, net::SocketAddr}; + +/// Connect an existing socket to a peer IP address. +pub trait SocketConnect: SocketOperation { + /// Connect an existing socket, preserving any prior local binding or configuration. A cancelled connect can still complete in the kernel; it does not make the descriptor reusable for another connection attempt. + fn socket_connect( + &self, + socket: &Self::Socket, + address: SocketAddr, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_get_option.rs b/crates/executor/src/scheduler/socket_get_option.rs new file mode 100644 index 0000000..ca4039f --- /dev/null +++ b/crates/executor/src/scheduler/socket_get_option.rs @@ -0,0 +1,41 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult, SocketOption}; +use std::future::Future; + +/// Read a typed socket option. See [`SocketOptions`](crate::SocketOptions) for the shared option contract; the opposite operation is not required. +/// +/// A getter-only adapter needs neither a setter nor any core socket operations: +/// +/// ``` +/// use socketry_executor::{Cancellation, OperationResult, SocketGetOption, SocketOperation, SocketOption}; +/// struct Inspector(S); +/// impl SocketOperation for Inspector { +/// type Socket = S::Socket; +/// } +/// impl, O: SocketOption> SocketGetOption for Inspector { +/// async fn socket_get_option(&self, socket: &Self::Socket, option: O, cancellation: &Cancellation) -> OperationResult { +/// self.0.socket_get_option(socket, option, cancellation).await +/// } +/// } +/// ``` +/// +/// Reading an option does not grant permission to set it: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, SocketGetOption, SocketSetOption, TcpNoDelay}; +/// async fn configure>(scheduler: &S, socket: &S::Socket) { +/// scheduler.socket_set_option(socket, TcpNoDelay, true, &Cancellation::never()).await; +/// } +/// ``` +pub trait SocketGetOption: SocketOperation { + /// Query the option's current value, preserving OS errors. + fn socket_get_option( + &self, + socket: &Self::Socket, + option: O, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_listen.rs b/crates/executor/src/scheduler/socket_listen.rs new file mode 100644 index 0000000..8b71d05 --- /dev/null +++ b/crates/executor/src/scheduler/socket_listen.rs @@ -0,0 +1,17 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult}; +use std::future::Future; + +/// Put an existing socket into listening mode. +pub trait SocketListen: SocketOperation { + /// Put an existing socket into listening mode using the supplied backlog. This does not create or explicitly bind the socket. OS-specific implicit binding, backlog limits, and repeated-listen behavior are preserved. + fn socket_listen( + &self, + socket: &Self::Socket, + backlog: i32, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_local_addr.rs b/crates/executor/src/scheduler/socket_local_addr.rs new file mode 100644 index 0000000..d5bb080 --- /dev/null +++ b/crates/executor/src/scheduler/socket_local_addr.rs @@ -0,0 +1,10 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use std::{io, net::SocketAddr}; + +/// Inspect the local IP address of a socket. +pub trait SocketLocalAddr: SocketOperation { + fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result; +} diff --git a/crates/executor/src/scheduler/socket_messages.rs b/crates/executor/src/scheduler/socket_messages.rs index 1c1919d..9ccda65 100644 --- a/crates/executor/src/scheduler/socket_messages.rs +++ b/crates/executor/src/scheduler/socket_messages.rs @@ -1,13 +1,9 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use crate::{ - Cancellation, IoBuf, IoBufMut, OperationResult, SocketMessage, SocketRecvFlags, - SocketRecvResult, SocketScheduler, SocketSendFlags, -}; -use std::future::Future; +use super::{SocketRecvMsg, SocketSendMsg}; -/// Scatter/gather message I/O with IP addresses. Generic consumers require this capability separately from `SocketScheduler`. +/// Scatter/gather message I/O with IP addresses. The individual `SocketSendMsg` and `SocketRecvMsg` capabilities share this contract. This aggregate is automatically implemented for schedulers supporting both; neither capability requires the core `SocketScheduler` operations. /// /// Operations are lazy, check cancellation before I/O, and return the original buffer owners on every result. Cancellation can race successful progress; success wins. Completion backends retain payload and native metadata until the original operation's terminal completion, including after waiter abandonment. Cancel and await outstanding operations before explicit close. /// @@ -16,26 +12,11 @@ use std::future::Future; /// This capability carries payload and IP addresses only. It cannot send ancillary data and supplies no receive control storage. Unix reports `MSG_CTRUNC` through the result; the Windows payload/address fallback cannot observe control metadata. Ancillary data requires a separate capability rather than silent emulation. Unsupported flags are not part of this portable interface. /// A socket backend must explicitly implement message support: /// ```compile_fail -/// use socketry_executor::{Cancellation, SocketScheduler, SocketMessages, SocketRecvFlags}; +/// use socketry_executor::{Cancellation, SocketScheduler, SocketRecvMsg, SocketRecvFlags}; /// async fn receive(scheduler: &S, socket: &S::Socket) { /// scheduler.socket_recvmsg(socket, vec![vec![0; 1024]], SocketRecvFlags::default(), &Cancellation::never()).await; /// } /// ``` -pub trait SocketMessages: SocketScheduler { - fn socket_sendmsg( - &self, - socket: &Self::Socket, - message: SocketMessage, - flags: SocketSendFlags, - cancellation: &Cancellation, - ) -> impl Future, SocketMessage)> + Send; +pub trait SocketMessages: SocketSendMsg + SocketRecvMsg {} - /// Receive one message into owned buffers. `WSAEMSGSIZE` on Windows is represented as a successful truncated receive, matching Unix; other OS errors are preserved. A zero-byte datagram is a message, whereas a zero-byte stream receive with nonempty capacity normally indicates EOF. - fn socket_recvmsg( - &self, - socket: &Self::Socket, - buffers: Vec, - flags: SocketRecvFlags, - cancellation: &Cancellation, - ) -> impl Future, Vec)> + Send; -} +impl SocketMessages for S where S: SocketSendMsg + SocketRecvMsg {} diff --git a/crates/executor/src/scheduler/socket_open.rs b/crates/executor/src/scheduler/socket_open.rs new file mode 100644 index 0000000..cd78a6f --- /dev/null +++ b/crates/executor/src/scheduler/socket_open.rs @@ -0,0 +1,18 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult, SocketDomain, SocketProtocol, SocketType}; +use std::future::Future; + +/// Create an unbound IP socket through the scheduler. +pub trait SocketOpen: SocketOperation { + /// Open a nonblocking socket without binding, listening, or connecting. `None` selects the OS default protocol for the requested domain and type. + fn socket_open( + &self, + domain: SocketDomain, + kind: SocketType, + protocol: Option, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_operation.rs b/crates/executor/src/scheduler/socket_operation.rs new file mode 100644 index 0000000..11de4ce --- /dev/null +++ b/crates/executor/src/scheduler/socket_operation.rs @@ -0,0 +1,23 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +/// Shared resource type for socket operations provided by a scheduler. Implement this trait on the scheduler; all socket capabilities use this one associated type. No concrete OS descriptor or shared-ownership wrapper is required. +/// +/// IP socket operations through the chosen scheduler. Each backend supplies one descriptor type for unconnected, listening, and connected sockets. Operations have the same state requirements as their system calls; invalid transitions report OS errors. +/// +/// Creation, binding, listening, and connecting are separate operations. All operation futures are lazy. Pre-cancellation prevents a syscall; successful progress wins a cancellation race. Cancelling or abandoning a pending connect does not roll back kernel connection state: discard that socket before trying another connection. Completion backends retain owned buffers until the original operation completes. +/// +/// File and socket resources remain distinct. There is no common descriptor trait, protocol buffering, or automatic close coordination. Message/address I/O belongs to the optional `SocketSendMsg` and `SocketRecvMsg` capabilities. Non-IP families remain an extension. +/// +/// Selecting a socket resource type does not supply reads: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, SocketOperation, SocketRead}; +/// async fn read(scheduler: &S, socket: &S::Socket) { +/// scheduler.socket_read(socket, vec![0; 16], &Cancellation::never()).await; +/// } +/// ``` +pub trait SocketOperation: Send + Sync { + /// The socket resource accepted by this scheduler's socket operations. + type Socket: Send + Sync; +} diff --git a/crates/executor/src/scheduler/socket_options.rs b/crates/executor/src/scheduler/socket_options.rs index 0aff066..844129f 100644 --- a/crates/executor/src/scheduler/socket_options.rs +++ b/crates/executor/src/scheduler/socket_options.rs @@ -1,12 +1,12 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use crate::{Cancellation, OperationResult, SocketOption, SocketScheduler}; -use std::future::Future; +use super::{SocketGetOption, SocketSetOption}; +use crate::SocketOption; -/// Typed get/set capability for one socket option. Generic consumers require only the options they use, such as `S: SocketOptions`. +/// Convenience aggregate of `SocketGetOption` and `SocketSetOption`, automatically implemented for schedulers providing both operations for an option. Generic consumers require only the options they use, such as `S: SocketOptions`. /// -/// An absent implementation is a compile-time error. Socket type, protocol, state, and OS policy can still reject an operation at runtime; preserve those errors. Options retain their OS semantics, including address reuse and inheritance on accepted sockets. Read-only, write-only, or differently shaped controls can use separate extension traits. +/// An absent implementation is a compile-time error. Socket type, protocol, state, and OS policy can still reject an operation at runtime; preserve those errors. Options retain their OS semantics, including address reuse and inheritance on accepted sockets. Read-only and write-only controls implement only their respective operation trait. Differently shaped controls can use separate extension traits. /// /// Both futures are lazy and check cancellation before the syscall. Once executed, the syscall's result wins concurrent cancellation; dropping an unpolled setter changes nothing. Await setters before operations that depend on them. There is no extra serialization between concurrent option operations, and get-followed-by-set is not atomic. /// @@ -19,26 +19,14 @@ use std::future::Future; /// ``` /// Merely supporting sockets does not imply support for every option: /// ```compile_fail -/// use socketry_executor::{Cancellation, SocketScheduler, SocketOptions, TcpNoDelay}; +/// use socketry_executor::{Cancellation, SocketScheduler, SocketSetOption, TcpNoDelay}; /// async fn configure(scheduler: &S, socket: &S::Socket) { /// scheduler.socket_set_option(socket, TcpNoDelay, true, &Cancellation::never()).await; /// } /// ``` -pub trait SocketOptions: SocketScheduler { - /// Query the option's current value, preserving OS errors. - fn socket_get_option( - &self, - socket: &Self::Socket, - option: O, - cancellation: &Cancellation, - ) -> impl Future> + Send; +pub trait SocketOptions: SocketGetOption + SocketSetOption {} - /// Set the option and wait for syscall completion. This borrows the descriptor and does not change its ownership. - fn socket_set_option( - &self, - socket: &Self::Socket, - option: O, - value: O::Value, - cancellation: &Cancellation, - ) -> impl Future> + Send; +impl SocketOptions for S where + S: SocketGetOption + SocketSetOption +{ } diff --git a/crates/executor/src/scheduler/socket_peer_addr.rs b/crates/executor/src/scheduler/socket_peer_addr.rs new file mode 100644 index 0000000..3e14086 --- /dev/null +++ b/crates/executor/src/scheduler/socket_peer_addr.rs @@ -0,0 +1,10 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use std::{io, net::SocketAddr}; + +/// Inspect the connected peer address of a socket. +pub trait SocketPeerAddr: SocketOperation { + fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result; +} diff --git a/crates/executor/src/scheduler/socket_read.rs b/crates/executor/src/scheduler/socket_read.rs new file mode 100644 index 0000000..6db2d4c --- /dev/null +++ b/crates/executor/src/scheduler/socket_read.rs @@ -0,0 +1,35 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{BufferResult, Cancellation, IoBufMut}; +use std::future::Future; + +/// Read once from an existing socket, preserving short transfers and datagram boundaries. +/// +/// A consumer can read an existing connection without opening, accepting, writing, or closing sockets: +/// +/// ``` +/// use socketry_executor::{BufferResult, Cancellation, SocketRead}; +/// async fn read_chunk(scheduler: &S, socket: &S::Socket) -> BufferResult { +/// scheduler.socket_read(socket, vec![0; 16], &Cancellation::never()).await +/// } +/// ``` +/// +/// Writing needs an explicit additional capability, even when its trait is imported: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, SocketRead, SocketWrite}; +/// async fn write(scheduler: &S, socket: &S::Socket) { +/// scheduler.socket_write(socket, vec![1], &Cancellation::never()).await; +/// } +/// ``` +pub trait SocketRead: SocketOperation { + /// Read once, returning the result and the original buffer. Short transfers are preserved. For datagrams, a small buffer can truncate and discard the remainder of a message. + fn socket_read( + &self, + socket: &Self::Socket, + buffer: B, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_recv_msg.rs b/crates/executor/src/scheduler/socket_recv_msg.rs new file mode 100644 index 0000000..631b344 --- /dev/null +++ b/crates/executor/src/scheduler/socket_recv_msg.rs @@ -0,0 +1,33 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, IoBufMut, OperationResult, SocketRecvFlags, SocketRecvResult}; +use std::future::Future; + +/// Receive one scatter/gather message through an existing socket. Buffer owners are returned on every result. See [`SocketMessages`](crate::SocketMessages) for the shared message I/O contract. +/// +/// A receive-only adapter need not provide sending or any core socket operations: +/// +/// ``` +/// use socketry_executor::{Cancellation, IoBufMut, OperationResult, SocketOperation, SocketRecvFlags, SocketRecvMsg, SocketRecvResult}; +/// struct Receiver(S); +/// impl SocketOperation for Receiver { +/// type Socket = S::Socket; +/// } +/// impl SocketRecvMsg for Receiver { +/// async fn socket_recvmsg(&self, socket: &Self::Socket, buffers: Vec, flags: SocketRecvFlags, cancellation: &Cancellation) -> (OperationResult, Vec) { +/// self.0.socket_recvmsg(socket, buffers, flags, cancellation).await +/// } +/// } +/// ``` +pub trait SocketRecvMsg: SocketOperation { + /// Receive one message into owned buffers. `WSAEMSGSIZE` on Windows is represented as a successful truncated receive, matching Unix; other OS errors are preserved. A zero-byte datagram is a message, whereas a zero-byte stream receive with nonempty capacity normally indicates EOF. + fn socket_recvmsg( + &self, + socket: &Self::Socket, + buffers: Vec, + flags: SocketRecvFlags, + cancellation: &Cancellation, + ) -> impl Future, Vec)> + Send; +} diff --git a/crates/executor/src/scheduler/socket_registration.rs b/crates/executor/src/scheduler/socket_registration.rs index 4a70e65..f338188 100644 --- a/crates/executor/src/scheduler/socket_registration.rs +++ b/crates/executor/src/scheduler/socket_registration.rs @@ -1,16 +1,15 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use super::SocketScheduler; +use super::SocketOperation; use std::{ io, net::{TcpListener, TcpStream}, }; /// Optional interoperability with existing standard-library TCP resources. Streams and listeners become the same backend socket descriptor. -/// Register once; the backend sets the nonblocking mode it needs. Do not change -/// that mode through another handle. Simulated schedulers need not implement it. -pub trait SocketRegistration: SocketScheduler { +/// Register once; the backend sets the nonblocking mode it needs. Do not change that mode through another handle. Simulated schedulers need not implement it. +pub trait SocketRegistration: SocketOperation { fn register_socket(&self, socket: TcpStream) -> io::Result; fn register_listener(&self, listener: TcpListener) -> io::Result; } diff --git a/crates/executor/src/scheduler/socket_scheduler.rs b/crates/executor/src/scheduler/socket_scheduler.rs index a719249..6d2ef0d 100644 --- a/crates/executor/src/scheduler/socket_scheduler.rs +++ b/crates/executor/src/scheduler/socket_scheduler.rs @@ -1,112 +1,53 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use crate::{ - BufferResult, Cancellation, Interest, IoBuf, IoBufMut, OperationResult, SocketDomain, - SocketProtocol, SocketType, -}; -use std::{ - future::Future, - io, - net::{Shutdown, SocketAddr}, +use super::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, SocketWrite, }; -/// IP socket operations through the chosen scheduler. Each backend supplies one descriptor type for unconnected, listening, and connected sockets. Operations have the same state requirements as their system calls; invalid transitions report OS errors. +/// Convenience aggregate for the core socket operations provided by a scheduler. /// -/// Creation, binding, listening, and connecting are separate operations. All operation futures are lazy. Pre-cancellation prevents a syscall; successful progress wins a cancellation race. Cancelling or abandoning a pending connect does not roll back kernel connection state: discard that socket before trying another connection. Completion backends retain owned buffers until the original operation completes. +/// Implemented automatically for schedulers supplying every core socket capability. Consumers can instead require individual traits such as `SocketRead + SocketWrite`. Message I/O, typed options, and standard-library resource registration remain optional. /// -/// File and socket resources remain distinct. There is no common descriptor trait, protocol buffering, or automatic close coordination. Message/address I/O belongs to the optional `SocketMessages` capability. Non-IP families remain an extension. -pub trait SocketScheduler: Send + Sync { - type Socket: Send + Sync; - - /// Consume and close an exclusively owned socket, awaiting bounded admission and OS completion. Finish all other operations first: close never cancels or joins them, and retained backend use may panic. Dropping an unpolled close releases the owned descriptor through its ordinary destructor. - fn socket_close(&self, socket: Self::Socket) -> impl Future> + Send; - - /// Consume the socket and await bounded cleanup admission only. Accepted close jobs survive waiter abandonment and are drained during graceful scheduler shutdown. Execution errors are discarded; use `socket_close` to observe them. The same exclusive-ownership requirement applies. - fn socket_close_submit( - &self, - socket: Self::Socket, - ) -> impl Future> + Send; - - /// Shut down reads, writes, or both, awaiting bounded admission and OS completion. The descriptor remains owned by the caller. Finish preceding writes before `Shutdown::Write`; reads may continue. Success confirms local shutdown, not peer receipt. Repeated shutdown and later I/O preserve platform-specific results. - /// - /// This lazy cleanup operation has no cancellation input. After admission it retains the socket and executes even if its waiter is dropped. Await its completion before explicitly closing the socket. - fn socket_shutdown( - &self, - socket: &Self::Socket, - how: Shutdown, - ) -> impl Future> + Send; - - /// Request shutdown and await bounded admission only. Acceptance does not establish completion or ordering with later operations, including close. The backend retains the socket until execution finishes; graceful scheduler shutdown drains accepted requests. Use `socket_shutdown` when subsequent work requires completion or errors. - fn socket_shutdown_submit( - &self, - socket: &Self::Socket, - how: Shutdown, - ) -> impl Future> + Send; - - /// Open a nonblocking socket without binding, listening, or connecting. `None` selects the OS default protocol for the requested domain and type. - fn socket_open( - &self, - domain: SocketDomain, - kind: SocketType, - protocol: Option, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Bind an existing socket to a local address. Port zero requests an OS-selected port, available through `socket_local_addr`. - fn socket_bind( - &self, - socket: &Self::Socket, - address: SocketAddr, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Put an existing socket into listening mode using the supplied backlog. This does not create or explicitly bind the socket. OS-specific implicit binding, backlog limits, and repeated-listen behavior are preserved. - fn socket_listen( - &self, - socket: &Self::Socket, - backlog: i32, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result; - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result; - - /// Connect an existing socket, preserving any prior local binding or configuration. A cancelled connect can still complete in the kernel; it does not make the descriptor reusable for another connection attempt. - fn socket_connect( - &self, - socket: &Self::Socket, - address: SocketAddr, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Accept one connection from a listening socket, returning an owned nonblocking socket and its peer address. - fn socket_accept( - &self, - socket: &Self::Socket, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Read once, returning the result and the original buffer. Short transfers are preserved. For datagrams, a small buffer can truncate and discard the remainder of a message. - fn socket_read( - &self, - socket: &Self::Socket, - buffer: B, - cancellation: &Cancellation, - ) -> impl Future> + Send; - - /// Write once, returning the result and the original buffer. Stream callers handle short transfers; datagram callers submit one complete message per operation. - fn socket_write( - &self, - socket: &Self::Socket, - buffer: B, - cancellation: &Cancellation, - ) -> impl Future> + Send; +/// Implement `SocketOperation` to select the resource type, then each operation trait to supply its methods. Import individual operation traits for concrete method calls; a generic `S: SocketScheduler` bound includes every core method. See `SocketOperation` for the shared resource and cancellation contract. +/// +/// Standard-library resource import remains an explicit optional capability: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{SocketRegistration, SocketScheduler}; +/// fn register(scheduler: &S, socket: std::net::TcpStream) { +/// scheduler.register_socket(socket); +/// } +/// ``` +pub trait SocketScheduler: + SocketClose + + SocketShutdown + + SocketOpen + + SocketBind + + SocketListen + + SocketLocalAddr + + SocketPeerAddr + + SocketConnect + + SocketAccept + + SocketRead + + SocketWrite + + SocketWait +{ +} - fn socket_wait( - &self, - socket: &Self::Socket, - interest: Interest, - cancellation: &Cancellation, - ) -> impl Future> + Send; +impl SocketScheduler for S where + S: SocketClose + + SocketShutdown + + SocketOpen + + SocketBind + + SocketListen + + SocketLocalAddr + + SocketPeerAddr + + SocketConnect + + SocketAccept + + SocketRead + + SocketWrite + + SocketWait +{ } diff --git a/crates/executor/src/scheduler/socket_send_msg.rs b/crates/executor/src/scheduler/socket_send_msg.rs new file mode 100644 index 0000000..3046117 --- /dev/null +++ b/crates/executor/src/scheduler/socket_send_msg.rs @@ -0,0 +1,26 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, IoBuf, OperationResult, SocketMessage, SocketSendFlags}; +use std::future::Future; + +/// Send one scatter/gather message through an existing socket. Buffer owners are returned on every result. See [`SocketMessages`](crate::SocketMessages) for the shared message I/O contract. +/// +/// Message sending does not imply message receiving: +/// +/// ```compile_fail,E0599 +/// use socketry_executor::{Cancellation, SocketRecvFlags, SocketRecvMsg, SocketSendMsg}; +/// async fn receive(scheduler: &S, socket: &S::Socket) { +/// scheduler.socket_recvmsg(socket, vec![vec![0; 16]], SocketRecvFlags::default(), &Cancellation::never()).await; +/// } +/// ``` +pub trait SocketSendMsg: SocketOperation { + fn socket_sendmsg( + &self, + socket: &Self::Socket, + message: SocketMessage, + flags: SocketSendFlags, + cancellation: &Cancellation, + ) -> impl Future, SocketMessage)> + Send; +} diff --git a/crates/executor/src/scheduler/socket_set_option.rs b/crates/executor/src/scheduler/socket_set_option.rs new file mode 100644 index 0000000..8681166 --- /dev/null +++ b/crates/executor/src/scheduler/socket_set_option.rs @@ -0,0 +1,18 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, OperationResult, SocketOption}; +use std::future::Future; + +/// Set a typed socket option. See [`SocketOptions`](crate::SocketOptions) for the shared option contract; the opposite operation is not required. +pub trait SocketSetOption: SocketOperation { + /// Set the option and wait for syscall completion. This borrows the descriptor and does not change its ownership. + fn socket_set_option( + &self, + socket: &Self::Socket, + option: O, + value: O::Value, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_shutdown.rs b/crates/executor/src/scheduler/socket_shutdown.rs new file mode 100644 index 0000000..f1e30c0 --- /dev/null +++ b/crates/executor/src/scheduler/socket_shutdown.rs @@ -0,0 +1,24 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use std::{future::Future, io, net::Shutdown}; + +/// Shut down one or both directions of a socket through the scheduler. +pub trait SocketShutdown: SocketOperation { + /// Shut down reads, writes, or both, awaiting bounded admission and OS completion. The descriptor remains owned by the caller. Finish preceding writes before `Shutdown::Write`; reads may continue. Success confirms local shutdown, not peer receipt. Repeated shutdown and later I/O preserve platform-specific results. + /// + /// This lazy cleanup operation has no cancellation input. After admission it retains the socket and executes even if its waiter is dropped. Await its completion before explicitly closing the socket. + fn socket_shutdown( + &self, + socket: &Self::Socket, + how: Shutdown, + ) -> impl Future> + Send; + + /// Request shutdown and await bounded admission only. Acceptance does not establish completion or ordering with later operations, including close. The backend retains the socket until execution finishes; graceful scheduler shutdown drains accepted requests. Use `socket_shutdown` when subsequent work requires completion or errors. + fn socket_shutdown_submit( + &self, + socket: &Self::Socket, + how: Shutdown, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_wait.rs b/crates/executor/src/scheduler/socket_wait.rs new file mode 100644 index 0000000..fc8f3c4 --- /dev/null +++ b/crates/executor/src/scheduler/socket_wait.rs @@ -0,0 +1,16 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{Cancellation, Interest, OperationResult}; +use std::future::Future; + +/// Wait for socket readiness. Readiness is a hint; a following operation can still need to wait. +pub trait SocketWait: SocketOperation { + fn socket_wait( + &self, + socket: &Self::Socket, + interest: Interest, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socket_write.rs b/crates/executor/src/scheduler/socket_write.rs new file mode 100644 index 0000000..d566080 --- /dev/null +++ b/crates/executor/src/scheduler/socket_write.rs @@ -0,0 +1,17 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::{BufferResult, Cancellation, IoBuf}; +use std::future::Future; + +/// Write once to an existing socket, preserving short transfers and datagram boundaries. +pub trait SocketWrite: SocketOperation { + /// Write once, returning the result and the original buffer. Stream callers handle short transfers; datagram callers submit one complete message per operation. + fn socket_write( + &self, + socket: &Self::Socket, + buffer: B, + cancellation: &Cancellation, + ) -> impl Future> + Send; +} diff --git a/crates/executor/src/scheduler/socketry/operations.rs b/crates/executor/src/scheduler/socketry/operations.rs index 0187c0e..3b4c5e5 100644 --- a/crates/executor/src/scheduler/socketry/operations.rs +++ b/crates/executor/src/scheduler/socketry/operations.rs @@ -5,7 +5,7 @@ use super::{Scheduler, SchedulerHandle}; use crate::SyncMode; use crate::scheduler::selector::DefaultSelector; -use crate::scheduler::{BufferResult, Clock, Interest, SocketScheduler}; +use crate::scheduler::{BufferResult, Clock, Interest}; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; use crate::{Cancellation, FileOptions, IoBuf, IoBufMut, OperationResult, SocketRegistration}; @@ -13,7 +13,12 @@ use crate::{ FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileSeek, FileStat, FileSync, FileTruncate, FileWrite, FileWriteAt, }; -use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; +use crate::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketShutdown, SocketWait, SocketWrite, +}; +use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use std::io::{self, SeekFrom}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::Path; @@ -83,9 +88,9 @@ impl SchedulerHandle { } } -impl crate::SocketOptions for SchedulerHandle +impl crate::SocketGetOption for SchedulerHandle where - DefaultSelector: crate::SocketOptions, + DefaultSelector: crate::SocketGetOption, { async fn socket_get_option( &self, @@ -97,7 +102,12 @@ where .socket_get_option(socket, option, cancellation) .await } +} +impl crate::SocketSetOption for SchedulerHandle +where + DefaultSelector: crate::SocketSetOption, +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -119,9 +129,11 @@ impl SocketRegistration for SchedulerHandle { self.selector()?.register_listener(listener) } } -impl SocketScheduler for SchedulerHandle { - type Socket = ::Socket; +impl SocketOperation for SchedulerHandle { + type Socket = ::Socket; +} +impl SocketClose for SchedulerHandle { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { check_open( self.shared @@ -139,7 +151,9 @@ impl SocketScheduler for SchedulerHandle { )?; crate::scheduler::cleanup::submit_only(&self.shared.cleanup, socket.into_close()).await } +} +impl SocketShutdown for SchedulerHandle { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -171,7 +185,9 @@ impl SocketScheduler for SchedulerHandle { }) .await } +} +impl SocketOpen for SchedulerHandle { async fn socket_open( &self, domain: crate::SocketDomain, @@ -183,6 +199,9 @@ impl SocketScheduler for SchedulerHandle { .socket_open(domain, kind, protocol, cancellation) .await } +} + +impl SocketBind for SchedulerHandle { async fn socket_bind( &self, socket: &Self::Socket, @@ -193,6 +212,9 @@ impl SocketScheduler for SchedulerHandle { .socket_bind(socket, address, cancellation) .await } +} + +impl SocketListen for SchedulerHandle { async fn socket_listen( &self, socket: &Self::Socket, @@ -203,12 +225,21 @@ impl SocketScheduler for SchedulerHandle { .socket_listen(socket, backlog, cancellation) .await } +} + +impl SocketLocalAddr for SchedulerHandle { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { self.selector()?.socket_local_addr(socket) } +} + +impl SocketPeerAddr for SchedulerHandle { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { self.selector()?.socket_peer_addr(socket) } +} + +impl SocketConnect for SchedulerHandle { async fn socket_connect( &self, socket: &Self::Socket, @@ -219,6 +250,9 @@ impl SocketScheduler for SchedulerHandle { .socket_connect(socket, address, cancellation) .await } +} + +impl SocketAccept for SchedulerHandle { async fn socket_accept( &self, socket: &Self::Socket, @@ -226,6 +260,9 @@ impl SocketScheduler for SchedulerHandle { ) -> OperationResult<(Self::Socket, SocketAddr)> { self.selector()?.socket_accept(socket, cancellation).await } +} + +impl SocketRead for SchedulerHandle { async fn socket_read( &self, socket: &Self::Socket, @@ -237,6 +274,9 @@ impl SocketScheduler for SchedulerHandle { Err(error) => (Err(error.into()), buffer), } } +} + +impl SocketWrite for SchedulerHandle { async fn socket_write( &self, socket: &Self::Socket, @@ -248,6 +288,9 @@ impl SocketScheduler for SchedulerHandle { Err(error) => (Err(error.into()), buffer), } } +} + +impl SocketWait for SchedulerHandle { async fn socket_wait( &self, socket: &Self::Socket, @@ -480,9 +523,9 @@ impl Clock for SchedulerHandle { crate::scheduler::clock::native_sleep_until(deadline, cancellation).await } } -impl crate::SocketOptions for Scheduler +impl crate::SocketGetOption for Scheduler where - SchedulerHandle: crate::SocketOptions, + SchedulerHandle: crate::SocketGetOption, { async fn socket_get_option( &self, @@ -494,7 +537,12 @@ where .socket_get_option(socket, option, cancellation) .await } +} +impl crate::SocketSetOption for Scheduler +where + SchedulerHandle: crate::SocketSetOption, +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -516,9 +564,11 @@ impl SocketRegistration for Scheduler { self.handle.register_listener(listener) } } -impl SocketScheduler for Scheduler { - type Socket = ::Socket; +impl SocketOperation for Scheduler { + type Socket = ::Socket; +} +impl SocketClose for Scheduler { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close(socket).await } @@ -526,7 +576,9 @@ impl SocketScheduler for Scheduler { async fn socket_close_submit(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close_submit(socket).await } +} +impl SocketShutdown for Scheduler { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -542,7 +594,9 @@ impl SocketScheduler for Scheduler { ) -> io::Result<()> { self.handle.socket_shutdown_submit(socket, how).await } +} +impl SocketOpen for Scheduler { async fn socket_open( &self, domain: crate::SocketDomain, @@ -554,6 +608,9 @@ impl SocketScheduler for Scheduler { .socket_open(domain, kind, protocol, cancellation) .await } +} + +impl SocketBind for Scheduler { async fn socket_bind( &self, socket: &Self::Socket, @@ -562,6 +619,9 @@ impl SocketScheduler for Scheduler { ) -> OperationResult<()> { self.handle.socket_bind(socket, address, cancellation).await } +} + +impl SocketListen for Scheduler { async fn socket_listen( &self, socket: &Self::Socket, @@ -572,12 +632,21 @@ impl SocketScheduler for Scheduler { .socket_listen(socket, backlog, cancellation) .await } +} + +impl SocketLocalAddr for Scheduler { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { self.handle.socket_local_addr(socket) } +} + +impl SocketPeerAddr for Scheduler { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { self.handle.socket_peer_addr(socket) } +} + +impl SocketConnect for Scheduler { async fn socket_connect( &self, socket: &Self::Socket, @@ -588,6 +657,9 @@ impl SocketScheduler for Scheduler { .socket_connect(socket, address, cancellation) .await } +} + +impl SocketAccept for Scheduler { async fn socket_accept( &self, socket: &Self::Socket, @@ -595,6 +667,9 @@ impl SocketScheduler for Scheduler { ) -> OperationResult<(Self::Socket, SocketAddr)> { self.handle.socket_accept(socket, cancellation).await } +} + +impl SocketRead for Scheduler { async fn socket_read( &self, socket: &Self::Socket, @@ -603,6 +678,9 @@ impl SocketScheduler for Scheduler { ) -> BufferResult { self.handle.socket_read(socket, buffer, cancellation).await } +} + +impl SocketWrite for Scheduler { async fn socket_write( &self, socket: &Self::Socket, @@ -611,6 +689,9 @@ impl SocketScheduler for Scheduler { ) -> BufferResult { self.handle.socket_write(socket, buffer, cancellation).await } +} + +impl SocketWait for Scheduler { async fn socket_wait( &self, socket: &Self::Socket, @@ -795,7 +876,7 @@ impl Clock for Scheduler { #[cfg(test)] mod tests; -impl SocketMessages for SchedulerHandle { +impl SocketSendMsg for SchedulerHandle { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -812,6 +893,9 @@ impl SocketMessages for SchedulerHandle { Err(error) => (Err(error.into()), message), } } +} + +impl SocketRecvMsg for SchedulerHandle { async fn socket_recvmsg( &self, socket: &Self::Socket, @@ -830,7 +914,7 @@ impl SocketMessages for SchedulerHandle { } } -impl SocketMessages for Scheduler { +impl SocketSendMsg for Scheduler { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -842,6 +926,9 @@ impl SocketMessages for Scheduler { .socket_sendmsg(socket, message, flags, cancellation) .await } +} + +impl SocketRecvMsg for Scheduler { async fn socket_recvmsg( &self, socket: &Self::Socket, diff --git a/crates/executor/src/scheduler/socketry/operations/tests.rs b/crates/executor/src/scheduler/socketry/operations/tests.rs index 6b14173..b9b4986 100644 --- a/crates/executor/src/scheduler/socketry/operations/tests.rs +++ b/crates/executor/src/scheduler/socketry/operations/tests.rs @@ -2,6 +2,7 @@ // Copyright, 2026, by Samuel Williams. use crate::{FileReadAt, FileWriteAt}; +use crate::{SocketAccept, SocketRead, SocketWait, SocketWrite}; static NEVER: crate::Cancellation = crate::Cancellation::never(); use super::{DefaultSelector, Scheduler, check_initialized_open, check_open}; diff --git a/crates/executor/src/scheduler/tokio.rs b/crates/executor/src/scheduler/tokio.rs index 0157e83..b9867e7 100644 --- a/crates/executor/src/scheduler/tokio.rs +++ b/crates/executor/src/scheduler/tokio.rs @@ -11,7 +11,7 @@ mod signals; use super::operation::cancellable; -use super::{BufferResult, Clock, Interest, SocketScheduler}; +use super::{BufferResult, Clock, Interest}; use crate::blocking_pool::BlockingPool; #[cfg(target_os = "linux")] use crate::{AllocationMode, FileAllocate}; @@ -24,7 +24,12 @@ use crate::{ FileTruncate, FileWrite, FileWriteAt, }; use crate::{FileDescriptor, SyncMode}; -use crate::{SocketMessage, SocketMessages, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; +use crate::{ + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketShutdown, SocketWait, SocketWrite, +}; +use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; use ::tokio::runtime::Handle; use ::tokio::task::{AbortHandle, JoinHandle}; use event_listener::Event; @@ -530,7 +535,7 @@ impl Future for InRuntime { } } -impl crate::SocketOptions +impl crate::SocketGetOption for SchedulerHandle { async fn socket_get_option( @@ -543,7 +548,11 @@ impl crate::Socke cancellation.check()?; Ok(option.get(socket.get_ref())?) } +} +impl crate::SocketSetOption + for SchedulerHandle +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -568,9 +577,11 @@ impl SocketRegistration for SchedulerHandle { SocketDescriptor::new(&self.shared.runtime, listener.into()) } } -impl SocketScheduler for SchedulerHandle { +impl SocketOperation for SchedulerHandle { type Socket = SocketDescriptor; +} +impl SocketClose for SchedulerHandle { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.check_runtime(socket.runtime())?; crate::scheduler::cleanup::run(&self.shared.cleanup, socket.into_close()).await @@ -580,7 +591,9 @@ impl SocketScheduler for SchedulerHandle { self.check_runtime(socket.runtime())?; crate::scheduler::cleanup::submit_only(&self.shared.cleanup, socket.into_close()).await } +} +impl SocketShutdown for SchedulerHandle { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -604,7 +617,9 @@ impl SocketScheduler for SchedulerHandle { }) .await } +} +impl SocketOpen for SchedulerHandle { async fn socket_open( &self, domain: crate::SocketDomain, @@ -622,7 +637,9 @@ impl SocketScheduler for SchedulerHandle { |socket| SocketDescriptor::new(&self.shared.runtime, socket), ) } +} +impl SocketBind for SchedulerHandle { async fn socket_bind( &self, socket: &Self::Socket, @@ -634,7 +651,9 @@ impl SocketScheduler for SchedulerHandle { socket.get_ref().bind(&address.into())?; Ok(()) } +} +impl SocketListen for SchedulerHandle { async fn socket_listen( &self, socket: &Self::Socket, @@ -646,17 +665,23 @@ impl SocketScheduler for SchedulerHandle { socket.get_ref().listen(backlog)?; Ok(()) } +} +impl SocketLocalAddr for SchedulerHandle { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { self.check_runtime(socket.runtime())?; socket.local_addr() } +} +impl SocketPeerAddr for SchedulerHandle { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { self.check_runtime(socket.runtime())?; socket.peer_addr() } +} +impl SocketConnect for SchedulerHandle { async fn socket_connect( &self, socket: &Self::Socket, @@ -675,7 +700,9 @@ impl SocketScheduler for SchedulerHandle { ) .await } +} +impl SocketAccept for SchedulerHandle { async fn socket_accept( &self, socket: &Self::Socket, @@ -691,7 +718,9 @@ impl SocketScheduler for SchedulerHandle { ) .await } +} +impl SocketRead for SchedulerHandle { async fn socket_read( &self, socket: &Self::Socket, @@ -711,7 +740,9 @@ impl SocketScheduler for SchedulerHandle { .await; (result, buffer) } +} +impl SocketWrite for SchedulerHandle { async fn socket_write( &self, socket: &Self::Socket, @@ -729,7 +760,9 @@ impl SocketScheduler for SchedulerHandle { .await; (result, buffer) } +} +impl SocketWait for SchedulerHandle { async fn socket_wait( &self, socket: &Self::Socket, @@ -1143,9 +1176,9 @@ impl Spawn for SchedulerHandle { } } -impl crate::SocketOptions for Scheduler +impl crate::SocketGetOption for Scheduler where - SchedulerHandle: crate::SocketOptions, + SchedulerHandle: crate::SocketGetOption, { async fn socket_get_option( &self, @@ -1157,7 +1190,12 @@ where .socket_get_option(socket, option, cancellation) .await } +} +impl crate::SocketSetOption for Scheduler +where + SchedulerHandle: crate::SocketSetOption, +{ async fn socket_set_option( &self, socket: &Self::Socket, @@ -1179,9 +1217,11 @@ impl SocketRegistration for Scheduler { self.handle.register_listener(listener) } } -impl SocketScheduler for Scheduler { - type Socket = ::Socket; +impl SocketOperation for Scheduler { + type Socket = ::Socket; +} +impl SocketClose for Scheduler { async fn socket_close(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close(socket).await } @@ -1189,7 +1229,9 @@ impl SocketScheduler for Scheduler { async fn socket_close_submit(&self, socket: Self::Socket) -> io::Result<()> { self.handle.socket_close_submit(socket).await } +} +impl SocketShutdown for Scheduler { async fn socket_shutdown( &self, socket: &Self::Socket, @@ -1205,7 +1247,9 @@ impl SocketScheduler for Scheduler { ) -> io::Result<()> { self.handle.socket_shutdown_submit(socket, how).await } +} +impl SocketOpen for Scheduler { async fn socket_open( &self, domain: crate::SocketDomain, @@ -1217,6 +1261,9 @@ impl SocketScheduler for Scheduler { .socket_open(domain, kind, protocol, cancellation) .await } +} + +impl SocketBind for Scheduler { async fn socket_bind( &self, socket: &Self::Socket, @@ -1225,6 +1272,9 @@ impl SocketScheduler for Scheduler { ) -> OperationResult<()> { self.handle.socket_bind(socket, address, cancellation).await } +} + +impl SocketListen for Scheduler { async fn socket_listen( &self, socket: &Self::Socket, @@ -1235,12 +1285,21 @@ impl SocketScheduler for Scheduler { .socket_listen(socket, backlog, cancellation) .await } +} + +impl SocketLocalAddr for Scheduler { fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { self.handle.socket_local_addr(socket) } +} + +impl SocketPeerAddr for Scheduler { fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { self.handle.socket_peer_addr(socket) } +} + +impl SocketConnect for Scheduler { async fn socket_connect( &self, socket: &Self::Socket, @@ -1251,6 +1310,9 @@ impl SocketScheduler for Scheduler { .socket_connect(socket, address, cancellation) .await } +} + +impl SocketAccept for Scheduler { async fn socket_accept( &self, socket: &Self::Socket, @@ -1258,6 +1320,9 @@ impl SocketScheduler for Scheduler { ) -> OperationResult<(Self::Socket, SocketAddr)> { self.handle.socket_accept(socket, cancellation).await } +} + +impl SocketRead for Scheduler { async fn socket_read( &self, socket: &Self::Socket, @@ -1266,6 +1331,9 @@ impl SocketScheduler for Scheduler { ) -> BufferResult { self.handle.socket_read(socket, buffer, cancellation).await } +} + +impl SocketWrite for Scheduler { async fn socket_write( &self, socket: &Self::Socket, @@ -1274,6 +1342,9 @@ impl SocketScheduler for Scheduler { ) -> BufferResult { self.handle.socket_write(socket, buffer, cancellation).await } +} + +impl SocketWait for Scheduler { async fn socket_wait( &self, socket: &Self::Socket, @@ -1501,7 +1572,7 @@ impl Blocking for SchedulerHandle { #[cfg(test)] mod tests; -impl SocketMessages for SchedulerHandle { +impl SocketSendMsg for SchedulerHandle { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -1536,6 +1607,9 @@ impl SocketMessages for SchedulerHandle { }, ) } +} + +impl SocketRecvMsg for SchedulerHandle { async fn socket_recvmsg( &self, socket: &Self::Socket, @@ -1564,7 +1638,7 @@ impl SocketMessages for SchedulerHandle { } } -impl SocketMessages for Scheduler { +impl SocketSendMsg for Scheduler { async fn socket_sendmsg( &self, socket: &Self::Socket, @@ -1576,6 +1650,9 @@ impl SocketMessages for Scheduler { .socket_sendmsg(socket, message, flags, cancellation) .await } +} + +impl SocketRecvMsg for Scheduler { async fn socket_recvmsg( &self, socket: &Self::Socket, diff --git a/crates/executor/src/socket_option.rs b/crates/executor/src/socket_option.rs index 6acb408..4e2d1c3 100644 --- a/crates/executor/src/socket_option.rs +++ b/crates/executor/src/socket_option.rs @@ -3,7 +3,7 @@ /// A typed socket-option identifier. This describes its value, not how an operating system represents or accesses it. /// -/// Backends implement `SocketOptions` separately for each supported option. Additional option types can be defined without changing the core `SocketScheduler` trait or introducing a runtime option enum. +/// Backends implement `SocketGetOption` and/or `SocketSetOption` for each supported operation and option; `SocketOptions` automatically aggregates both. Additional option types can be defined without changing the core `SocketScheduler` trait or introducing a runtime option enum. pub trait SocketOption: Send { type Value: Send; } diff --git a/crates/executor/tests/contracts.rs b/crates/executor/tests/contracts.rs index 95f5f54..59ebaf3 100644 --- a/crates/executor/tests/contracts.rs +++ b/crates/executor/tests/contracts.rs @@ -5,6 +5,7 @@ use socketry_executor::{ Cancellation, FileOptions, FileScheduler, Interest, IoBuf, OperationError, OperationResult, SocketDomain, SocketProtocol, SocketScheduler, SocketType, Spawn, }; + use std::{ future::{Future, poll_fn}, io, diff --git a/crates/executor/tests/io.rs b/crates/executor/tests/io.rs index bc378d8..6e83dc6 100644 --- a/crates/executor/tests/io.rs +++ b/crates/executor/tests/io.rs @@ -11,6 +11,10 @@ use socketry_executor::{ Clock, FileScheduler, Interest, OperationResult, SocketDomain, SocketProtocol, SocketScheduler, SocketType, Spawn, }; +use socketry_executor::{ + SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, + SocketPeerAddr, SocketRead, SocketWait, SocketWrite, +}; use socketry_executor::{SocketRegistration, SyncMode}; use std::fs::File as StdFile; use std::future::{Future, poll_fn}; diff --git a/crates/executor/tests/socket_capabilities.rs b/crates/executor/tests/socket_capabilities.rs new file mode 100644 index 0000000..080d26b --- /dev/null +++ b/crates/executor/tests/socket_capabilities.rs @@ -0,0 +1,119 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use futures_lite::future::block_on; +use socketry_executor::{ + BufferResult, Cancellation, IoBuf, IoBufMut, OperationError, SocketOperation, SocketRead, + SocketWrite, +}; +use std::sync::Mutex; + +// A protocol can run against existing in-memory connections without socket creation, listening, options, messages, or explicit close. +struct Memory; + +struct Connection { + incoming: &'static [u8], + outgoing: Vec, +} + +impl SocketOperation for Memory { + type Socket = Mutex; +} + +impl SocketRead for Memory { + async fn socket_read( + &self, + socket: &Self::Socket, + mut buffer: B, + cancellation: &Cancellation, + ) -> BufferResult { + if let Err(error) = cancellation.check() { + return (Err(error.into()), buffer); + } + let mut connection = socket.lock().unwrap(); + let count = connection.incoming.len().min(buffer.as_mut().len()).min(2); + buffer.as_mut()[..count].copy_from_slice(&connection.incoming[..count]); + connection.incoming = &connection.incoming[count..]; + (Ok(count), buffer) + } +} + +impl SocketWrite for Memory { + async fn socket_write( + &self, + socket: &Self::Socket, + buffer: B, + cancellation: &Cancellation, + ) -> BufferResult { + if let Err(error) = cancellation.check() { + return (Err(error.into()), buffer); + } + let count = buffer.as_ref().len().min(1); + socket + .lock() + .unwrap() + .outgoing + .extend_from_slice(&buffer.as_ref()[..count]); + (Ok(count), buffer) + } +} + +async fn echo_chunk( + scheduler: &S, + socket: &S::Socket, + buffer: Vec, + cancellation: &Cancellation, +) -> BufferResult { + let (result, mut buffer) = scheduler.socket_read(socket, buffer, cancellation).await; + let count = match result { + Ok(count) => count, + Err(error) => return (Err(error), buffer), + }; + let mut offset = 0; + while offset < count { + let (result, view) = scheduler + .socket_write(socket, buffer.slice(offset..count), cancellation) + .await; + buffer = view.into_inner(); + match result { + Ok(0) => { + return ( + Err(std::io::Error::from(std::io::ErrorKind::WriteZero).into()), + buffer, + ); + } + Ok(written) => offset += written, + Err(error) => return (Err(error), buffer), + } + } + (Ok(count), buffer) +} + +#[test] +fn protocol_needs_only_reads_and_writes_on_one_shared_resource_type() { + let socket = Mutex::new(Connection { + incoming: b"abc", + outgoing: Vec::new(), + }); + let buffer = vec![0; 8]; + let allocation = buffer.as_ptr(); + let never = Cancellation::never(); + let (result, buffer) = block_on(echo_chunk(&Memory, &socket, buffer, &never)); + assert_eq!(result.unwrap(), 2); + assert_eq!(buffer.as_ptr(), allocation); + assert_eq!(socket.lock().unwrap().outgoing, b"ab"); + + let cancelled = Cancellation::new(); + cancelled.cancel(); + let (result, buffer) = block_on(echo_chunk(&Memory, &socket, buffer, &cancelled)); + assert!(matches!(result, Err(OperationError::Cancelled(_)))); + assert_eq!(buffer.as_ptr(), allocation); + assert_eq!(socket.lock().unwrap().incoming, b"c"); + + let (result, buffer) = block_on(echo_chunk(&Memory, &socket, buffer, &never)); + assert_eq!(result.unwrap(), 1); + let (result, buffer) = block_on(echo_chunk(&Memory, &socket, buffer, &never)); + assert_eq!(result.unwrap(), 0); + assert_eq!(buffer.as_ptr(), allocation); + assert_eq!(socket.lock().unwrap().outgoing, b"abc"); +} diff --git a/crates/executor/tests/socket_lifecycle.rs b/crates/executor/tests/socket_lifecycle.rs index 1013061..3178ad7 100644 --- a/crates/executor/tests/socket_lifecycle.rs +++ b/crates/executor/tests/socket_lifecycle.rs @@ -4,6 +4,7 @@ #![cfg(any(feature = "native", feature = "tokio"))] use socketry_executor::{Cancellation, SocketDomain, SocketScheduler, SocketType}; + use std::{io, net::Shutdown}; async fn sockets(scheduler: &S) -> [S::Socket; 3] { diff --git a/crates/executor/tests/socket_messages.rs b/crates/executor/tests/socket_messages.rs index 47365dd..16220bf 100644 --- a/crates/executor/tests/socket_messages.rs +++ b/crates/executor/tests/socket_messages.rs @@ -7,12 +7,13 @@ mod contracts; mod support; use socketry_executor::{ - Cancellation, SocketDomain, SocketMessage, SocketMessages, SocketRecvFlags, SocketScheduler, - SocketSendFlags, SocketType, + Cancellation, SocketDomain, SocketMessage, SocketMessages, SocketRecvFlags, SocketSendFlags, + SocketType, }; +use socketry_executor::{SocketClose, SocketOpen}; use std::io; -async fn errors(scheduler: &S) { +async fn errors(scheduler: &S) { let never = Cancellation::never(); let socket = scheduler .socket_open(SocketDomain::Ipv4, SocketType::Stream, None, &never) diff --git a/crates/executor/tests/socket_options.rs b/crates/executor/tests/socket_options.rs index 50c1c7a..5d8db95 100644 --- a/crates/executor/tests/socket_options.rs +++ b/crates/executor/tests/socket_options.rs @@ -6,9 +6,10 @@ use socketry_executor::{ Cancellation, OperationError, SocketDomain, SocketOptions, SocketType, TcpNoDelay, }; +use socketry_executor::{SocketClose, SocketOpen}; use std::io; -async fn open>(scheduler: &S, kind: SocketType) -> S::Socket { +async fn open(scheduler: &S, kind: SocketType) -> S::Socket { scheduler .socket_open(SocketDomain::Ipv4, kind, None, &Cancellation::never()) .await @@ -39,7 +40,7 @@ async fn rejected>( ); } -async fn invalid_protocol>(scheduler: &S) { +async fn invalid_protocol + SocketOpen + SocketClose>(scheduler: &S) { let socket = open(scheduler, SocketType::Datagram).await; let never = Cancellation::never(); for error in [ diff --git a/examples/echo/client.rs b/examples/echo/client.rs index ef05b58..f6a8d56 100644 --- a/examples/echo/client.rs +++ b/examples/echo/client.rs @@ -1,15 +1,16 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -// `SocketScheduler`, `AddressResolution`, and `IoBuf` are traits: importing them makes their methods available. +// `SocketOpen`, `SocketConnect`, `SocketRead`, `SocketWrite`, `AddressResolution`, and `IoBuf` are traits: importing them makes their methods available. use socketry::{ AddressHints, AddressResolution, AddressService, Cancellation, IoBuf, OperationResult, - Scheduler, SocketDomain, SocketScheduler, SocketType, + Scheduler, SocketDomain, SocketType, }; +use socketry::{SocketConnect, SocketOpen, SocketRead, SocketWrite}; use std::io; -// This function works with any scheduler implementing the `SocketScheduler` and `AddressResolution` traits. `&S` and `&Cancellation` borrow values owned by `main()`. The `Vec` is owned by this future and returned with the echoed bytes. -async fn echo( +// This function works with any scheduler implementing socket open/connect/read/write and `AddressResolution` traits. `&S` and `&Cancellation` borrow values owned by `main()`. The `Vec` is owned by this future and returned with the echoed bytes. +async fn echo( scheduler: &S, mut buffer: Vec, cancellation: &Cancellation, diff --git a/examples/echo/server.rs b/examples/echo/server.rs index 3cef95c..46298b1 100644 --- a/examples/echo/server.rs +++ b/examples/echo/server.rs @@ -3,19 +3,21 @@ // Bring these types and traits into scope. // -// Traits such as `SocketScheduler`, `Signals`, and `IoBuf` provide methods used below. Importing a trait makes its methods available on implementing types. +// Traits such as `SocketRead`, `SocketWrite`, `Signals`, and `IoBuf` provide methods used below. Importing a trait makes its methods available on implementing types. #[cfg(unix)] use socketry::{ Cancellation, IoBuf, OperationError, OperationResult, Scheduler, Signals, SocketDomain, - SocketProtocol, SocketScheduler, SocketType, signal::SIGINT, + SocketProtocol, SocketType, signal::SIGINT, }; +#[cfg(unix)] +use socketry::{SocketAccept, SocketBind, SocketListen, SocketOpen, SocketRead, SocketWrite}; #[cfg(unix)] use std::io; // `async fn` returns a future. Its body runs when that future is polled. // -// `` means this function works with any scheduler type `S` implementing the `SocketScheduler` capability. +// `` means this function works with any scheduler type `S` providing reads and writes. It does not require socket creation, accepting, or other operations. // // `&S` borrows the scheduler instead of taking ownership. // @@ -28,7 +30,7 @@ use std::io; // - `Ok(())`: succeeded, with no meaningful return value. // - `Err(error)`: failed or was cancelled. #[cfg(unix)] -async fn echo( +async fn echo( scheduler: &S, socket: S::Socket, shutdown: &Cancellation, diff --git a/readme.md b/readme.md index 901bc7f..ee0fbe0 100644 --- a/readme.md +++ b/readme.md @@ -78,17 +78,17 @@ The optional `signals` feature adds Unix `Signals::signal_notify(&signals, targe ## Portable I/O and runtime selection -Generic code can accept `SocketScheduler`, `FileScheduler`, `Clock`, and `Spawn` capabilities. Socketry and the optional Tokio adapter implement these contracts with concrete future and resource types. File consumers can require individual operations such as `FileRead + FileWrite`; import those operation traits for method calls on concrete schedulers. +Generic code can accept `SocketScheduler`, `FileScheduler`, `Clock`, and `Spawn` capabilities. Socketry and the optional Tokio adapter implement these contracts with concrete future and resource types. Consumers can require individual operations such as `FileRead + FileWrite` or `SocketRead + SocketWrite`; import the individual operation traits for method calls on concrete schedulers. `file_close` waits for OS completion; `file_close_submit` waits only for bounded admission. Both consume an owned file handle and require prior operations to have finished. The [commented file-close example](crates/executor/examples/file_close.rs) demonstrates requesting cancellation, awaiting completion, and then closing safely. `socket_shutdown(&socket, how)` supports directional shutdown while retaining the descriptor. `socket_close(socket)` consumes it after outstanding work has finished. Both have admission-only `_submit` forms. The [commented half-close example](crates/executor/examples/socket_shutdown.rs) sends a request, shuts down writes, receives the response, then closes. -`SocketOptions` adds typed, cancellable option get/set. Require `SocketReuseAddress`, `SocketKeepalive`, or `TcpNoDelay` as needed; the option selects its value type and backend capability. The [listener configuration example](crates/executor/examples/socket_options.rs) shows configuration before binding. +`SocketGetOption` and `SocketSetOption` add typed, cancellable option get/set; `SocketOptions` aggregates both. Require `SocketReuseAddress`, `SocketKeepalive`, or `TcpNoDelay` as needed; the option selects its value type and backend capability. The [listener configuration example](crates/executor/examples/socket_options.rs) shows configuration before binding. `FileStat::file_stat` queries an open descriptor and returns owned, native-backed metadata through portable `FileMetadata` accessors. The result remains usable after close; the operation leaves the cursor unchanged. See the [commented metadata example](crates/executor/examples/file_stat.rs). -The optional `SocketMessages` capability provides `socket_sendmsg`/`socket_recvmsg` with owned scatter/gather buffers, IP addresses, portable flags, and receive truncation metadata. See the [commented UDP message example](crates/executor/examples/socket_messages.rs). Ancillary data remains a separate extension. +The optional `SocketSendMsg` and `SocketRecvMsg` capabilities, aggregated as `SocketMessages`, provide `socket_sendmsg`/`socket_recvmsg` with owned scatter/gather buffers, IP addresses, portable flags, and receive truncation metadata. See the [commented UDP message example](crates/executor/examples/socket_messages.rs). Ancillary data remains a separate extension. `FileAllocate` adds optional range allocation with `AllocationMode::Extend` or `KeepSize`. Native and Tokio implementations currently provide it on Linux; unsupported targets omit the trait implementation. See the [executor I/O documentation](crates/executor/readme.md#io-and-selectors) for allocation and filesystem guarantees. diff --git a/releases.md b/releases.md index a0fe273..dabf260 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,8 @@ ## Unreleased +- Split socket capabilities into `SocketOperation` for the shared resource type and individual core operation traits, with `SocketScheduler` as an automatic aggregate. Separate message send/receive and typed option get/set, retaining `SocketMessages` and `SocketOptions` as automatic aggregates. Optional operations and registration depend only on `SocketOperation`. Implementors migrate to the individual traits; concrete callers import operation traits, qualified resource paths use `SocketOperation::Socket`, and extension consumers explicitly require their setup and cleanup capabilities. Preserve backend behavior and update examples, migration guidance, and conformance tests. + - Split file capabilities into `FileOperation` for the shared resource type and individual operation traits, with `FileScheduler` as a blanket convenience aggregate. Rename the broad `File` and `Socket` traits to `FileScheduler` and `SocketScheduler`. `FileStat` owns the metadata type; optional `FileAllocate` and `FileAdvice` no longer require all file operations. Implementors must migrate `impl File` to the individual traits and rename `impl Socket` to `impl SocketScheduler`; concrete file callers must import the operation traits. Fully qualified associated types move to `FileOperation::File`, `FileStat::Metadata`, and `SocketScheduler::Socket`. Update native, Tokio, simulated implementations, examples, and capability-focused contracts. - Add optional Linux `FileAdvice` with six `AdviceMode` hints and completion/admission-only `file_advise` forms. Native, Tokio, and io_uring schedulers use bounded cleanup admission, retain descriptors through execution, and drain accepted jobs during graceful shutdown. Include simulator contracts and a commented example. diff --git a/src/lib.rs b/src/lib.rs index e9abd5a..cc4cf6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,10 +12,13 @@ pub use socketry_executor::{ FileAllocate, FileClose, FileDescriptor, FileMetadata, FileOpen, FileOperation, FileOptions, FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, FileType, FileWrite, FileWriteAt, Interest, IoBuf, IoBufMut, Notify, OperationError, OperationResult, - Scheduler, SchedulerHandle, Signals, SocketDomain, SocketKeepalive, SocketMessage, - SocketMessages, SocketOption, SocketOptions, SocketProtocol, SocketRecvFlags, SocketRecvResult, - SocketRegistration, SocketReuseAddress, SocketScheduler, SocketSendFlags, SocketType, Spawn, - SpawnError, SyncMode, Task, TaskError, TaskHandle, TcpNoDelay, defer_cancel, yield_now, + Scheduler, SchedulerHandle, Signals, SocketAccept, SocketBind, SocketClose, SocketConnect, + SocketDomain, SocketGetOption, SocketKeepalive, SocketListen, SocketLocalAddr, SocketMessage, + SocketMessages, SocketOpen, SocketOperation, SocketOption, SocketOptions, SocketPeerAddr, + SocketProtocol, SocketRead, SocketRecvFlags, SocketRecvMsg, SocketRecvResult, + SocketRegistration, SocketReuseAddress, SocketScheduler, SocketSendFlags, SocketSendMsg, + SocketSetOption, SocketShutdown, SocketType, SocketWait, SocketWrite, Spawn, SpawnError, + SyncMode, Task, TaskError, TaskHandle, TcpNoDelay, defer_cancel, yield_now, }; #[cfg(all(unix, feature = "signals", any(feature = "native", feature = "tokio")))] From 15f26d9244731ee6b130d07c1d24bd0463f89684 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 18 Oct 2026 16:52:56 +1300 Subject: [PATCH 2/2] Spell out socket address names in the public interface. --- context/design.md | 2 ++ crates/executor/design.md | 30 +++++++++---------- crates/executor/examples/interface_probe.rs | 8 ++--- .../examples/interface_probe/simulated.rs | 14 ++++----- .../examples/interface_probe/tests.rs | 6 ++-- .../tests/address_resolution.rs | 2 +- .../interface_probe/tests/socket_lifecycle.rs | 8 ++--- .../interface_probe/tests/socket_messages.rs | 12 ++++---- .../interface_probe/tests/socket_options.rs | 7 +++-- crates/executor/examples/portable_io.rs | 2 +- crates/executor/examples/socket_messages.rs | 4 +-- crates/executor/examples/socket_options.rs | 4 +-- crates/executor/examples/socket_shutdown.rs | 2 +- crates/executor/readme.md | 4 ++- crates/executor/src/address_info.rs | 5 ++-- crates/executor/src/lib.rs | 12 ++++++-- crates/executor/src/scheduler.rs | 8 ++--- .../src/scheduler/selector/io_uring.rs | 16 +++++----- .../src/scheduler/selector/io_uring/tests.rs | 6 ++-- .../src/scheduler/selector/readiness.rs | 12 ++++---- .../src/scheduler/selector/readiness/tests.rs | 9 ++++-- .../executor/src/scheduler/socket_accept.rs | 6 ++-- crates/executor/src/scheduler/socket_bind.rs | 8 ++--- .../executor/src/scheduler/socket_connect.rs | 6 ++-- .../src/scheduler/socket_local_addr.rs | 10 ------- .../src/scheduler/socket_local_address.rs | 11 +++++++ .../src/scheduler/socket_peer_addr.rs | 10 ------- .../src/scheduler/socket_peer_address.rs | 11 +++++++ .../src/scheduler/socket_scheduler.rs | 12 ++++---- .../src/scheduler/socketry/operations.rs | 28 ++++++++--------- crates/executor/src/scheduler/tokio.rs | 24 +++++++-------- crates/executor/src/socket_domain.rs | 10 ++++--- crates/executor/src/socket_message.rs | 4 +-- crates/executor/src/socket_recv_result.rs | 6 ++-- crates/executor/tests/contracts.rs | 20 ++++++------- crates/executor/tests/io.rs | 26 ++++++++-------- releases.md | 2 ++ src/lib.rs | 14 ++++----- 38 files changed, 201 insertions(+), 180 deletions(-) delete mode 100644 crates/executor/src/scheduler/socket_local_addr.rs create mode 100644 crates/executor/src/scheduler/socket_local_address.rs delete mode 100644 crates/executor/src/scheduler/socket_peer_addr.rs create mode 100644 crates/executor/src/scheduler/socket_peer_address.rs diff --git a/context/design.md b/context/design.md index c92f37e..1434b75 100644 --- a/context/design.md +++ b/context/design.md @@ -141,6 +141,8 @@ The implementations live under `scheduler/selector/`, selected with Cargo featur File operation traits share `FileOperation::File`; `FileStat` declares `Metadata`. `FileScheduler` is an automatically implemented aggregate of the core file capabilities, while `FileAllocate` and `FileAdvice` remain optional and depend only on `FileOperation`. Use narrow bounds for generic consumers and import individual operation traits for concrete method calls. Socket operation traits share `SocketOperation::Socket`, and `SocketScheduler` is their automatic core aggregate. `SocketMessages` aggregates `SocketSendMsg`/`SocketRecvMsg`, and `SocketOptions` aggregates `SocketGetOption`/`SocketSetOption`. Each individual extension and `SocketRegistration` depend only on `SocketOperation`; consumers explicitly require any additional setup, transfer, or cleanup operations. Import operation traits for concrete method calls and use `::Socket` for qualified resource paths. +Spell out address names in the scheduler interface: `SocketAddress` re-exports `std::net::SocketAddr` unchanged, while `SocketLocalAddress::socket_local_address` and `SocketPeerAddress::socket_peer_address` distinguish the two endpoints. Public socket address parameters, message metadata, and resolver candidates use the re-export without introducing wrappers or conversion costs. + The Linux `io-uring` feature selects a dedicated ring for socket/file reads and writes, scatter/gather message sends/receives, and `file_sync`, plus descriptor-based `file_stat` using required `Statx` support. `file_truncate` uses the ring when its opcode is available, with a blocking-file fallback on older kernels. Optional `FileAllocate` adds Linux range reservation with `AllocationMode::Extend` or `KeepSize`, using a probed `Fallocate` request or the blocking syscall fallback. Other platforms currently omit that capability. Synchronization, allocation, and native truncation retain the file without a payload buffer and use the same original-completion retirement contract as transfers. Connect, accept, explicit readiness waits and sleep still use async-io. Initialization probes required opcodes and reliable completion overflow; it returns an error if unavailable rather than silently changing implementation. Shutdown cancels and drains original completions. Operation identifiers are never reused, and cancellation completions cannot release original buffers. Unexpected selector failure retains resources whose kernel lifetime cannot be established and panics their waiting operations. Normal cancellation does not retain completed resources. The owned-buffer API supports `IoBuf`, `IoBufMut`, and `BufferSlice`, returning `(OperationResult, B)` with ownership on every normal result. Read buffers must already have an initialized length. Operations may be partial; dropping a waiting future can abandon I/O that consumes or transmits bytes. Each implementation chooses one associated socket resource for both accepted and listening sockets. Tokio registrations reject use through another runtime's adapter. diff --git a/crates/executor/design.md b/crates/executor/design.md index 50d74e6..dd6a78e 100644 --- a/crates/executor/design.md +++ b/crates/executor/design.md @@ -87,13 +87,13 @@ This is a source-breaking implementor refactor. Replace the former `impl File` w ### Socket operation capabilities -`SocketOperation` supplies the shared `Socket` associated type. Each core operation trait depends only on that base: `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddr`, `SocketPeerAddr`, `SocketShutdown`, and `SocketClose`. Keep the existing syscall-shaped method names; this split adds no new runtime behavior or allocations. Close and shutdown retain their ordinary and `_submit` forms together. +`SocketOperation` supplies the shared `Socket` associated type. Each core operation trait depends only on that base: `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddress`, `SocketPeerAddress`, `SocketShutdown`, and `SocketClose`. Keep the existing syscall-shaped method names; this split adds no new runtime behavior or allocations. Close and shutdown retain their ordinary and `_submit` forms together. `SocketScheduler` is implemented automatically for schedulers providing all core operations. It declares no methods or associated types. Existing-connection protocol code can require only `S: SocketRead + SocketWrite`, and a send loop can require only `S: SocketWrite`, without socket creation, listening, or accepting. The same resource type is shared across these bounds. Optional `SocketSendMsg` and `SocketRecvMsg` supply `socket_sendmsg` and `socket_recvmsg`; `SocketMessages` is their blanket aggregate. `SocketGetOption` and `SocketSetOption` independently supply typed option get/set; `SocketOptions` is their blanket aggregate. Each individual capability and `SocketRegistration` depend only on `SocketOperation`, so an extension-only implementation needs no core operations. Message, option, and registration capabilities remain outside `SocketScheduler`. -Implementors move `type Socket` from `impl SocketScheduler` into `impl SocketOperation` and move each method into its operation trait implementation. Replace explicit `SocketMessages` and `SocketOptions` implementations with the respective send/receive and get/set implementations. Do not implement the three aggregates explicitly. Fully qualified resource paths become `::Socket`; method paths name the operation trait, such as `SocketRead::socket_read`. Concrete callers import individual operation traits; importing only an aggregate no longer exposes methods on a concrete scheduler. Generic `S: SocketScheduler`, `S: SocketMessages`, and `S: SocketOptions` bounds retain their grouped methods. Consumers that previously relied on optional capabilities implying core socket operations must now add every setup, I/O, or cleanup capability they use. +Implementors move `type Socket` from `impl SocketScheduler` into `impl SocketOperation` and move each method into its operation trait implementation. Replace explicit `SocketMessages` and `SocketOptions` implementations with the respective send/receive and get/set implementations. Do not implement the three aggregates explicitly. Fully qualified resource paths become `::Socket`; method paths name the operation trait, such as `SocketRead::socket_read`. Concrete callers import individual operation traits; importing only an aggregate no longer exposes methods on a concrete scheduler. Generic `S: SocketScheduler`, `S: SocketMessages`, and `S: SocketOptions` bounds retain their grouped methods. Consumers that previously relied on optional capabilities implying core socket operations must now add every setup, I/O, or cleanup capability they use. Address inspection spells out `Address`: rename `socket_local_addr`/`socket_peer_addr` to `socket_local_address`/`socket_peer_address`; the corresponding traits are `SocketLocalAddress` and `SocketPeerAddress`. Native readiness, io_uring, Tokio, and the in-memory simulator implement the split interfaces. Shared conformance tests state their setup and observation requirements. A descriptor-free read/write backend exercises a protocol without other socket capabilities, while positive and compile-fail examples verify extension-only adapters, absent operations, typed options, and the aggregate boundaries. Runtime operation bodies and cancellation, buffer ownership, and cleanup contracts are unchanged. @@ -103,7 +103,7 @@ Generic code takes a scheduler such as `&S`, with bounds describing the portion Resource creation belongs inside the corresponding capability. `FileOperation` declares an associated file type, and `FileOpen` returns that type. `SocketOperation` declares one associated socket type for unconnected, bound, listening, connected, and datagram sockets. Its `socket_open`, `socket_bind`, `socket_listen`, and `socket_connect` operations follow the corresponding system-call boundaries; `socket_accept` returns another value of that same type. `socket_listen` takes an existing socket and backlog and leaves bind behavior to the operating system, including implicit binding where supported. Generic consumers use these associated resources rather than creating a `std::fs::File` or `std::net::TcpListener` themselves. -The initial socket arguments select IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Address parameters use `std::net::SocketAddr`. `SocketOptions` adds typed configuration without extending the required `SocketScheduler` surface. `SocketMessages` adds owned scatter/gather payloads, IP addresses, and portable flags. Non-IP families and ancillary data remain future extensions. File and socket descriptors remain distinct; no common descriptor trait is required. Existing std TCP resources can still enter through the optional `SocketRegistration` interoperability trait. +The initial socket arguments select IPv4 or IPv6, stream or datagram, and an optional TCP or UDP protocol. Address parameters use `SocketAddress`, a re-export of `std::net::SocketAddr`. Local and peer inspection use `SocketLocalAddress::socket_local_address` and `SocketPeerAddress::socket_peer_address`. `SocketOptions` adds typed configuration without extending the required `SocketScheduler` surface. `SocketMessages` adds owned scatter/gather payloads, IP addresses, and portable flags. Non-IP families and ancillary data remain future extensions. File and socket descriptors remain distinct; no common descriptor trait is required. Existing std TCP resources can still enter through the optional `SocketRegistration` interoperability trait. The interface does not require `Arc` around every resource. Concrete implementations choose their resource representation and how to retain it during in-flight work. They may use OS handles, registered-resource tables, shared ownership, or simulated resource identifiers. Taking a resource by reference does not remove the backend's obligation to retain everything still accessible by a submitted operation after its waiting future is dropped. @@ -152,7 +152,7 @@ let received = received?; // received.bytes, address, truncated, control_truncated ``` -`SocketMessage` owns a `Vec` of `IoBuf` payload buffers and an optional `SocketAddr` destination. Sends return `(OperationResult, SocketMessage)`. Receives accept `Vec` and return `(OperationResult, Vec)`. Receive metadata contains the copied byte count, an optional sender address, payload truncation, and control truncation. Buffers are filled in order, their lengths stay unchanged, and only the returned byte count is newly received data. The sum of initialized lengths must fit `i32::MAX`, matching completion-result limits; native vector/message limits can still return OS errors. No payload concatenation is needed. +`SocketMessage` owns a `Vec` of `IoBuf` payload buffers and an optional `SocketAddress` destination. Sends return `(OperationResult, SocketMessage)`. Receives accept `Vec` and return `(OperationResult, Vec)`. Receive metadata contains the copied byte count, an optional sender address, payload truncation, and control truncation. Buffers are filled in order, their lengths stay unchanged, and only the returned byte count is newly received data. The sum of initialized lengths must fit `i32::MAX`, matching completion-result limits; native vector/message limits can still return OS errors. No payload concatenation is needed. Each call performs one transfer: streams can make partial progress, datagrams retain their boundaries, and a zero-byte UDP result can be an empty message. Empty buffer lists are represented by one zero-length native entry where required. Zero-capacity receives retain OS-specific behavior, including whether they consume data, wait, or return an address; use nonempty storage to receive empty datagrams portably. `SocketRecvFlags::peek` maps to `MSG_PEEK`; `SocketSendFlags::dont_route` maps to `MSG_DONTROUTE`. Unix sends suppress `SIGPIPE`, consistent with `socket_write`. Additional native flags, `MSG_WAITALL` policy, zero-copy sends, and original datagram-length reporting are separate extensions. @@ -188,7 +188,7 @@ for candidate in addresses { } ``` -The result is `OperationResult>`; each candidate owns its `SocketAddr`, `SocketType`, and optional `SocketProtocol`. `AddressService` accepts a numeric `Port(u16)` or a service `Name(&str)`. `AddressHints` selects optional domain, socket type, and protocol plus `passive`, `numeric_host`, and `address_configured` flags. Defaults request both IP families, unspecified type/protocol, and no flags. Native implementations pass these hints into `getaddrinfo` rather than filtering its output. A named service can select different ports or transports according to the system service database. +The result is `OperationResult>`; each candidate owns its `SocketAddress`, `SocketType`, and optional `SocketProtocol`. `AddressService` accepts a numeric `Port(u16)` or a service `Name(&str)`. `AddressHints` selects optional domain, socket type, and protocol plus `passive`, `numeric_host`, and `address_configured` flags. Defaults request both IP families, unspecified type/protocol, and no flags. Native implementations pass these hints into `getaddrinfo` rather than filtering its output. A named service can select different ports or transports according to the system service database. Names contain no port; numeric IPv6 literals are unbracketed. `None` selects loopback addresses, or wildcard bind addresses when `passive` is set. Empty strings and embedded NUL bytes return `InvalidInput`. `numeric_host` prevents hostname lookup. `address_configured` maps to opt-in `AI_ADDRCONFIG`; native resolvers do not count loopback alone as evidence of a configured family. Backends preserve candidate order, duplicates, IPv6 scope/flow metadata, and socket creation values. Native results outside the IP families (such as FreeBSD pathname resolution) return `InvalidData`. A successful list may be empty; incompatible hints or absent matches can instead produce resolver errors. @@ -362,7 +362,7 @@ scheduler.sleep_until(deadline, cancellation) scheduler.sleep(duration, cancellation) ``` -This list includes proposed extensions. The implemented `FileScheduler` methods are `file_open`, `file_stat`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at`. The implemented `SocketScheduler` methods are `socket_open`, `socket_bind`, `socket_listen`, `socket_connect`, `socket_accept`, `socket_read`, `socket_write`, and `socket_wait`, plus synchronous `socket_local_addr` and `socket_peer_addr`. Initial creation supports IPv4/IPv6 stream and datagram sockets with TCP/UDP protocol selection. Connected UDP preserves datagram boundaries. Optional `SocketMessages` adds address-based message operations; ancillary data remains an extension. Async I/O methods take `&Cancellation`; buffer operations return `(OperationResult, B)` on success, error, and cancellation. +This list includes proposed extensions. The implemented `FileScheduler` methods are `file_open`, `file_stat`, `file_seek`, `file_sync`, `file_truncate`, shared-cursor `file_read`/`file_write`, and explicit-offset `file_read_at`/`file_write_at`. The implemented `SocketScheduler` methods are `socket_open`, `socket_bind`, `socket_listen`, `socket_connect`, `socket_accept`, `socket_read`, `socket_write`, and `socket_wait`, plus synchronous `socket_local_address` and `socket_peer_address`. Initial creation supports IPv4/IPv6 stream and datagram sockets with TCP/UDP protocol selection. Connected UDP preserves datagram boundaries. Optional `SocketMessages` adds address-based message operations; ancillary data remains an extension. Async I/O methods take `&Cancellation`; buffer operations return `(OperationResult, B)` on success, error, and cancellation. An optional context pairing a scheduler reference with a cancellation signal could reduce repetitive arguments. Such a context should preserve explicit dependency and cancellation boundaries; it is a convenience, not a new computation abstraction. @@ -591,7 +591,7 @@ pub trait SocketBind: SocketOperation { fn socket_bind( &self, socket: &Self::Socket, - address: SocketAddr, + address: SocketAddress, cancellation: &Cancellation, ) -> impl Future> + Send; } @@ -605,19 +605,19 @@ pub trait SocketListen: SocketOperation { ) -> impl Future> + Send; } -pub trait SocketLocalAddr: SocketOperation { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result; +pub trait SocketLocalAddress: SocketOperation { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result; } -pub trait SocketPeerAddr: SocketOperation { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result; +pub trait SocketPeerAddress: SocketOperation { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result; } pub trait SocketConnect: SocketOperation { fn socket_connect( &self, socket: &Self::Socket, - address: SocketAddr, + address: SocketAddress, cancellation: &Cancellation, ) -> impl Future> + Send; } @@ -627,7 +627,7 @@ pub trait SocketAccept: SocketOperation { &self, socket: &Self::Socket, cancellation: &Cancellation, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } pub trait SocketRead: SocketOperation { @@ -684,8 +684,8 @@ pub trait SocketScheduler: + SocketOpen + SocketBind + SocketListen - + SocketLocalAddr - + SocketPeerAddr + + SocketLocalAddress + + SocketPeerAddress + SocketConnect + SocketAccept + SocketRead diff --git a/crates/executor/examples/interface_probe.rs b/crates/executor/examples/interface_probe.rs index 137f4ef..75e401a 100644 --- a/crates/executor/examples/interface_probe.rs +++ b/crates/executor/examples/interface_probe.rs @@ -13,8 +13,8 @@ use socketry_executor::{ SocketScheduler, SocketType, }; use socketry_executor::{ - SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, SocketRead, - SocketWrite, + SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddress, SocketOpen, + SocketRead, SocketWrite, }; use std::{io, path::Path}; static NEVER: Cancellation = Cancellation::never(); @@ -113,7 +113,7 @@ async fn exchange< S: SocketOpen + SocketBind + SocketListen - + SocketLocalAddr + + SocketLocalAddress + SocketConnect + SocketAccept + SocketRead @@ -133,7 +133,7 @@ async fn exchange< .await?; scheduler.socket_bind(&listener, address, &NEVER).await?; scheduler.socket_listen(&listener, 128, &NEVER).await?; - let address = scheduler.socket_local_addr(&listener)?; + let address = scheduler.socket_local_address(&listener)?; let client = async { // The same hostname resolves through the system backend or the simulator's explicit table. Connection policy remains application code. let addresses = scheduler diff --git a/crates/executor/examples/interface_probe/simulated.rs b/crates/executor/examples/interface_probe/simulated.rs index f718c15..83a19ac 100644 --- a/crates/executor/examples/interface_probe/simulated.rs +++ b/crates/executor/examples/interface_probe/simulated.rs @@ -12,8 +12,8 @@ use socketry_executor::{ }; use socketry_executor::{Interest, IoBuf, IoBufMut, SocketDomain, SocketProtocol, SocketType}; use socketry_executor::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketOperation, SocketPeerAddress, SocketRead, SocketShutdown, SocketWait, SocketWrite, }; use std::collections::{HashMap, VecDeque}; @@ -675,8 +675,8 @@ impl SocketListen for Scheduler { } } -impl SocketLocalAddr for Scheduler { - fn socket_local_addr(&self, socket: &SocketDescriptor) -> io::Result { +impl SocketLocalAddress for Scheduler { + fn socket_local_address(&self, socket: &SocketDescriptor) -> io::Result { match &*socket.state.lock().unwrap() { SocketState::Open { local, .. } => Ok(*local), SocketState::Listening { address, .. } => Ok(*address), @@ -685,8 +685,8 @@ impl SocketLocalAddr for Scheduler { } } -impl SocketPeerAddr for Scheduler { - fn socket_peer_addr(&self, socket: &SocketDescriptor) -> io::Result { +impl SocketPeerAddress for Scheduler { + fn socket_peer_address(&self, socket: &SocketDescriptor) -> io::Result { match &*socket.state.lock().unwrap() { SocketState::Connected(connection) => Ok(connection.peer), _ => Err(io::Error::from(io::ErrorKind::NotConnected)), @@ -778,7 +778,7 @@ impl SocketAccept for Scheduler { let ready = incoming.ready.listen(); let accepted = incoming.sockets.lock().unwrap().pop_front(); if let Some(socket) = accepted { - let peer = self.socket_peer_addr(&socket)?; + let peer = self.socket_peer_address(&socket)?; return Ok((socket, peer)); } wait(ready, cancellation).await?; diff --git a/crates/executor/examples/interface_probe/tests.rs b/crates/executor/examples/interface_probe/tests.rs index 47a251f..c462baa 100644 --- a/crates/executor/examples/interface_probe/tests.rs +++ b/crates/executor/examples/interface_probe/tests.rs @@ -293,7 +293,7 @@ fn pending_accept_and_receive_wake_and_drop_produces_eof() { let listener = open_listener(&scheduler, "127.0.0.1:0".parse().unwrap()) .await .unwrap(); - let address = scheduler.socket_local_addr(&listener).unwrap(); + let address = scheduler.socket_local_address(&listener).unwrap(); let mut accept = pin!(scheduler.socket_accept(&listener, &NEVER)); assert!(accept.as_mut().poll(&mut context).is_pending()); let client = open_client(&scheduler, address).await.unwrap(); @@ -329,7 +329,7 @@ fn listener_drop_releases_address_and_pending_connections() { let listener = open_listener(&scheduler, "127.0.0.1:12345".parse().unwrap()) .await .unwrap(); - let address = scheduler.socket_local_addr(&listener).unwrap(); + let address = scheduler.socket_local_address(&listener).unwrap(); let duplicate = scheduler .socket_open( SocketDomain::Ipv4, @@ -350,7 +350,7 @@ fn listener_drop_releases_address_and_pending_connections() { let (result, _) = scheduler.socket_read(&client, vec![0; 1], &NEVER).await; assert_eq!(result.unwrap(), 0); let listener = open_listener(&scheduler, address).await.unwrap(); - assert_eq!(scheduler.socket_local_addr(&listener).unwrap(), address); + assert_eq!(scheduler.socket_local_address(&listener).unwrap(), address); }); } diff --git a/crates/executor/examples/interface_probe/tests/address_resolution.rs b/crates/executor/examples/interface_probe/tests/address_resolution.rs index 92ebb5d..f8bc7c3 100644 --- a/crates/executor/examples/interface_probe/tests/address_resolution.rs +++ b/crates/executor/examples/interface_probe/tests/address_resolution.rs @@ -169,7 +169,7 @@ pub async fn connection( .await .unwrap(); scheduler.socket_listen(&listener, 8, &never).await.unwrap(); - let port = scheduler.socket_local_addr(&listener).unwrap().port(); + let port = scheduler.socket_local_address(&listener).unwrap().port(); let addresses = scheduler .address_resolve( Some(name), diff --git a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs index bc11b03..9ad6d56 100644 --- a/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs +++ b/crates/executor/examples/interface_probe/tests/socket_lifecycle.rs @@ -3,7 +3,7 @@ use socketry_executor::{Cancellation, OperationError, SocketDomain, SocketType}; use socketry_executor::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, SocketOpen, SocketRead, SocketShutdown, SocketWrite, }; use std::net::Shutdown; @@ -26,7 +26,7 @@ async fn pair< + SocketListen + SocketConnect + SocketAccept - + SocketLocalAddr + + SocketLocalAddress + SocketClose, >( scheduler: &S, @@ -45,7 +45,7 @@ async fn pair< scheduler .socket_connect( &client, - scheduler.socket_local_addr(&listener).unwrap(), + scheduler.socket_local_address(&listener).unwrap(), &never, ) .await @@ -61,7 +61,7 @@ pub async fn check< + SocketListen + SocketConnect + SocketAccept - + SocketLocalAddr + + SocketLocalAddress + SocketClose + SocketRead + SocketWrite diff --git a/crates/executor/examples/interface_probe/tests/socket_messages.rs b/crates/executor/examples/interface_probe/tests/socket_messages.rs index f200a38..7bd6d1c 100644 --- a/crates/executor/examples/interface_probe/tests/socket_messages.rs +++ b/crates/executor/examples/interface_probe/tests/socket_messages.rs @@ -8,7 +8,7 @@ use socketry_executor::{ SocketRecvFlags, SocketSendFlags, SocketType, }; use socketry_executor::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, SocketOpen, SocketShutdown, }; use std::{ @@ -49,7 +49,7 @@ pub async fn check< + SocketOpen + SocketBind + SocketConnect - + SocketLocalAddr + + SocketLocalAddress + SocketClose + SocketListen + SocketAccept @@ -60,8 +60,8 @@ pub async fn check< for local in ["127.0.0.1:0", "[::1]:0"] { let receiver = bound(scheduler, local.parse().unwrap(), SocketType::Datagram).await; let sender = bound(scheduler, local.parse().unwrap(), SocketType::Datagram).await; - let address = scheduler.socket_local_addr(&receiver).unwrap(); - let source = scheduler.socket_local_addr(&sender).unwrap(); + let address = scheduler.socket_local_address(&receiver).unwrap(); + let source = scheduler.socket_local_address(&sender).unwrap(); // An unpolled send has no effect. Receive cancellation also returns the original owners. drop(scheduler.socket_sendmsg( @@ -265,7 +265,7 @@ async fn stream< + SocketOpen + SocketBind + SocketConnect - + SocketLocalAddr + + SocketLocalAddress + SocketClose + SocketListen + SocketAccept @@ -287,7 +287,7 @@ async fn stream< scheduler .socket_connect( &sender, - scheduler.socket_local_addr(&listener).unwrap(), + scheduler.socket_local_address(&listener).unwrap(), &NEVER, ) .await diff --git a/crates/executor/examples/interface_probe/tests/socket_options.rs b/crates/executor/examples/interface_probe/tests/socket_options.rs index a18aad5..5d1f993 100644 --- a/crates/executor/examples/interface_probe/tests/socket_options.rs +++ b/crates/executor/examples/interface_probe/tests/socket_options.rs @@ -6,7 +6,8 @@ use socketry_executor::{ SocketReuseAddress, SocketType, TcpNoDelay, }; use socketry_executor::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, }; async fn round_trip(scheduler: &S, socket: &S::Socket, option: O) @@ -75,7 +76,7 @@ where + SocketListen + SocketAccept + SocketConnect - + SocketLocalAddr + + SocketLocalAddress + SocketClose, { let never = Cancellation::never(); @@ -114,7 +115,7 @@ where scheduler .socket_connect( &client, - scheduler.socket_local_addr(&listener).unwrap(), + scheduler.socket_local_address(&listener).unwrap(), &never, ) .await diff --git a/crates/executor/examples/portable_io.rs b/crates/executor/examples/portable_io.rs index dbbee4e..a09f905 100644 --- a/crates/executor/examples/portable_io.rs +++ b/crates/executor/examples/portable_io.rs @@ -27,7 +27,7 @@ where .socket_bind(&listener, "127.0.0.1:0".parse().unwrap(), &NEVER) .await?; scheduler.socket_listen(&listener, 128, &NEVER).await?; - let address = scheduler.socket_local_addr(&listener)?; + let address = scheduler.socket_local_address(&listener)?; let server_scheduler = scheduler.clone(); let server = scheduler .spawn(async move { diff --git a/crates/executor/examples/socket_messages.rs b/crates/executor/examples/socket_messages.rs index 414bab6..d018ae3 100644 --- a/crates/executor/examples/socket_messages.rs +++ b/crates/executor/examples/socket_messages.rs @@ -10,7 +10,7 @@ async fn exchange< S: socketry_executor::SocketMessages + socketry_executor::SocketOpen + socketry_executor::SocketBind - + socketry_executor::SocketLocalAddr + + socketry_executor::SocketLocalAddress + socketry_executor::SocketClose, >( scheduler: &S, @@ -40,7 +40,7 @@ async fn exchange< scheduler .socket_bind(&receiver, "127.0.0.1:0".parse().unwrap(), &cancellation) .await?; - let address = scheduler.socket_local_addr(&receiver)?; + let address = scheduler.socket_local_address(&receiver)?; // These two allocations form one datagram. The backend builds the native iovec array without concatenating or copying their contents. An address selects the destination without connecting the socket; None would use an existing connected peer. let message = SocketMessage { diff --git a/crates/executor/examples/socket_options.rs b/crates/executor/examples/socket_options.rs index 158d3fb..8c63003 100644 --- a/crates/executor/examples/socket_options.rs +++ b/crates/executor/examples/socket_options.rs @@ -12,7 +12,7 @@ where + socketry_executor::SocketOpen + socketry_executor::SocketBind + socketry_executor::SocketListen - + socketry_executor::SocketLocalAddr + + socketry_executor::SocketLocalAddress + socketry_executor::SocketClose, { use socketry_executor::{Cancellation, SocketDomain, SocketReuseAddress, SocketType}; @@ -40,7 +40,7 @@ where scheduler.socket_listen(&socket, 16, &cancellation).await?; println!( "Configured listener at {}", - scheduler.socket_local_addr(&socket)? + scheduler.socket_local_address(&socket)? ); // A server could now accept connections and explicitly configure TcpNoDelay or SocketKeepalive on each accepted descriptor. This example stops after setup; every operation is complete before consuming close. diff --git a/crates/executor/examples/socket_shutdown.rs b/crates/executor/examples/socket_shutdown.rs index 0e417a8..e4512e6 100644 --- a/crates/executor/examples/socket_shutdown.rs +++ b/crates/executor/examples/socket_shutdown.rs @@ -73,7 +73,7 @@ async fn exchange(scheduler: &S) -> io::Result<()> { scheduler .socket_connect( &client, - scheduler.socket_local_addr(&listener)?, + scheduler.socket_local_address(&listener)?, &cancellation, ) .await?; diff --git a/crates/executor/readme.md b/crates/executor/readme.md index e97a81e..9a24916 100644 --- a/crates/executor/readme.md +++ b/crates/executor/readme.md @@ -191,7 +191,9 @@ File capabilities are split by operation: `FileOperation` selects the associated When migrating an implementation, move `type File` from the former `impl File` into `impl FileOperation`, move `type Metadata` and `file_stat` into `impl FileStat`, and move each remaining method into its operation trait implementation. Do not implement `FileScheduler` explicitly. Fully qualified paths become `::File`, `::Metadata`, and operation paths such as `FileRead::file_read`. For method calls on concrete schedulers, import the relevant operation traits; importing `FileScheduler` alone is insufficient. Generic functions bounded by `S: FileScheduler` still see every core method. Bounds on `FileAdvice` or `FileAllocate` now require explicit additional capabilities when the function also opens, reads, or closes files. -Socket capabilities are split by operation too. `SocketOperation` selects the shared associated resource, and `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddr`, `SocketPeerAddr`, `SocketShutdown`, and `SocketClose` supply core operations. `SocketScheduler` is their automatic aggregate. Existing-connection protocols can require just `SocketRead + SocketWrite`. +Socket capabilities are split by operation too. `SocketOperation` selects the shared associated resource, and `SocketOpen`, `SocketBind`, `SocketListen`, `SocketConnect`, `SocketAccept`, `SocketRead`, `SocketWrite`, `SocketWait`, `SocketLocalAddress`, `SocketPeerAddress`, `SocketShutdown`, and `SocketClose` supply core operations. `SocketScheduler` is their automatic aggregate. Existing-connection protocols can require just `SocketRead + SocketWrite`. + +`SocketAddress` re-exports `std::net::SocketAddr`; standard-library addresses work directly without conversion. Use `SocketLocalAddress::socket_local_address` and `SocketPeerAddress::socket_peer_address` to inspect the local and remote endpoints. These replace the abbreviated `SocketLocalAddr`/`SocketPeerAddr` traits and `socket_local_addr`/`socket_peer_addr` methods. Optional message I/O uses `SocketSendMsg` and `SocketRecvMsg`, with `SocketMessages` as their automatic aggregate. Options use `SocketGetOption` and `SocketSetOption`, with `SocketOptions` as their automatic aggregate. Each individual capability and `SocketRegistration` depend only on `SocketOperation`. An implementation may support only receiving messages or inspecting an option, without opening or writing sockets. diff --git a/crates/executor/src/address_info.rs b/crates/executor/src/address_info.rs index 19bcea9..d2c3ffd 100644 --- a/crates/executor/src/address_info.rs +++ b/crates/executor/src/address_info.rs @@ -1,13 +1,12 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use crate::{SocketProtocol, SocketType}; -use std::net::SocketAddr; +use crate::{SocketAddress, SocketProtocol, SocketType}; /// An owned IP endpoint and its socket creation parameters. Obtain its domain with `SocketDomain::from(info.address)`; pass its type and protocol directly to `socket_open`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct AddressInfo { - pub address: SocketAddr, + pub address: SocketAddress, pub socket_type: SocketType, /// `None` represents the resolver's zero/default protocol. pub protocol: Option, diff --git a/crates/executor/src/lib.rs b/crates/executor/src/lib.rs index 3082535..b502b4f 100644 --- a/crates/executor/src/lib.rs +++ b/crates/executor/src/lib.rs @@ -80,8 +80,8 @@ pub use scheduler::{ AddressResolution, BufferResult, Clock, FileAdvice, FileAllocate, FileClose, FileOpen, FileOperation, FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, FileWrite, FileWriteAt, Interest, Scheduler, SchedulerHandle, Signals, SocketAccept, - SocketBind, SocketClose, SocketConnect, SocketGetOption, SocketListen, SocketLocalAddr, - SocketMessages, SocketOpen, SocketOperation, SocketOptions, SocketPeerAddr, SocketRead, + SocketBind, SocketClose, SocketConnect, SocketGetOption, SocketListen, SocketLocalAddress, + SocketMessages, SocketOpen, SocketOperation, SocketOptions, SocketPeerAddress, SocketRead, SocketRecvMsg, SocketRegistration, SocketScheduler, SocketSendMsg, SocketSetOption, SocketShutdown, SocketWait, SocketWrite, }; @@ -102,6 +102,14 @@ pub use socket_type::SocketType; pub use spawn::{Spawn, SpawnError}; #[cfg(all(target_os = "linux", feature = "io-uring"))] pub use statx_metadata::StatxMetadata; +/// An IP socket address, re-exported from [`std::net::SocketAddr`]. Standard-library addresses can be passed directly without conversion. +/// +/// ``` +/// use socketry_executor::SocketAddress; +/// let address: SocketAddress = std::net::SocketAddr::from(([127, 0, 0, 1], 8080)); +/// let _: std::net::SocketAddr = address; +/// ``` +pub use std::net::SocketAddr as SocketAddress; pub use sync_mode::SyncMode; pub use system_file_metadata::SystemFileMetadata; pub use task::{Task, TaskError, TaskHandle, yield_now}; diff --git a/crates/executor/src/scheduler.rs b/crates/executor/src/scheduler.rs index 99a7b61..48c3f63 100644 --- a/crates/executor/src/scheduler.rs +++ b/crates/executor/src/scheduler.rs @@ -58,11 +58,11 @@ pub use socket_bind::SocketBind; mod socket_listen; pub use socket_listen::SocketListen; -mod socket_local_addr; -pub use socket_local_addr::SocketLocalAddr; +mod socket_local_address; +pub use socket_local_address::SocketLocalAddress; -mod socket_peer_addr; -pub use socket_peer_addr::SocketPeerAddr; +mod socket_peer_address; +pub use socket_peer_address::SocketPeerAddress; mod socket_connect; pub use socket_connect::SocketConnect; diff --git a/crates/executor/src/scheduler/selector/io_uring.rs b/crates/executor/src/scheduler/selector/io_uring.rs index f59d355..e6aa495 100644 --- a/crates/executor/src/scheduler/selector/io_uring.rs +++ b/crates/executor/src/scheduler/selector/io_uring.rs @@ -23,8 +23,8 @@ use crate::{ FileWrite, FileWriteAt, }; use crate::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketOperation, SocketPeerAddress, SocketRead, SocketShutdown, SocketWait, SocketWrite, }; use event_listener::{Event as CompletionEvent, Listener as _}; @@ -806,17 +806,17 @@ impl SocketListen for Selector { } } -impl SocketLocalAddr for Selector { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { +impl SocketLocalAddress for Selector { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result { self.check_open()?; - readiness::Selector.socket_local_addr(socket) + readiness::Selector.socket_local_address(socket) } } -impl SocketPeerAddr for Selector { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { +impl SocketPeerAddress for Selector { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result { self.check_open()?; - readiness::Selector.socket_peer_addr(socket) + readiness::Selector.socket_peer_address(socket) } } diff --git a/crates/executor/src/scheduler/selector/io_uring/tests.rs b/crates/executor/src/scheduler/selector/io_uring/tests.rs index bf863aa..25ba11f 100644 --- a/crates/executor/src/scheduler/selector/io_uring/tests.rs +++ b/crates/executor/src/scheduler/selector/io_uring/tests.rs @@ -445,9 +445,9 @@ fn closed_selector_rejects_its_direct_network_operations() { .await .is_err() ); - assert!(selector.socket_local_addr(&listener).is_err()); - assert!(selector.socket_local_addr(&socket).is_err()); - assert!(selector.socket_peer_addr(&socket).is_err()); + assert!(selector.socket_local_address(&listener).is_err()); + assert!(selector.socket_local_address(&socket).is_err()); + assert!(selector.socket_peer_address(&socket).is_err()); let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("closed"); assert!( diff --git a/crates/executor/src/scheduler/selector/readiness.rs b/crates/executor/src/scheduler/selector/readiness.rs index 8add98b..8e0c911 100644 --- a/crates/executor/src/scheduler/selector/readiness.rs +++ b/crates/executor/src/scheduler/selector/readiness.rs @@ -19,8 +19,8 @@ use crate::{ }; use crate::{FileDescriptor, SyncMode}; use crate::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketOperation, SocketPeerAddress, SocketRead, SocketRecvMsg, SocketSendMsg, SocketShutdown, SocketWait, SocketWrite, }; use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; @@ -196,14 +196,14 @@ impl SocketListen for Selector { } } -impl SocketLocalAddr for Selector { - fn socket_local_addr(&self, socket: &SocketDescriptor) -> io::Result { +impl SocketLocalAddress for Selector { + fn socket_local_address(&self, socket: &SocketDescriptor) -> io::Result { socket.local_addr() } } -impl SocketPeerAddr for Selector { - fn socket_peer_addr(&self, socket: &SocketDescriptor) -> io::Result { +impl SocketPeerAddress for Selector { + fn socket_peer_address(&self, socket: &SocketDescriptor) -> io::Result { socket.peer_addr() } } diff --git a/crates/executor/src/scheduler/selector/readiness/tests.rs b/crates/executor/src/scheduler/selector/readiness/tests.rs index 2b20e42..1e50c8b 100644 --- a/crates/executor/src/scheduler/selector/readiness/tests.rs +++ b/crates/executor/src/scheduler/selector/readiness/tests.rs @@ -25,7 +25,7 @@ fn readiness_operations_remain_available_with_other_backends_selected() { .socket_listen(&listener, 128, &NEVER) .await .unwrap(); - let address = selector.socket_local_addr(&listener).unwrap(); + let address = selector.socket_local_address(&listener).unwrap(); let duplicate = selector .socket_open( crate::SocketDomain::Ipv4, @@ -57,8 +57,11 @@ fn readiness_operations_remain_available_with_other_backends_selected() { .await .unwrap(); let (peer, peer_address) = selector.socket_accept(&listener, &NEVER).await.unwrap(); - assert_eq!(selector.socket_local_addr(&socket).unwrap(), peer_address); - assert_eq!(selector.socket_peer_addr(&socket).unwrap(), address); + assert_eq!( + selector.socket_local_address(&socket).unwrap(), + peer_address + ); + assert_eq!(selector.socket_peer_address(&socket).unwrap(), address); let mut written = 0; while written < 5 { let (result, buffer) = selector diff --git a/crates/executor/src/scheduler/socket_accept.rs b/crates/executor/src/scheduler/socket_accept.rs index 91802cc..9a26fbb 100644 --- a/crates/executor/src/scheduler/socket_accept.rs +++ b/crates/executor/src/scheduler/socket_accept.rs @@ -2,8 +2,8 @@ // Copyright, 2026, by Samuel Williams. use super::SocketOperation; -use crate::{Cancellation, OperationResult}; -use std::{future::Future, net::SocketAddr}; +use crate::{Cancellation, OperationResult, SocketAddress}; +use std::future::Future; /// Accept one connection from a listening socket. pub trait SocketAccept: SocketOperation { @@ -12,5 +12,5 @@ pub trait SocketAccept: SocketOperation { &self, socket: &Self::Socket, cancellation: &Cancellation, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } diff --git a/crates/executor/src/scheduler/socket_bind.rs b/crates/executor/src/scheduler/socket_bind.rs index 5786f7b..b390cfd 100644 --- a/crates/executor/src/scheduler/socket_bind.rs +++ b/crates/executor/src/scheduler/socket_bind.rs @@ -2,16 +2,16 @@ // Copyright, 2026, by Samuel Williams. use super::SocketOperation; -use crate::{Cancellation, OperationResult}; -use std::{future::Future, net::SocketAddr}; +use crate::{Cancellation, OperationResult, SocketAddress}; +use std::future::Future; /// Bind an existing socket to a local IP address. pub trait SocketBind: SocketOperation { - /// Bind an existing socket to a local address. Port zero requests an OS-selected port, available through `socket_local_addr`. + /// Bind an existing socket to a local address. Port zero requests an OS-selected port, available through `socket_local_address`. fn socket_bind( &self, socket: &Self::Socket, - address: SocketAddr, + address: SocketAddress, cancellation: &Cancellation, ) -> impl Future> + Send; } diff --git a/crates/executor/src/scheduler/socket_connect.rs b/crates/executor/src/scheduler/socket_connect.rs index f08ebc9..929e410 100644 --- a/crates/executor/src/scheduler/socket_connect.rs +++ b/crates/executor/src/scheduler/socket_connect.rs @@ -2,8 +2,8 @@ // Copyright, 2026, by Samuel Williams. use super::SocketOperation; -use crate::{Cancellation, OperationResult}; -use std::{future::Future, net::SocketAddr}; +use crate::{Cancellation, OperationResult, SocketAddress}; +use std::future::Future; /// Connect an existing socket to a peer IP address. pub trait SocketConnect: SocketOperation { @@ -11,7 +11,7 @@ pub trait SocketConnect: SocketOperation { fn socket_connect( &self, socket: &Self::Socket, - address: SocketAddr, + address: SocketAddress, cancellation: &Cancellation, ) -> impl Future> + Send; } diff --git a/crates/executor/src/scheduler/socket_local_addr.rs b/crates/executor/src/scheduler/socket_local_addr.rs deleted file mode 100644 index d5bb080..0000000 --- a/crates/executor/src/scheduler/socket_local_addr.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Released under the MIT License. -// Copyright, 2026, by Samuel Williams. - -use super::SocketOperation; -use std::{io, net::SocketAddr}; - -/// Inspect the local IP address of a socket. -pub trait SocketLocalAddr: SocketOperation { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result; -} diff --git a/crates/executor/src/scheduler/socket_local_address.rs b/crates/executor/src/scheduler/socket_local_address.rs new file mode 100644 index 0000000..90a5ebb --- /dev/null +++ b/crates/executor/src/scheduler/socket_local_address.rs @@ -0,0 +1,11 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::SocketAddress; +use std::io; + +/// Inspect the local IP address of a socket. +pub trait SocketLocalAddress: SocketOperation { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result; +} diff --git a/crates/executor/src/scheduler/socket_peer_addr.rs b/crates/executor/src/scheduler/socket_peer_addr.rs deleted file mode 100644 index 3e14086..0000000 --- a/crates/executor/src/scheduler/socket_peer_addr.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Released under the MIT License. -// Copyright, 2026, by Samuel Williams. - -use super::SocketOperation; -use std::{io, net::SocketAddr}; - -/// Inspect the connected peer address of a socket. -pub trait SocketPeerAddr: SocketOperation { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result; -} diff --git a/crates/executor/src/scheduler/socket_peer_address.rs b/crates/executor/src/scheduler/socket_peer_address.rs new file mode 100644 index 0000000..c02a698 --- /dev/null +++ b/crates/executor/src/scheduler/socket_peer_address.rs @@ -0,0 +1,11 @@ +// Released under the MIT License. +// Copyright, 2026, by Samuel Williams. + +use super::SocketOperation; +use crate::SocketAddress; +use std::io; + +/// Inspect the connected peer address of a socket. +pub trait SocketPeerAddress: SocketOperation { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result; +} diff --git a/crates/executor/src/scheduler/socket_scheduler.rs b/crates/executor/src/scheduler/socket_scheduler.rs index 6d2ef0d..0072de3 100644 --- a/crates/executor/src/scheduler/socket_scheduler.rs +++ b/crates/executor/src/scheduler/socket_scheduler.rs @@ -2,8 +2,8 @@ // Copyright, 2026, by Samuel Williams. use super::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketPeerAddr, SocketRead, SocketShutdown, SocketWait, SocketWrite, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketPeerAddress, SocketRead, SocketShutdown, SocketWait, SocketWrite, }; /// Convenience aggregate for the core socket operations provided by a scheduler. @@ -26,8 +26,8 @@ pub trait SocketScheduler: + SocketOpen + SocketBind + SocketListen - + SocketLocalAddr - + SocketPeerAddr + + SocketLocalAddress + + SocketPeerAddress + SocketConnect + SocketAccept + SocketRead @@ -42,8 +42,8 @@ impl SocketScheduler for S where + SocketOpen + SocketBind + SocketListen - + SocketLocalAddr - + SocketPeerAddr + + SocketLocalAddress + + SocketPeerAddress + SocketConnect + SocketAccept + SocketRead diff --git a/crates/executor/src/scheduler/socketry/operations.rs b/crates/executor/src/scheduler/socketry/operations.rs index 3b4c5e5..8989c60 100644 --- a/crates/executor/src/scheduler/socketry/operations.rs +++ b/crates/executor/src/scheduler/socketry/operations.rs @@ -14,8 +14,8 @@ use crate::{ FileTruncate, FileWrite, FileWriteAt, }; use crate::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketOperation, SocketPeerAddress, SocketRead, SocketRecvMsg, SocketSendMsg, SocketShutdown, SocketWait, SocketWrite, }; use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; @@ -227,15 +227,15 @@ impl SocketListen for SchedulerHandle { } } -impl SocketLocalAddr for SchedulerHandle { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { - self.selector()?.socket_local_addr(socket) +impl SocketLocalAddress for SchedulerHandle { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result { + self.selector()?.socket_local_address(socket) } } -impl SocketPeerAddr for SchedulerHandle { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { - self.selector()?.socket_peer_addr(socket) +impl SocketPeerAddress for SchedulerHandle { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result { + self.selector()?.socket_peer_address(socket) } } @@ -634,15 +634,15 @@ impl SocketListen for Scheduler { } } -impl SocketLocalAddr for Scheduler { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { - self.handle.socket_local_addr(socket) +impl SocketLocalAddress for Scheduler { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result { + self.handle.socket_local_address(socket) } } -impl SocketPeerAddr for Scheduler { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { - self.handle.socket_peer_addr(socket) +impl SocketPeerAddress for Scheduler { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result { + self.handle.socket_peer_address(socket) } } diff --git a/crates/executor/src/scheduler/tokio.rs b/crates/executor/src/scheduler/tokio.rs index b9867e7..92dabfe 100644 --- a/crates/executor/src/scheduler/tokio.rs +++ b/crates/executor/src/scheduler/tokio.rs @@ -25,8 +25,8 @@ use crate::{ }; use crate::{FileDescriptor, SyncMode}; use crate::{ - SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddr, - SocketOpen, SocketOperation, SocketPeerAddr, SocketRead, SocketRecvMsg, SocketSendMsg, + SocketAccept, SocketBind, SocketClose, SocketConnect, SocketListen, SocketLocalAddress, + SocketOpen, SocketOperation, SocketPeerAddress, SocketRead, SocketRecvMsg, SocketSendMsg, SocketShutdown, SocketWait, SocketWrite, }; use crate::{SocketMessage, SocketRecvFlags, SocketRecvResult, SocketSendFlags}; @@ -667,15 +667,15 @@ impl SocketListen for SchedulerHandle { } } -impl SocketLocalAddr for SchedulerHandle { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { +impl SocketLocalAddress for SchedulerHandle { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result { self.check_runtime(socket.runtime())?; socket.local_addr() } } -impl SocketPeerAddr for SchedulerHandle { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { +impl SocketPeerAddress for SchedulerHandle { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result { self.check_runtime(socket.runtime())?; socket.peer_addr() } @@ -1287,15 +1287,15 @@ impl SocketListen for Scheduler { } } -impl SocketLocalAddr for Scheduler { - fn socket_local_addr(&self, socket: &Self::Socket) -> io::Result { - self.handle.socket_local_addr(socket) +impl SocketLocalAddress for Scheduler { + fn socket_local_address(&self, socket: &Self::Socket) -> io::Result { + self.handle.socket_local_address(socket) } } -impl SocketPeerAddr for Scheduler { - fn socket_peer_addr(&self, socket: &Self::Socket) -> io::Result { - self.handle.socket_peer_addr(socket) +impl SocketPeerAddress for Scheduler { + fn socket_peer_address(&self, socket: &Self::Socket) -> io::Result { + self.handle.socket_peer_address(socket) } } diff --git a/crates/executor/src/socket_domain.rs b/crates/executor/src/socket_domain.rs index 6d4789c..03347c7 100644 --- a/crates/executor/src/socket_domain.rs +++ b/crates/executor/src/socket_domain.rs @@ -1,6 +1,8 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. +use crate::SocketAddress; + /// The address family of an IP socket. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SocketDomain { @@ -10,11 +12,11 @@ pub enum SocketDomain { Ipv6, } -impl From for SocketDomain { - fn from(address: std::net::SocketAddr) -> Self { +impl From for SocketDomain { + fn from(address: SocketAddress) -> Self { match address { - std::net::SocketAddr::V4(_) => Self::Ipv4, - std::net::SocketAddr::V6(_) => Self::Ipv6, + SocketAddress::V4(_) => Self::Ipv4, + SocketAddress::V6(_) => Self::Ipv6, } } } diff --git a/crates/executor/src/socket_message.rs b/crates/executor/src/socket_message.rs index 3080bfa..8ee81b9 100644 --- a/crates/executor/src/socket_message.rs +++ b/crates/executor/src/socket_message.rs @@ -1,11 +1,11 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use std::net::SocketAddr; +use crate::SocketAddress; /// Owned scatter/gather payload and optional destination for one send. `None` uses the connected peer. The operation returns this value on success, error, and cancellation without copying its payload. #[derive(Debug)] pub struct SocketMessage> { pub buffers: Vec, - pub address: Option, + pub address: Option, } diff --git a/crates/executor/src/socket_recv_result.rs b/crates/executor/src/socket_recv_result.rs index 03086ed..7cd4b26 100644 --- a/crates/executor/src/socket_recv_result.rs +++ b/crates/executor/src/socket_recv_result.rs @@ -1,14 +1,14 @@ // Released under the MIT License. // Copyright, 2026, by Samuel Williams. -use std::net::SocketAddr; +use crate::SocketAddress; /// Metadata from one receive. Only `bytes` bytes, spread across the buffers in order, were written; their initialized lengths remain unchanged. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SocketRecvResult { pub bytes: usize, - /// The sender when supplied by the OS, usually for datagrams. Stream receives may return `None`; use `socket_peer_addr` for the connected peer. - pub address: Option, + /// The sender when supplied by the OS, usually for datagrams. Stream receives may return `None`; use `socket_peer_address` for the connected peer. + pub address: Option, /// A datagram exceeded the total buffer length. With nonzero capacity, remaining bytes were discarded unless this was a peek. Zero-capacity receive behavior is OS-specific. `bytes` is the copied length, not the original datagram length. pub truncated: bool, /// The OS reported discarded ancillary data. This initial interface supplies no control buffer; ancillary data requires a separate capability. diff --git a/crates/executor/tests/contracts.rs b/crates/executor/tests/contracts.rs index 59ebaf3..5d70347 100644 --- a/crates/executor/tests/contracts.rs +++ b/crates/executor/tests/contracts.rs @@ -159,7 +159,7 @@ where .socket_bind(&reserved, address, &never) .await .unwrap(); - let reserved_address = scheduler.socket_local_addr(&reserved).unwrap(); + let reserved_address = scheduler.socket_local_address(&reserved).unwrap(); let another_reserved = scheduler .socket_open(SocketDomain::Ipv4, SocketType::Stream, None, &never) .await @@ -169,7 +169,7 @@ where .await .unwrap(); assert_ne!( - scheduler.socket_local_addr(&another_reserved).unwrap(), + scheduler.socket_local_address(&another_reserved).unwrap(), reserved_address ); let duplicate = scheduler @@ -202,7 +202,7 @@ where .socket_bind(&ipv6, "[::1]:0".parse().unwrap(), &never) .await .unwrap(); - assert!(scheduler.socket_local_addr(&ipv6).unwrap().is_ipv6()); + assert!(scheduler.socket_local_address(&ipv6).unwrap().is_ipv6()); scheduler .socket_bind(&listener, address, &never) .await @@ -211,7 +211,7 @@ where .socket_listen(&listener, 128, &never) .await .unwrap(); - let address = scheduler.socket_local_addr(&listener).unwrap(); + let address = scheduler.socket_local_address(&listener).unwrap(); assert_ne!(address.port(), 0); let duplicate = scheduler .socket_open( @@ -269,10 +269,10 @@ where .await .unwrap(); let (server, peer) = scheduler.socket_accept(&listener, &never).await.unwrap(); - assert_eq!(scheduler.socket_local_addr(&server).unwrap(), address); - assert_eq!(scheduler.socket_peer_addr(&client).unwrap(), address); - assert_eq!(scheduler.socket_local_addr(&client).unwrap(), peer); - assert_eq!(scheduler.socket_peer_addr(&server).unwrap(), peer); + assert_eq!(scheduler.socket_local_address(&server).unwrap(), address); + assert_eq!(scheduler.socket_peer_address(&client).unwrap(), address); + assert_eq!(scheduler.socket_local_address(&client).unwrap(), peer); + assert_eq!(scheduler.socket_peer_address(&server).unwrap(), peer); scheduler .socket_wait(&client, Interest::Writable, &never) .await @@ -336,7 +336,7 @@ where .socket_bind(&receiver, "127.0.0.1:0".parse().unwrap(), &never) .await .unwrap(); - let address = scheduler.socket_local_addr(&receiver).unwrap(); + let address = scheduler.socket_local_address(&receiver).unwrap(); scheduler .socket_connect(&datagram, address, &never) .await @@ -382,7 +382,7 @@ where scheduler .socket_connect( &zero_sender, - scheduler.socket_local_addr(&zero_receiver).unwrap(), + scheduler.socket_local_address(&zero_receiver).unwrap(), &never, ) .await diff --git a/crates/executor/tests/io.rs b/crates/executor/tests/io.rs index 6e83dc6..49c9eab 100644 --- a/crates/executor/tests/io.rs +++ b/crates/executor/tests/io.rs @@ -12,8 +12,8 @@ use socketry_executor::{ SocketType, Spawn, }; use socketry_executor::{ - SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddr, SocketOpen, - SocketPeerAddr, SocketRead, SocketWait, SocketWrite, + SocketAccept, SocketBind, SocketConnect, SocketListen, SocketLocalAddress, SocketOpen, + SocketPeerAddress, SocketRead, SocketWait, SocketWrite, }; use socketry_executor::{SocketRegistration, SyncMode}; use std::fs::File as StdFile; @@ -89,7 +89,7 @@ async fn socket_variants(scheduler: &S) { .socket_bind(&ipv6, "[::1]:0".parse().unwrap(), &NEVER) .await .unwrap(); - assert!(scheduler.socket_local_addr(&ipv6).unwrap().is_ipv6()); + assert!(scheduler.socket_local_address(&ipv6).unwrap().is_ipv6()); let datagram = scheduler .socket_open( @@ -104,7 +104,7 @@ async fn socket_variants(scheduler: &S) { .socket_connect(&datagram, "127.0.0.1:9".parse().unwrap(), &NEVER) .await .unwrap(); - assert!(scheduler.socket_peer_addr(&datagram).is_ok()); + assert!(scheduler.socket_peer_address(&datagram).is_ok()); assert!(scheduler.socket_listen(&datagram, 1, &NEVER).await.is_err()); assert!( scheduler @@ -214,7 +214,7 @@ where .unwrap(); let sender = UdpSocket::bind("127.0.0.1:0").unwrap(); sender - .connect(scheduler.socket_local_addr(&socket).unwrap()) + .connect(scheduler.socket_local_address(&socket).unwrap()) .unwrap(); scheduler .socket_connect(&socket, sender.local_addr().unwrap(), &NEVER) @@ -519,17 +519,17 @@ mod native { assert_eq!( handle - .socket_local_addr(®istered_listener) + .socket_local_address(®istered_listener) .unwrap_err() .kind(), io::ErrorKind::BrokenPipe ); assert_eq!( - handle.socket_local_addr(&socket).unwrap_err().kind(), + handle.socket_local_address(&socket).unwrap_err().kind(), io::ErrorKind::BrokenPipe ); assert_eq!( - handle.socket_peer_addr(&socket).unwrap_err().kind(), + handle.socket_peer_address(&socket).unwrap_err().kind(), io::ErrorKind::BrokenPipe ); let executor = Scheduler::with_workers(1).unwrap(); @@ -964,15 +964,15 @@ mod tokio_adapter { ); assert_eq!( - handle.socket_local_addr(&listener).unwrap_err().kind(), + handle.socket_local_address(&listener).unwrap_err().kind(), io::ErrorKind::BrokenPipe ); assert_eq!( - handle.socket_local_addr(&socket).unwrap_err().kind(), + handle.socket_local_address(&socket).unwrap_err().kind(), io::ErrorKind::BrokenPipe ); assert_eq!( - handle.socket_peer_addr(&socket).unwrap_err().kind(), + handle.socket_peer_address(&socket).unwrap_err().kind(), io::ErrorKind::BrokenPipe ); runtime.block_on(async { @@ -1194,11 +1194,11 @@ mod tokio_adapter { io::ErrorKind::InvalidInput ); assert_eq!( - second.socket_local_addr(&socket).unwrap_err().kind(), + second.socket_local_address(&socket).unwrap_err().kind(), io::ErrorKind::InvalidInput ); assert_eq!( - second.socket_peer_addr(&socket).unwrap_err().kind(), + second.socket_peer_address(&socket).unwrap_err().kind(), io::ErrorKind::InvalidInput ); }); diff --git a/releases.md b/releases.md index dabf260..4c0c017 100644 --- a/releases.md +++ b/releases.md @@ -2,6 +2,8 @@ ## Unreleased +- Export `SocketAddress` as the standard-library `SocketAddr` type. Rename address inspection to `SocketLocalAddress::socket_local_address` and `SocketPeerAddress::socket_peer_address`; migrate abbreviated trait and method names in implementations and callers. Public socket operations and address/message fields use `SocketAddress` without requiring conversions. + - Split socket capabilities into `SocketOperation` for the shared resource type and individual core operation traits, with `SocketScheduler` as an automatic aggregate. Separate message send/receive and typed option get/set, retaining `SocketMessages` and `SocketOptions` as automatic aggregates. Optional operations and registration depend only on `SocketOperation`. Implementors migrate to the individual traits; concrete callers import operation traits, qualified resource paths use `SocketOperation::Socket`, and extension consumers explicitly require their setup and cleanup capabilities. Preserve backend behavior and update examples, migration guidance, and conformance tests. - Split file capabilities into `FileOperation` for the shared resource type and individual operation traits, with `FileScheduler` as a blanket convenience aggregate. Rename the broad `File` and `Socket` traits to `FileScheduler` and `SocketScheduler`. `FileStat` owns the metadata type; optional `FileAllocate` and `FileAdvice` no longer require all file operations. Implementors must migrate `impl File` to the individual traits and rename `impl Socket` to `impl SocketScheduler`; concrete file callers must import the operation traits. Fully qualified associated types move to `FileOperation::File`, `FileStat::Metadata`, and `SocketScheduler::Socket`. Update native, Tokio, simulated implementations, examples, and capability-focused contracts. diff --git a/src/lib.rs b/src/lib.rs index cc4cf6f..1698aee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,13 +12,13 @@ pub use socketry_executor::{ FileAllocate, FileClose, FileDescriptor, FileMetadata, FileOpen, FileOperation, FileOptions, FileRead, FileReadAt, FileScheduler, FileSeek, FileStat, FileSync, FileTruncate, FileType, FileWrite, FileWriteAt, Interest, IoBuf, IoBufMut, Notify, OperationError, OperationResult, - Scheduler, SchedulerHandle, Signals, SocketAccept, SocketBind, SocketClose, SocketConnect, - SocketDomain, SocketGetOption, SocketKeepalive, SocketListen, SocketLocalAddr, SocketMessage, - SocketMessages, SocketOpen, SocketOperation, SocketOption, SocketOptions, SocketPeerAddr, - SocketProtocol, SocketRead, SocketRecvFlags, SocketRecvMsg, SocketRecvResult, - SocketRegistration, SocketReuseAddress, SocketScheduler, SocketSendFlags, SocketSendMsg, - SocketSetOption, SocketShutdown, SocketType, SocketWait, SocketWrite, Spawn, SpawnError, - SyncMode, Task, TaskError, TaskHandle, TcpNoDelay, defer_cancel, yield_now, + Scheduler, SchedulerHandle, Signals, SocketAccept, SocketAddress, SocketBind, SocketClose, + SocketConnect, SocketDomain, SocketGetOption, SocketKeepalive, SocketListen, + SocketLocalAddress, SocketMessage, SocketMessages, SocketOpen, SocketOperation, SocketOption, + SocketOptions, SocketPeerAddress, SocketProtocol, SocketRead, SocketRecvFlags, SocketRecvMsg, + SocketRecvResult, SocketRegistration, SocketReuseAddress, SocketScheduler, SocketSendFlags, + SocketSendMsg, SocketSetOption, SocketShutdown, SocketType, SocketWait, SocketWrite, Spawn, + SpawnError, SyncMode, Task, TaskError, TaskHandle, TcpNoDelay, defer_cancel, yield_now, }; #[cfg(all(unix, feature = "signals", any(feature = "native", feature = "tokio")))]