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;