Skip to main content

windows_overlapped_io_sys/
endpoint.rs

1// Copyright (c) 2026 Mike Grier
2//! Owned overlapped-capable endpoints and controlled association provenance.
3//!
4//! An endpoint is a Windows handle that has been established for overlapped I/O
5//! but not yet associated with a completion backend. Association is a later,
6//! consuming transition; this module models only ownership and the provenance
7//! that must hold before an endpoint may be trusted for safe completion routing.
8
9use std::fs::OpenOptions;
10use std::io;
11use std::os::windows::fs::OpenOptionsExt;
12use std::os::windows::io::{AsHandle, BorrowedHandle, OwnedHandle};
13use std::path::Path;
14
15/// The Win32 `FILE_FLAG_OVERLAPPED` flag. Changing this value is a breaking change.
16const FILE_FLAG_OVERLAPPED: u32 = 0x4000_0000;
17
18/// The `SetFileCompletionNotificationModes` flag bits.
19///
20/// `windows-sys` does not export these, so they are named here rather than
21/// written as bare literals at the call site. Changing either value is a
22/// breaking change.
23pub(crate) mod notification_flags {
24    /// `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS`.
25    pub(crate) const SKIP_COMPLETION_PORT_ON_SUCCESS: u8 = 0x1;
26    /// `FILE_SKIP_SET_EVENT_ON_HANDLE`.
27    pub(crate) const SKIP_SET_EVENT_ON_HANDLE: u8 = 0x2;
28}
29
30/// Which completion-notification shortcuts a handle should take.
31///
32/// These are the two `SetFileCompletionNotificationModes` flags. Both trade a
33/// notification the I/O Manager would otherwise deliver for the cost of not
34/// having it, so both are opt-in per endpoint rather than anything this crate
35/// chooses on a caller's behalf.
36///
37/// Every field defaults to `false`, which is the handle's ordinary behaviour.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
39pub struct NotificationModes {
40    /// Suppress the completion packet for an operation that succeeds
41    /// immediately, rather than queueing one the caller must still dequeue.
42    ///
43    /// This is the throughput knob. Ordinarily an IOCP-associated overlapped
44    /// handle gets a packet for *every* completed request, including one that
45    /// returned success without ever returning `ERROR_IO_PENDING` -- see
46    /// [`crate::Issued::Pending`]. Setting this removes the queue, the dequeue,
47    /// and the worker wakeup for each such operation, which is a real win where
48    /// operations frequently complete synchronously (cached reads, small socket
49    /// sends, loopback) and changes nothing for ones that genuinely go
50    /// asynchronous.
51    ///
52    /// The cost is that a submission now has two possible shapes, so every
53    /// adapter on this endpoint reports [`crate::Started::Completed`] on the
54    /// synchronous path instead of a claim-later token. A caller that does not
55    /// handle that arm will lose results.
56    pub skip_completion_port_on_success: bool,
57    /// Do not set the file object's own event for a request that returns
58    /// success, or that returns `ERROR_IO_PENDING` from an asynchronous call.
59    ///
60    /// Independent of the completion port: it concerns the handle's internal
61    /// event, which completion-port-driven code does not wait on. An event
62    /// supplied explicitly in the `OVERLAPPED` is still signalled.
63    ///
64    /// **Do not set this on an endpoint destined for
65    /// [`crate::BlockingEndpoint`]**, which waits on exactly that internal
66    /// event: suppressing it leaves the wait with nothing to wake it.
67    pub skip_set_event_on_handle: bool,
68}
69
70/// An overlapped-capable endpoint that has not yet been associated with a
71/// completion backend.
72///
73/// The endpoint owns its handle and closes it on drop. It is intentionally not
74/// `Clone`: a second owner could route completions through a duplicate handle
75/// and break the single-association invariant that the completion backends rely
76/// on.
77#[derive(Debug)]
78pub struct UnassociatedEndpoint {
79    handle: OwnedHandle,
80    /// What [`UnassociatedEndpoint::set_notification_modes`] has established on
81    /// the handle. Carried with the endpoint, and onward into association,
82    /// because the submission seam has to answer "will a completion packet
83    /// arrive" and skip-on-success is what changes that answer.
84    modes: NotificationModes,
85}
86
87impl UnassociatedEndpoint {
88    /// Open a filesystem path (file, directory, or device) for overlapped I/O
89    /// and wrap it as an endpoint, establishing overlapped provenance safely.
90    ///
91    /// The handle is always opened with `FILE_FLAG_OVERLAPPED`, so the overlapped
92    /// invariant holds without the unsafe [`UnassociatedEndpoint::assume_overlapped`]
93    /// seam. Pass any additional `FILE_FLAG_*` bits in `extra_flags` -- for
94    /// example the backup-semantics flag to open a directory handle for change
95    /// notifications. Set `read` and/or `write` for the access the operations
96    /// will need.
97    ///
98    /// # Errors
99    ///
100    /// Returns any error encountered opening the path.
101    pub fn open(
102        path: impl AsRef<Path>,
103        read: bool,
104        write: bool,
105        extra_flags: u32,
106    ) -> io::Result<Self> {
107        let file = OpenOptions::new()
108            .read(read)
109            .write(write)
110            .custom_flags(FILE_FLAG_OVERLAPPED | extra_flags)
111            .open(path)?;
112        // SAFETY: the handle was just opened with FILE_FLAG_OVERLAPPED, is fresh
113        // and unassociated, has no duplicates, and ownership moves in exclusively.
114        Ok(unsafe { Self::assume_overlapped(OwnedHandle::from(file)) })
115    }
116
117    /// Wrap an owned handle whose overlapped provenance the caller vouches for.
118    ///
119    /// This is the narrow unsafe extensibility seam for handles the crate cannot
120    /// create itself -- sockets, devices, or handles obtained elsewhere. For
121    /// filesystem paths, prefer the safe [`UnassociatedEndpoint::open`].
122    ///
123    /// # Safety
124    ///
125    /// The caller guarantees that:
126    ///
127    /// - the handle was opened for overlapped I/O (for example a file opened
128    ///   with `FILE_FLAG_OVERLAPPED`, or an overlapped-capable socket handle);
129    /// - the handle is not already associated with any completion port;
130    /// - no duplicate of the handle exists that could generate competing
131    ///   completions for the same operations;
132    /// - ownership is transferred exclusively into the returned endpoint; and
133    /// - **any completion-notification mode already set on the handle is
134    ///   declared** through [`UnassociatedEndpoint::set_notification_modes`].
135    ///   The endpoint is assumed to be in the default mode, and the submission
136    ///   seam relies on that to decide whether a completion packet will arrive;
137    ///   a handle silently in `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` mode would
138    ///   have its synchronous successes reported as pending, leaving operations
139    ///   outstanding forever. Re-declaring is safe: the call is additive and
140    ///   idempotent.
141    #[must_use]
142    pub unsafe fn assume_overlapped(handle: OwnedHandle) -> Self {
143        Self {
144            handle,
145            modes: NotificationModes::default(),
146        }
147    }
148
149    /// Borrow the underlying handle for the duration of a native call.
150    ///
151    /// The borrow cannot outlive the endpoint and does not confer ownership, so
152    /// it cannot be used to establish a competing completion association.
153    #[must_use]
154    pub fn handle(&self) -> BorrowedHandle<'_> {
155        self.handle.as_handle()
156    }
157
158    /// The completion-notification modes established on this endpoint.
159    #[must_use]
160    pub fn notification_modes(&self) -> NotificationModes {
161        self.modes
162    }
163
164    /// Consume the endpoint and recover the owned handle.
165    ///
166    /// This abandons the overlapped-endpoint invariants; the recovered handle is
167    /// an ordinary [`OwnedHandle`] again. Any notification mode set on it stays
168    /// set -- Win32 offers no way to clear one -- so a handle rewrapped later
169    /// must re-declare it.
170    #[must_use]
171    pub fn into_handle(self) -> OwnedHandle {
172        self.handle
173    }
174
175    /// Set this endpoint's completion-notification modes before it is
176    /// associated with a backend.
177    ///
178    /// Setting the mode here rather than after association is deliberate: it is
179    /// an attribute of the endpoint's provenance, so an endpoint carries it from
180    /// the moment it exists and no operation can ever be issued against a handle
181    /// whose notification behaviour is still in question.
182    ///
183    /// Passing every field `false` is a no-op call, not a reset. **A mode cannot
184    /// be removed once set** -- that is a Win32 property of the handle, not a
185    /// limitation of this wrapper -- so a second call can only ever add modes.
186    ///
187    /// [`NotificationModes::skip_completion_port_on_success`] takes effect only
188    /// once all three of Win32's conditions hold: the handle is associated with
189    /// a completion port, it was opened for asynchronous I/O (this type
190    /// guarantees that), and the request returns success immediately. Until the
191    /// association exists the flag is simply inert, which is why setting it
192    /// first is safe.
193    ///
194    /// Sockets set their modes elsewhere, on `AssociatedSocket::set_notification_modes`
195    /// (behind the `socket` feature, so not always linkable here): they have no
196    /// unassociated stage to hang provenance on, and Win32 additionally
197    /// restricts skip-on-success to Layered Service Providers that return IFS
198    /// handles, so that setter probes the socket's own provider rather than
199    /// setting the flag blind.
200    ///
201    /// # Errors
202    ///
203    /// Returns any error from `SetFileCompletionNotificationModes`, which
204    /// reports `ERROR_INVALID_PARAMETER` for a handle whose device does not
205    /// support the requested mode.
206    pub fn set_notification_modes(&mut self, modes: NotificationModes) -> io::Result<()> {
207        use std::os::windows::io::AsRawHandle;
208
209        let mut flags = 0_u8;
210        if modes.skip_completion_port_on_success {
211            flags |= notification_flags::SKIP_COMPLETION_PORT_ON_SUCCESS;
212        }
213        if modes.skip_set_event_on_handle {
214            flags |= notification_flags::SKIP_SET_EVENT_ON_HANDLE;
215        }
216        // SAFETY: a live handle this endpoint owns, and a flags byte built only
217        // from the two documented bits. The call sets a handle attribute and
218        // starts no I/O, so it borrows nothing beyond this statement.
219        let ok = unsafe {
220            windows_sys::Win32::Storage::FileSystem::SetFileCompletionNotificationModes(
221                self.handle.as_raw_handle(),
222                flags,
223            )
224        };
225        if ok == 0 {
226            return Err(io::Error::last_os_error());
227        }
228        // Accumulated, never replaced: Win32 cannot clear a mode, so what this
229        // endpoint records has to be the union of everything ever set on it.
230        self.modes.skip_completion_port_on_success |= modes.skip_completion_port_on_success;
231        self.modes.skip_set_event_on_handle |= modes.skip_set_event_on_handle;
232        Ok(())
233    }
234}
235
236#[cfg(test)]
237mod tests;