1 +
//
 
2 +
// Copyright (c) 2026 Steve Gerbino
 
3 +
//
 
4 +
// Distributed under the Boost Software License, Version 1.0. (See accompanying
 
5 +
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
 
6 +
//
 
7 +
// Official repository: https://github.com/cppalliance/corosio
 
8 +
//
 
9 +

 
10 +
#ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP
 
11 +
#define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP
 
12 +

 
13 +
#include <boost/corosio/detail/platform.hpp>
 
14 +

 
15 +
#if BOOST_COROSIO_POSIX
 
16 +

 
17 +
#include <boost/corosio/detail/config.hpp>
 
18 +
#include <boost/corosio/resolver.hpp>
 
19 +
#include <boost/capy/ex/execution_context.hpp>
 
20 +

 
21 +
#include <boost/corosio/detail/endpoint_convert.hpp>
 
22 +
#include <boost/corosio/detail/intrusive.hpp>
 
23 +
#include <boost/corosio/detail/dispatch_coro.hpp>
 
24 +
#include <boost/corosio/detail/scheduler_op.hpp>
 
25 +

 
26 +
#include <boost/corosio/detail/scheduler.hpp>
 
27 +
#include <boost/corosio/resolver_results.hpp>
 
28 +
#include <boost/capy/ex/executor_ref.hpp>
 
29 +
#include <coroutine>
 
30 +
#include <boost/capy/error.hpp>
 
31 +

 
32 +
#include <netdb.h>
 
33 +
#include <netinet/in.h>
 
34 +
#include <sys/socket.h>
 
35 +

 
36 +
#include <atomic>
 
37 +
#include <cassert>
 
38 +
#include <condition_variable>
 
39 +
#include <cstring>
 
40 +
#include <memory>
 
41 +
#include <mutex>
 
42 +
#include <optional>
 
43 +
#include <stop_token>
 
44 +
#include <string>
 
45 +
#include <thread>
 
46 +
#include <unordered_map>
 
47 +
#include <vector>
 
48 +

 
49 +
/*
 
50 +
    POSIX Resolver Service
 
51 +
    ======================
 
52 +

 
53 +
    POSIX getaddrinfo() is a blocking call that cannot be monitored with
 
54 +
    epoll/kqueue/io_uring. We use a worker thread approach: each resolution
 
55 +
    spawns a dedicated thread that runs the blocking call and posts completion
 
56 +
    back to the scheduler.
 
57 +

 
58 +
    Thread-per-resolution Design
 
59 +
    ----------------------------
 
60 +
    Simple, no thread pool complexity. DNS lookups are infrequent enough that
 
61 +
    thread creation overhead is acceptable. Detached threads self-manage;
 
62 +
    shared_ptr capture keeps impl alive until completion.
 
63 +

 
64 +
    Cancellation
 
65 +
    ------------
 
66 +
    getaddrinfo() cannot be interrupted mid-call. We use an atomic flag to
 
67 +
    indicate cancellation was requested. The worker thread checks this flag
 
68 +
    after getaddrinfo() returns and reports the appropriate error.
 
69 +

 
70 +
    Class Hierarchy
 
71 +
    ---------------
 
72 +
    - posix_resolver_service (execution_context service, one per context)
 
73 +
        - Owns all posix_resolver instances via shared_ptr
 
74 +
        - Stores scheduler* for posting completions
 
75 +
    - posix_resolver (one per resolver object)
 
76 +
        - Contains embedded resolve_op and reverse_resolve_op for reuse
 
77 +
        - Uses shared_from_this to prevent premature destruction
 
78 +
    - resolve_op (forward resolution state)
 
79 +
        - Uses getaddrinfo() to resolve host/service to endpoints
 
80 +
    - reverse_resolve_op (reverse resolution state)
 
81 +
        - Uses getnameinfo() to resolve endpoint to host/service
 
82 +

 
83 +
    Worker Thread Lifetime
 
84 +
    ----------------------
 
85 +
    Each resolve() spawns a detached thread. The thread captures a shared_ptr
 
86 +
    to posix_resolver, ensuring the impl (and its embedded op_) stays
 
87 +
    alive until the thread completes, even if the resolver is destroyed.
 
88 +

 
89 +
    Completion Flow
 
90 +
    ---------------
 
91 +
    Forward resolution:
 
92 +
    1. resolve() sets up op_, spawns worker thread
 
93 +
    2. Worker runs getaddrinfo() (blocking)
 
94 +
    3. Worker stores results in op_.stored_results
 
95 +
    4. Worker calls svc_.post(&op_) to queue completion
 
96 +
    5. Scheduler invokes op_() which resumes the coroutine
 
97 +

 
98 +
    Reverse resolution follows the same pattern using getnameinfo().
 
99 +

 
100 +
    Single-Inflight Constraint
 
101 +
    --------------------------
 
102 +
    Each resolver has ONE embedded op_ for forward and ONE reverse_op_ for
 
103 +
    reverse resolution. Concurrent operations of the same type on the same
 
104 +
    resolver would corrupt state. Users must serialize operations per-resolver.
 
105 +

 
106 +
    Shutdown Synchronization
 
107 +
    ------------------------
 
108 +
    The service tracks active worker threads via thread_started()/thread_finished().
 
109 +
    During shutdown(), the service sets shutting_down_ flag and waits for all
 
110 +
    threads to complete before destroying resources.
 
111 +
*/
 
112 +

 
113 +
namespace boost::corosio::detail {
 
114 +

 
115 +
struct scheduler;
 
116 +

 
117 +
namespace posix_resolver_detail {
 
118 +

 
119 +
// Convert resolve_flags to addrinfo ai_flags
 
120 +
int flags_to_hints(resolve_flags flags);
 
121 +

 
122 +
// Convert reverse_flags to getnameinfo NI_* flags
 
123 +
int flags_to_ni_flags(reverse_flags flags);
 
124 +

 
125 +
// Convert addrinfo results to resolver_results
 
126 +
resolver_results convert_results(
 
127 +
    struct addrinfo* ai, std::string_view host, std::string_view service);
 
128 +

 
129 +
// Convert getaddrinfo error codes to std::error_code
 
130 +
std::error_code make_gai_error(int gai_err);
 
131 +

 
132 +
} // namespace posix_resolver_detail
 
133 +

 
134 +
class posix_resolver_service;
 
135 +

 
136 +
/** Resolver implementation for POSIX backends.
 
137 +

 
138 +
    Each resolver instance contains a single embedded operation object (op_)
 
139 +
    that is reused for each resolve() call. This design avoids per-operation
 
140 +
    heap allocation but imposes a critical constraint:
 
141 +

 
142 +
    @par Single-Inflight Contract
 
143 +

 
144 +
    Only ONE resolve operation may be in progress at a time per resolver
 
145 +
    instance. Calling resolve() while a previous resolve() is still pending
 
146 +
    results in undefined behavior:
 
147 +

 
148 +
    - The new call overwrites op_ fields (host, service, coroutine handle)
 
149 +
    - The worker thread from the first call reads corrupted state
 
150 +
    - The wrong coroutine may be resumed, or resumed multiple times
 
151 +
    - Data races occur on non-atomic op_ members
 
152 +

 
153 +
    @par Safe Usage Patterns
 
154 +

 
155 +
    @code
 
156 +
    // CORRECT: Sequential resolves
 
157 +
    auto [ec1, r1] = co_await resolver.resolve("host1", "80");
 
158 +
    auto [ec2, r2] = co_await resolver.resolve("host2", "80");
 
159 +

 
160 +
    // CORRECT: Parallel resolves with separate resolver instances
 
161 +
    resolver r1(ctx), r2(ctx);
 
162 +
    auto [ec1, res1] = co_await r1.resolve("host1", "80");  // in one coroutine
 
163 +
    auto [ec2, res2] = co_await r2.resolve("host2", "80");  // in another
 
164 +

 
165 +
    // WRONG: Concurrent resolves on same resolver
 
166 +
    // These may run concurrently if launched in parallel - UNDEFINED BEHAVIOR
 
167 +
    auto f1 = resolver.resolve("host1", "80");
 
168 +
    auto f2 = resolver.resolve("host2", "80");  // BAD: overlaps with f1
 
169 +
    @endcode
 
170 +

 
171 +
    @par Thread Safety
 
172 +
    Distinct objects: Safe.
 
173 +
    Shared objects: Unsafe. See single-inflight contract above.
 
174 +
*/
 
175 +
class posix_resolver final
 
176 +
    : public resolver::implementation
 
177 +
    , public std::enable_shared_from_this<posix_resolver>
 
178 +
    , public intrusive_list<posix_resolver>::node
 
179 +
{
 
180 +
    friend class posix_resolver_service;
 
181 +

 
182 +
public:
 
183 +
    // resolve_op - operation state for a single DNS resolution
 
184 +

 
185 +
    struct resolve_op : scheduler_op
 
186 +
    {
 
187 +
        struct canceller
 
188 +
        {
 
189 +
            resolve_op* op;
 
190 +
            void operator()() const noexcept
 
191 +
            {
 
192 +
                op->request_cancel();
 
193 +
            }
 
194 +
        };
 
195 +

 
196 +
        // Coroutine state
 
197 +
        std::coroutine_handle<> h;
 
198 +
        capy::executor_ref ex;
 
199 +
        posix_resolver* impl = nullptr;
 
200 +

 
201 +
        // Output parameters
 
202 +
        std::error_code* ec_out = nullptr;
 
203 +
        resolver_results* out   = nullptr;
 
204 +

 
205 +
        // Input parameters (owned copies for thread safety)
 
206 +
        std::string host;
 
207 +
        std::string service;
 
208 +
        resolve_flags flags = resolve_flags::none;
 
209 +

 
210 +
        // Result storage (populated by worker thread)
 
211 +
        resolver_results stored_results;
 
212 +
        int gai_error = 0;
 
213 +

 
214 +
        // Thread coordination
 
215 +
        std::atomic<bool> cancelled{false};
 
216 +
        std::optional<std::stop_callback<canceller>> stop_cb;
 
217 +

 
218 +
        resolve_op() = default;
 
219 +

 
220 +
        void reset() noexcept;
 
221 +
        void operator()() override;
 
222 +
        void destroy() override;
 
223 +
        void request_cancel() noexcept;
 
224 +
        void start(std::stop_token token);
 
225 +
    };
 
226 +

 
227 +
    // reverse_resolve_op - operation state for reverse DNS resolution
 
228 +

 
229 +
    struct reverse_resolve_op : scheduler_op
 
230 +
    {
 
231 +
        struct canceller
 
232 +
        {
 
233 +
            reverse_resolve_op* op;
 
234 +
            void operator()() const noexcept
 
235 +
            {
 
236 +
                op->request_cancel();
 
237 +
            }
 
238 +
        };
 
239 +

 
240 +
        // Coroutine state
 
241 +
        std::coroutine_handle<> h;
 
242 +
        capy::executor_ref ex;
 
243 +
        posix_resolver* impl = nullptr;
 
244 +

 
245 +
        // Output parameters
 
246 +
        std::error_code* ec_out             = nullptr;
 
247 +
        reverse_resolver_result* result_out = nullptr;
 
248 +

 
249 +
        // Input parameters
 
250 +
        endpoint ep;
 
251 +
        reverse_flags flags = reverse_flags::none;
 
252 +

 
253 +
        // Result storage (populated by worker thread)
 
254 +
        std::string stored_host;
 
255 +
        std::string stored_service;
 
256 +
        int gai_error = 0;
 
257 +

 
258 +
        // Thread coordination
 
259 +
        std::atomic<bool> cancelled{false};
 
260 +
        std::optional<std::stop_callback<canceller>> stop_cb;
 
261 +

 
262 +
        reverse_resolve_op() = default;
 
263 +

 
264 +
        void reset() noexcept;
 
265 +
        void operator()() override;
 
266 +
        void destroy() override;
 
267 +
        void request_cancel() noexcept;
 
268 +
        void start(std::stop_token token);
 
269 +
    };
 
270 +

 
271 +
    explicit posix_resolver(posix_resolver_service& svc) noexcept;
 
272 +

 
273 +
    std::coroutine_handle<> resolve(
 
274 +
        std::coroutine_handle<>,
 
275 +
        capy::executor_ref,
 
276 +
        std::string_view host,
 
277 +
        std::string_view service,
 
278 +
        resolve_flags flags,
 
279 +
        std::stop_token,
 
280 +
        std::error_code*,
 
281 +
        resolver_results*) override;
 
282 +

 
283 +
    std::coroutine_handle<> reverse_resolve(
 
284 +
        std::coroutine_handle<>,
 
285 +
        capy::executor_ref,
 
286 +
        endpoint const& ep,
 
287 +
        reverse_flags flags,
 
288 +
        std::stop_token,
 
289 +
        std::error_code*,
 
290 +
        reverse_resolver_result*) override;
 
291 +

 
292 +
    void cancel() noexcept override;
 
293 +

 
294 +
    resolve_op op_;
 
295 +
    reverse_resolve_op reverse_op_;
 
296 +

 
297 +
private:
 
298 +
    posix_resolver_service& svc_;
 
299 +
};
 
300 +

 
301 +
} // namespace boost::corosio::detail
 
302 +

 
303 +
#endif // BOOST_COROSIO_POSIX
 
304 +

 
305 +
#endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RESOLVER_HPP