include/boost/corosio/native/detail/iocp/win_resolver.hpp

0.0% Lines (0 / 6) 0.0% Branches (0 / 8) 0.0% Functions (0 / 1)
win_resolver.hpp
f(x) Functions (1)
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco ([email protected])
3 // Copyright (c) 2026 Steve Gerbino
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RESOLVER_HPP
12 #define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RESOLVER_HPP
13
14 #include <boost/corosio/detail/platform.hpp>
15
16 #if BOOST_COROSIO_HAS_IOCP
17
18 #include <boost/corosio/detail/config.hpp>
19
20 // GetAddrInfoExW requires Windows 8 or later
21 #if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0602)
22 #error "corosio resolver requires Windows 8 or later (_WIN32_WINNT >= 0x0602)"
23 #endif
24
25 #include <boost/corosio/detail/scheduler.hpp>
26 #include <boost/corosio/detail/object_ref.hpp>
27 #include <boost/corosio/detail/thread_pool.hpp>
28 #include <boost/corosio/endpoint.hpp>
29 #include <boost/corosio/resolver.hpp>
30 #include <boost/capy/continuation.hpp>
31 #include <boost/capy/ex/executor_ref.hpp>
32 #include <boost/capy/ex/execution_context.hpp>
33 #include <boost/corosio/detail/intrusive.hpp>
34
35 #include <boost/corosio/native/detail/iocp/win_windows.hpp>
36 #include <boost/corosio/native/detail/iocp/win_overlapped_op.hpp>
37 #include <boost/corosio/native/detail/iocp/win_mutex.hpp>
38 #include <boost/corosio/native/detail/iocp/win_wsa_init.hpp>
39
40 #include <boost/corosio/native/detail/endpoint_convert.hpp>
41 #include <boost/corosio/native/detail/make_err.hpp>
42 #include <boost/corosio/detail/dispatch_coro.hpp>
43
44 #include <ws2tcpip.h>
45
46 #include <atomic>
47 #include <cstring>
48 #include <memory>
49 #include <string>
50
51 // MinGW may not have GetAddrInfoExCancel declared
52 #if defined(__MINGW32__) || defined(__MINGW64__)
53 extern "C"
54 {
55 INT WSAAPI GetAddrInfoExCancel(LPHANDLE lpHandle);
56 }
57 #endif
58
59 /*
60 Windows IOCP Resolver Service
61 =============================
62
63 This header declares the Windows resolver implementation.
64
65 Forward Resolution (GetAddrInfoExW)
66 -----------------------------------
67 Uses the native async GetAddrInfoExW API which provides completion
68 callbacks that integrate with IOCP. This avoids worker threads for
69 forward DNS lookups.
70
71 Reverse Resolution (GetNameInfoW)
72 ---------------------------------
73 Unlike GetAddrInfoExW, GetNameInfoW has no async variant. Reverse
74 resolution dispatches the blocking call to the shared
75 resolver_thread_pool service.
76
77 Class Hierarchy
78 ---------------
79 - win_resolver_service (execution_context::service)
80 - Owns all win_resolver instances through a recycling object_pool
81 - Coordinates with win_scheduler for work tracking
82 - win_resolver (one per resolver object)
83 - Contains embedded resolve_op and reverse_resolve_op
84 - Kept alive during I/O by each op's intrusive object_ref_
85 - resolve_op (overlapped_op subclass)
86 - OVERLAPPED base enables IOCP integration
87 - Static completion() callback invoked by Windows
88 - reverse_resolve_op (overlapped_op subclass)
89 - Used by pool thread for reverse resolution
90
91 Shutdown
92 --------
93 The resolver service cancels all resolvers; each then stays alive
94 until its own in-flight completion drains, same as every other
95 pooled impl. The thread pool service shuts down separately via
96 execution_context service ordering, joining all worker threads.
97
98 Cancellation
99 ------------
100 GetAddrInfoExCancel() can cancel in-progress forward resolutions.
101 Reverse resolution checks an atomic cancelled flag after GetNameInfoW
102 returns. The cancel() method sets flags and calls the Windows cancel API.
103
104 Single-Inflight Constraint
105 --------------------------
106 Each resolver has ONE embedded op_ for forward resolution and ONE
107 reverse_op_ for reverse resolution. Concurrent operations of the same
108 type on the same resolver would corrupt state. Users must serialize
109 operations per-resolver.
110 */
111
112 /*
113 Windows IOCP Resolver Implementation
114 ====================================
115
116 See above for architecture overview.
117
118 Forward Resolution (GetAddrInfoExW)
119 -----------------------------------
120 1. resolve() converts host/service to wide strings (Windows API requirement)
121 2. GetAddrInfoExW() is called with our completion callback
122 3. If it returns WSA_IO_PENDING, completion comes later via callback
123 4. If it returns immediately (0 or error), we post completion manually
124 5. completion() callback stores error, posts op
125 6. op_() resumes the coroutine with results or error
126
127 Reverse Resolution (GetNameInfoW)
128 ---------------------------------
129 Unlike GetAddrInfoExW, GetNameInfoW has no async variant. The blocking
130 call is dispatched to the shared resolver_thread_pool:
131 1. reverse_resolve() posts work to the thread pool
132 2. Pool thread calls GetNameInfoW() (blocking)
133 3. Pool thread converts wide results to UTF-8 via WideCharToMultiByte
134 4. Pool thread posts completion to scheduler
135 5. op_() resumes the coroutine with results
136
137 String Conversion
138 -----------------
139 Windows APIs require wide strings. We use MultiByteToWideChar for
140 UTF-8 to UTF-16 and WideCharToMultiByte for UTF-16 to UTF-8.
141
142 Work Tracking
143 -------------
144 work_started() is called before async operations to keep io_context alive.
145 work_finished() is called when the posted operation completes or is
146 drained at shutdown.
147 */
148
149 namespace boost::corosio::detail {
150
151 class win_resolver_service;
152 class win_resolver;
153
154 /** Resolve operation state. */
155 struct resolve_op : overlapped_op
156 {
157 ADDRINFOEXW* results = nullptr;
158
159 // GetAddrInfoExW reads these past the frame that starts the lookup.
160 ADDRINFOEXW hints{};
161
162 HANDLE cancel_handle = nullptr;
163 std::vector<endpoint>* out = nullptr;
164 std::string host;
165 std::string service;
166 std::wstring host_w;
167 std::wstring service_w;
168 win_resolver* impl = nullptr;
169
170 /** Completion callback for GetAddrInfoExW. */
171 static void CALLBACK completion(DWORD dwError, DWORD bytes, OVERLAPPED* ov);
172
173 /// Cancel the in-flight lookup on a stop request.
174 static void do_cancel_impl(overlapped_op* base) noexcept;
175
176 static void do_complete(
177 void* owner,
178 scheduler_op* base,
179 std::uint32_t bytes,
180 std::uint32_t error);
181
182 resolve_op() noexcept;
183 };
184
185 /** Reverse resolve operation state. */
186 struct reverse_resolve_op : overlapped_op
187 {
188 endpoint_name* result_out = nullptr;
189 endpoint ep;
190 reverse_flags flags = reverse_flags::none;
191 std::string stored_host;
192 std::string stored_service;
193 int gai_error = 0;
194 win_resolver* impl = nullptr;
195
196 static void do_complete(
197 void* owner,
198 scheduler_op* base,
199 std::uint32_t bytes,
200 std::uint32_t error);
201
202 reverse_resolve_op() noexcept;
203 };
204
205 /** Resolver implementation for IOCP-based async DNS.
206
207 Each resolver instance contains a single embedded operation object (op_)
208 that is reused for each resolve() call. This design avoids per-operation
209 heap allocation but imposes a critical constraint:
210
211 @par Single-Inflight Contract
212
213 Only ONE resolve operation may be in progress at a time per resolver
214 instance. Calling resolve() while a previous resolve() is still pending
215 results in undefined behavior:
216
217 - The new call overwrites op_ fields (host, service, coroutine handle)
218 - The pending GetAddrInfoExW callback reads corrupted state
219 - The wrong coroutine may be resumed, or resumed multiple times
220 - Data races occur on non-atomic op_ members
221
222 @par Safe Usage Patterns
223
224 @code
225 // CORRECT: Sequential resolves
226 auto [ec1, r1] = co_await resolver.resolve("host1", "80");
227 auto [ec2, r2] = co_await resolver.resolve("host2", "80");
228
229 // CORRECT: Parallel resolves with separate resolver instances
230 resolver r1(ctx), r2(ctx);
231 auto [ec1, res1] = co_await r1.resolve("host1", "80"); // in one coroutine
232 auto [ec2, res2] = co_await r2.resolve("host2", "80"); // in another
233
234 // WRONG: Concurrent resolves on same resolver
235 // These may run concurrently if launched in parallel - UNDEFINED BEHAVIOR
236 auto f1 = resolver.resolve("host1", "80");
237 auto f2 = resolver.resolve("host2", "80"); // BAD: overlaps with f1
238 @endcode
239
240 @par Thread Safety
241 Distinct objects: Safe.
242 Shared objects: Unsafe. See single-inflight contract above.
243
244 @note Internal implementation detail. Users interact with resolver class.
245 */
246 class win_resolver final
247 : public resolver::implementation
248 , public intrusive_list<win_resolver>::node
249 {
250 friend class win_resolver_service;
251 friend struct resolve_op;
252 friend struct reverse_resolve_op;
253
254 public:
255 /// Embedded pool work item for thread pool dispatch.
256 struct pool_op : pool_work_item
257 {
258 /// Resolver that owns this work item.
259 win_resolver* resolver_ = nullptr;
260
261 /// Prevent impl destruction while work is in flight.
262 detail::object_ref ref_;
263 };
264
265 explicit win_resolver(win_resolver_service& svc) noexcept;
266
267 /// Recycle into the owning service's pool. Defined out-of-line
268 /// after `win_resolver_service` is complete (needs `svc_.pool_`).
269 void retire() noexcept override;
270
271 /** Reset recycled state for reuse.
272
273 Does not clear `host`/`service`/`stored_host`/`stored_service`
274 itself -- each op's own `reset()` (called at the top of the
275 next `resolve()`/`reverse_resolve()`) already does that,
276 preserving allocated capacity. That same `reset()` re-seeds
277 the op's `OVERLAPPED` fields, so they are not zeroed here.
278
279 @pre refs_ == 0, no wait in flight.
280 */
281 void reuse() noexcept;
282
283 std::coroutine_handle<> resolve(
284 capy::continuation&,
285 capy::executor_ref,
286 std::string_view host,
287 std::string_view service,
288 resolve_flags flags,
289 std::stop_token,
290 std::error_code*,
291 std::vector<endpoint>*) override;
292
293 std::coroutine_handle<> reverse_resolve(
294 capy::continuation&,
295 capy::executor_ref,
296 endpoint const& ep,
297 reverse_flags flags,
298 std::stop_token,
299 std::error_code*,
300 endpoint_name*) override;
301
302 void cancel() noexcept override;
303
304 resolve_op op_;
305 reverse_resolve_op reverse_op_;
306
307 /// Pool work item for reverse resolution.
308 pool_op reverse_pool_op_;
309
310 /// Execute blocking `GetNameInfoW()` on a pool thread.
311 static void do_reverse_resolve_work(pool_work_item*) noexcept;
312
313 /// Destroy a reverse resolution the pool shut down before running.
314 static void destroy_reverse_resolve_work(pool_work_item*) noexcept;
315
316 private:
317 /// Cancel the in-flight forward lookup, if any.
318 void cancel_lookup() noexcept;
319
320 // The cancel handle dies when the completion callback is entered, so
321 // cancel() and that callback must not both claim it. Held across the
322 // claim only: GetAddrInfoExCancel can wait on the callback.
323 win_mutex cancel_mutex_;
324
325 win_resolver_service& svc_;
326 };
327
328 inline void
329 ✗ win_resolver::reuse() noexcept
330 {
331
0/2
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 5 not taken.
✗ BOOST_COROSIO_ASSERT(!op_.stop_cb);
332
0/2
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 9 not taken.
✗ BOOST_COROSIO_ASSERT(!reverse_op_.stop_cb);
333
0/2
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
✗ BOOST_COROSIO_ASSERT(op_.results == nullptr);
334
0/2
✗ Branch 13 → 14 not taken.
✗ Branch 13 → 15 not taken.
✗ BOOST_COROSIO_ASSERT(op_.cancel_handle == nullptr);
335 ✗ }
336
337 } // namespace boost::corosio::detail
338
339 #endif // BOOST_COROSIO_HAS_IOCP
340
341 #endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RESOLVER_HPP
342