Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
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/capy
8 : //
9 :
10 : #ifndef BOOST_CAPY_TEST_RUN_BLOCKING_HPP
11 : #define BOOST_CAPY_TEST_RUN_BLOCKING_HPP
12 :
13 : #include <boost/capy/detail/config.hpp>
14 : #include <boost/capy/concept/execution_context.hpp>
15 : #include <boost/capy/concept/executor.hpp>
16 : #include <boost/capy/ex/run_async.hpp>
17 :
18 : #include <coroutine>
19 : #include <exception>
20 : #include <stop_token>
21 : #include <type_traits>
22 : #include <utility>
23 :
24 : namespace boost {
25 : namespace capy {
26 : namespace test {
27 :
28 : class blocking_context;
29 :
30 : /** Single-threaded executor for blocking synchronous tests.
31 :
32 : This executor is used internally by @ref run_blocking to
33 : execute coroutine tasks on the calling thread. Work submitted
34 : via `dispatch()` is returned for symmetric transfer. Work
35 : submitted via `post()` is enqueued and processed by the
36 : @ref blocking_context event loop.
37 :
38 : Users do not construct this type directly. It is obtained
39 : from @ref blocking_context::get_executor.
40 :
41 : @par Thread Safety
42 : All member functions are safe to call from any thread.
43 :
44 : @see blocking_context, run_blocking
45 : */
46 : struct BOOST_CAPY_DECL blocking_executor
47 : {
48 : /// Construct from a context pointer.
49 1876 : explicit blocking_executor(
50 : blocking_context* ctx) noexcept
51 1876 : : ctx_(ctx)
52 : {
53 1876 : }
54 :
55 : /** Compare two blocking executors for equality.
56 :
57 : Two executors are equal if they share the same context.
58 : */
59 : bool
60 : operator==(blocking_executor const& other) const noexcept;
61 :
62 : /** Return the associated execution context.
63 :
64 : @return A reference to the owning `blocking_context`.
65 : */
66 : blocking_context&
67 : context() const noexcept;
68 :
69 : /// Called when work is submitted (no-op).
70 : void on_work_started() const noexcept;
71 :
72 : /// Called when work completes (no-op).
73 : void on_work_finished() const noexcept;
74 :
75 : /** Dispatch work for immediate inline execution.
76 :
77 : Returns the handle for symmetric transfer. The caller
78 : resumes the coroutine via the returned handle.
79 :
80 : @param h The coroutine handle to execute.
81 :
82 : @return `h` for symmetric transfer.
83 : */
84 : std::coroutine_handle<>
85 : dispatch(std::coroutine_handle<> h) const;
86 :
87 : /** Post work for deferred execution.
88 :
89 : Enqueues the coroutine handle into the context's work
90 : queue. The handle is resumed when the blocking event
91 : loop processes it.
92 :
93 : @param h The coroutine handle to enqueue.
94 : */
95 : void
96 : post(std::coroutine_handle<> h) const;
97 :
98 : private:
99 : blocking_context* ctx_;
100 : };
101 :
102 : /** Single-threaded execution context for blocking tests.
103 :
104 : Provides a work queue and event loop that runs on the
105 : calling thread. Coroutines dispatched through the
106 : associated @ref blocking_executor have their `post()`
107 : calls enqueued and processed by @ref run, which blocks
108 : until @ref signal_done is called.
109 :
110 : This context is created internally by @ref run_blocking.
111 : Users do not interact with it directly.
112 :
113 : @par Thread Safety
114 : The event loop runs on the thread that calls `run()`.
115 : `signal_done()` and `enqueue()` are safe to call from
116 : any thread.
117 :
118 : @see blocking_executor, run_blocking
119 : */
120 : class BOOST_CAPY_DECL blocking_context
121 : : public execution_context
122 : {
123 : struct impl;
124 : impl* impl_;
125 :
126 : public:
127 : using executor_type = blocking_executor;
128 :
129 : /** Construct a blocking context.
130 :
131 : Allocates the internal work queue and
132 : synchronization state.
133 : */
134 : blocking_context();
135 :
136 : /** Destroy the blocking context. */
137 : ~blocking_context();
138 :
139 : /** Return an executor bound to this context.
140 :
141 : @return A `blocking_executor` that enqueues work
142 : into this context's queue.
143 : */
144 : blocking_executor
145 : get_executor() noexcept;
146 :
147 : /** Signal that the task has completed.
148 :
149 : Wakes the event loop so that @ref run returns.
150 : */
151 : void
152 : signal_done() noexcept;
153 :
154 : /** Signal that the task has completed with an error.
155 :
156 : Stores the exception and wakes the event loop
157 : so that @ref run rethrows it.
158 :
159 : @param ep The exception to propagate.
160 : */
161 : void
162 : signal_done(std::exception_ptr ep) noexcept;
163 :
164 : /** Run the event loop until done.
165 :
166 : Blocks the calling thread, processing posted
167 : coroutine handles until @ref signal_done is called.
168 : After draining remaining work, rethrows any stored
169 : exception.
170 :
171 : @par Exception Safety
172 : Basic guarantee. If the completed task stored an
173 : exception via `signal_done(ep)`, it is rethrown.
174 : */
175 : void
176 : run();
177 :
178 : /** Enqueue a coroutine handle for processing.
179 :
180 : @param h The coroutine handle to enqueue.
181 : */
182 : void
183 : enqueue(std::coroutine_handle<> h);
184 : };
185 :
186 : /** Wrapper that signals completion after invoking the handler.
187 :
188 : Forwards invocations to the contained handler_pair, then
189 : signals the `blocking_context` so that its event loop
190 : unblocks. Exceptions thrown by the handler are captured
191 : and stored for later rethrow.
192 :
193 : @tparam H1 The success handler type.
194 : @tparam H2 The error handler type.
195 :
196 : @par Thread Safety
197 : Safe to invoke from any thread.
198 :
199 : @see run_blocking, blocking_context
200 : */
201 : template<class H1, class H2>
202 : struct blocking_handler_wrapper
203 : {
204 : blocking_context* ctx_;
205 : detail::handler_pair<H1, H2> handlers_;
206 :
207 : /** Invoke the handler with a non-void result. */
208 : template<class T>
209 21 : void operator()(T&& v)
210 : {
211 : try
212 : {
213 21 : handlers_(std::forward<T>(v));
214 : }
215 : catch(...)
216 : {
217 : ctx_->signal_done(std::current_exception());
218 : return;
219 : }
220 21 : ctx_->signal_done();
221 : }
222 :
223 : /** Invoke the handler for a void result. */
224 1152 : void operator()()
225 : {
226 : try
227 : {
228 1152 : handlers_();
229 : }
230 : catch(...)
231 : {
232 : ctx_->signal_done(std::current_exception());
233 : return;
234 : }
235 1152 : ctx_->signal_done();
236 : }
237 :
238 : /** Invoke the handler with an exception. */
239 695 : void operator()(std::exception_ptr ep)
240 : {
241 : try
242 : {
243 1387 : handlers_(ep);
244 : }
245 692 : catch(...)
246 : {
247 692 : ctx_->signal_done(std::current_exception());
248 692 : return;
249 : }
250 3 : ctx_->signal_done();
251 : }
252 : };
253 :
254 : /** Wrapper returned by run_blocking that accepts a task.
255 :
256 : Holds the handlers and optional stop token. When invoked
257 : with a task, creates a @ref blocking_context, launches
258 : the task via `run_async`, and pumps the event loop until
259 : the task completes.
260 :
261 : The rvalue ref-qualifier on `operator()` ensures the
262 : wrapper can only be used as a temporary.
263 :
264 : @tparam H1 The success handler type.
265 : @tparam H2 The error handler type.
266 :
267 : @par Thread Safety
268 : The wrapper itself should only be used from one thread.
269 : The calling thread blocks until the task completes.
270 :
271 : @par Example
272 : @code
273 : int result = 0;
274 : run_blocking([&](int v) { result = v; })(my_task());
275 : @endcode
276 :
277 : @see run_blocking, run_async
278 : */
279 : template<class H1, class H2>
280 : class [[nodiscard]] run_blocking_wrapper
281 : {
282 : std::stop_token st_;
283 : H1 h1_;
284 : H2 h2_;
285 :
286 : public:
287 : /** Construct wrapper with stop token and handlers.
288 :
289 : @param st The stop token for cooperative cancellation.
290 : @param h1 The success handler.
291 : @param h2 The error handler.
292 : */
293 1868 : run_blocking_wrapper(
294 : std::stop_token st,
295 : H1 h1,
296 : H2 h2)
297 1868 : : st_(std::move(st))
298 1868 : , h1_(std::move(h1))
299 1868 : , h2_(std::move(h2))
300 : {
301 1868 : }
302 :
303 : run_blocking_wrapper(run_blocking_wrapper const&) = delete;
304 : run_blocking_wrapper(run_blocking_wrapper&&) = delete;
305 : run_blocking_wrapper& operator=(run_blocking_wrapper const&) = delete;
306 : run_blocking_wrapper& operator=(run_blocking_wrapper&&) = delete;
307 :
308 : /** Launch the task and block until completion.
309 :
310 : Creates a blocking_context with a single-threaded
311 : event loop, launches the task via `run_async`, then
312 : pumps the loop until the task completes or throws.
313 :
314 : @tparam Task The IoRunnable type.
315 :
316 : @param t The task to execute.
317 : */
318 : template<IoRunnable Task>
319 : void
320 1868 : operator()(Task t) &&
321 : {
322 1868 : blocking_context ctx;
323 :
324 3736 : auto make_handlers = [&]() {
325 : if constexpr(
326 : std::is_same_v<H2, detail::default_handler>)
327 : return detail::handler_pair<H1, H2>{
328 1865 : std::move(h1_)};
329 : else
330 : return detail::handler_pair<H1, H2>{
331 3 : std::move(h1_), std::move(h2_)};
332 : };
333 :
334 : run_async(
335 : ctx.get_executor(),
336 1868 : st_,
337 : blocking_handler_wrapper<H1, H2>{
338 1868 : &ctx, make_handlers()}
339 1868 : )(std::move(t));
340 :
341 1868 : ctx.run();
342 1868 : }
343 : };
344 :
345 : /** Block until task completes and discard result.
346 :
347 : Executes a lazy task on a single-threaded event loop
348 : and blocks the calling thread until the task completes
349 : or throws.
350 :
351 : @par Exception Safety
352 : Basic guarantee. If the task throws, the exception is
353 : rethrown to the caller.
354 :
355 : @par Example
356 : @code
357 : run_blocking()(my_void_task());
358 : @endcode
359 :
360 : @return A wrapper that accepts a task for blocking execution.
361 :
362 : @see run_async
363 : */
364 : [[nodiscard]] inline auto
365 1844 : run_blocking()
366 : {
367 : return run_blocking_wrapper<
368 : detail::default_handler,
369 : detail::default_handler>(
370 3688 : std::stop_token{},
371 : detail::default_handler{},
372 1844 : detail::default_handler{});
373 : }
374 :
375 : /** Block until task completes and invoke handler with result.
376 :
377 : Executes a lazy task on a single-threaded event loop
378 : and blocks until completion. The handler `h1` is called
379 : with the result on success. If `h1` is also invocable
380 : with `std::exception_ptr`, it handles exceptions too.
381 : Otherwise, exceptions are rethrown.
382 :
383 : @par Exception Safety
384 : Basic guarantee. Exceptions from the task are passed
385 : to `h1` if it accepts `std::exception_ptr`, otherwise
386 : rethrown.
387 :
388 : @par Example
389 : @code
390 : int result = 0;
391 : run_blocking([&](int v) { result = v; })(compute());
392 : @endcode
393 :
394 : @param h1 Handler invoked with the result on success,
395 : and optionally with `std::exception_ptr` on failure.
396 :
397 : @return A wrapper that accepts a task for blocking execution.
398 :
399 : @see run_async
400 : */
401 : template<class H1>
402 : [[nodiscard]] auto
403 19 : run_blocking(H1 h1)
404 : {
405 : return run_blocking_wrapper<
406 : H1,
407 : detail::default_handler>(
408 38 : std::stop_token{},
409 19 : std::move(h1),
410 19 : detail::default_handler{});
411 : }
412 :
413 : /** Block until task completes with separate handlers.
414 :
415 : Executes a lazy task on a single-threaded event loop
416 : and blocks until completion. The handler `h1` is called
417 : on success, `h2` on failure.
418 :
419 : @par Exception Safety
420 : Basic guarantee. Exceptions from the task are passed
421 : to `h2`.
422 :
423 : @par Example
424 : @code
425 : int result = 0;
426 : run_blocking(
427 : [&](int v) { result = v; },
428 : [](std::exception_ptr ep) {
429 : std::rethrow_exception(ep);
430 : }
431 : )(compute());
432 : @endcode
433 :
434 : @param h1 Handler invoked with the result on success.
435 : @param h2 Handler invoked with the exception on failure.
436 :
437 : @return A wrapper that accepts a task for blocking execution.
438 :
439 : @see run_async
440 : */
441 : template<class H1, class H2>
442 : [[nodiscard]] auto
443 3 : run_blocking(H1 h1, H2 h2)
444 : {
445 : return run_blocking_wrapper<
446 : H1,
447 : H2>(
448 6 : std::stop_token{},
449 3 : std::move(h1),
450 6 : std::move(h2));
451 : }
452 :
453 : /** Block until task completes with stop token support.
454 :
455 : Executes a lazy task on a single-threaded event loop
456 : with the given stop token and blocks until completion.
457 :
458 : @par Exception Safety
459 : Basic guarantee. If the task throws, the exception is
460 : rethrown to the caller.
461 :
462 : @param st The stop token for cooperative cancellation.
463 :
464 : @return A wrapper that accepts a task for blocking execution.
465 :
466 : @see run_async
467 : */
468 : [[nodiscard]] inline auto
469 : run_blocking(std::stop_token st)
470 : {
471 : return run_blocking_wrapper<
472 : detail::default_handler,
473 : detail::default_handler>(
474 : std::move(st),
475 : detail::default_handler{},
476 : detail::default_handler{});
477 : }
478 :
479 : /** Block until task completes with stop token and handler.
480 :
481 : @param st The stop token for cooperative cancellation.
482 : @param h1 Handler invoked with the result on success.
483 :
484 : @return A wrapper that accepts a task for blocking execution.
485 :
486 : @see run_async
487 : */
488 : template<class H1>
489 : [[nodiscard]] auto
490 2 : run_blocking(std::stop_token st, H1 h1)
491 : {
492 : return run_blocking_wrapper<
493 : H1,
494 : detail::default_handler>(
495 2 : std::move(st),
496 2 : std::move(h1),
497 2 : detail::default_handler{});
498 : }
499 :
500 : /** Block until task completes with stop token and handlers.
501 :
502 : @param st The stop token for cooperative cancellation.
503 : @param h1 Handler invoked with the result on success.
504 : @param h2 Handler invoked with the exception on failure.
505 :
506 : @return A wrapper that accepts a task for blocking execution.
507 :
508 : @see run_async
509 : */
510 : template<class H1, class H2>
511 : [[nodiscard]] auto
512 : run_blocking(std::stop_token st, H1 h1, H2 h2)
513 : {
514 : return run_blocking_wrapper<
515 : H1,
516 : H2>(
517 : std::move(st),
518 : std::move(h1),
519 : std::move(h2));
520 : }
521 :
522 : } // namespace test
523 : } // namespace capy
524 : } // namespace boost
525 :
526 : #endif
|