Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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/coro.hpp>
14 : #include <boost/capy/concept/executor.hpp>
15 : #include <boost/capy/ex/execution_context.hpp>
16 : #include <boost/capy/ex/run_async.hpp>
17 : #include <boost/capy/ex/system_context.hpp>
18 :
19 : #include <condition_variable>
20 : #include <exception>
21 : #include <mutex>
22 : #include <stdexcept>
23 : #include <stop_token>
24 : #include <type_traits>
25 : #include <utility>
26 :
27 : namespace boost {
28 : namespace capy {
29 : namespace test {
30 :
31 : /** Synchronous executor that executes inline and disallows posting.
32 :
33 : This executor executes work synchronously on the calling thread
34 : via `dispatch()`. Calling `post()` throws `std::logic_error`
35 : because posting implies deferred execution which is incompatible
36 : with blocking synchronous semantics.
37 :
38 : @par Thread Safety
39 : All member functions are thread-safe.
40 :
41 : @see run_blocking
42 : */
43 : struct inline_executor
44 : {
45 : /// Compare two inline executors for equality.
46 : bool
47 1 : operator==(inline_executor const&) const noexcept
48 : {
49 1 : return true;
50 : }
51 :
52 : /** Return the associated execution context.
53 :
54 : @return A reference to a function-local static `inline_context`.
55 : */
56 : execution_context&
57 1541 : context() const noexcept
58 : {
59 : struct inline_context : public execution_context {};
60 1541 : static inline_context ctx;
61 1541 : return ctx;
62 : }
63 :
64 : /// Called when work is submitted (no-op).
65 0 : void on_work_started() const noexcept {}
66 :
67 : /// Called when work completes (no-op).
68 0 : void on_work_finished() const noexcept {}
69 :
70 : /** Dispatch work for immediate inline execution.
71 :
72 : @param h The coroutine handle to execute.
73 :
74 : @return The same handle for symmetric transfer.
75 : */
76 : coro
77 1972 : dispatch(coro h) const
78 : {
79 1972 : return h;
80 : }
81 :
82 : /** Post work for deferred execution.
83 :
84 : @par Exception Safety
85 : Always throws.
86 :
87 : @throws std::logic_error Always, because posting is not
88 : supported in a blocking context.
89 :
90 : @param h The coroutine handle (unused).
91 : */
92 : [[noreturn]] void
93 1 : post(coro) const
94 : {
95 : throw std::logic_error(
96 1 : "post not supported in blocking context");
97 : }
98 : };
99 :
100 : static_assert(Executor<inline_executor>);
101 :
102 : //----------------------------------------------------------
103 : //
104 : // blocking_state
105 : //
106 : //----------------------------------------------------------
107 :
108 : /** Synchronization state for blocking execution.
109 :
110 : Holds the mutex, condition variable, and completion flag
111 : used to block the caller until the task completes.
112 :
113 : @par Thread Safety
114 : Thread-safe when accessed under the mutex.
115 :
116 : @see run_blocking
117 : @see blocking_handler_wrapper
118 : */
119 : struct blocking_state
120 : {
121 : std::mutex mtx;
122 : std::condition_variable cv;
123 : bool done = false;
124 : std::exception_ptr ep;
125 : };
126 :
127 : //----------------------------------------------------------
128 : //
129 : // blocking_handler_wrapper
130 : //
131 : //----------------------------------------------------------
132 :
133 : /** Wrapper that signals completion after invoking the underlying handler_pair.
134 :
135 : This wrapper forwards invocations to the contained handler_pair,
136 : then signals the `blocking_state` condition variable so
137 : that `run_blocking` can unblock. Any exceptions thrown by the
138 : handler are captured and stored for later rethrow.
139 :
140 : @tparam H1 The success handler type.
141 : @tparam H2 The error handler type.
142 :
143 : @par Thread Safety
144 : Safe to invoke from any thread. Signals the condition
145 : variable after calling the handler.
146 :
147 : @see run_blocking
148 : @see blocking_state
149 : */
150 : template<class H1, class H2>
151 : struct blocking_handler_wrapper
152 : {
153 : blocking_state* state_;
154 : detail::handler_pair<H1, H2> handlers_;
155 :
156 : /** Invoke the handler with non-void result and signal completion. */
157 : template<class T>
158 21 : void operator()(T&& v)
159 : {
160 : try
161 : {
162 21 : handlers_(std::forward<T>(v));
163 : }
164 : catch(...)
165 : {
166 : std::lock_guard<std::mutex> lock(state_->mtx);
167 : state_->ep = std::current_exception();
168 : state_->done = true;
169 : state_->cv.notify_one();
170 : return;
171 : }
172 : {
173 21 : std::lock_guard<std::mutex> lock(state_->mtx);
174 21 : state_->done = true;
175 21 : }
176 21 : state_->cv.notify_one();
177 : }
178 :
179 : /** Invoke the handler for void result and signal completion. */
180 919 : void operator()()
181 : {
182 : try
183 : {
184 919 : handlers_();
185 : }
186 : catch(...)
187 : {
188 : std::lock_guard<std::mutex> lock(state_->mtx);
189 : state_->ep = std::current_exception();
190 : state_->done = true;
191 : state_->cv.notify_one();
192 : return;
193 : }
194 : {
195 919 : std::lock_guard<std::mutex> lock(state_->mtx);
196 919 : state_->done = true;
197 919 : }
198 919 : state_->cv.notify_one();
199 : }
200 :
201 : /** Invoke the handler with exception and signal completion. */
202 602 : void operator()(std::exception_ptr ep)
203 : {
204 : try
205 : {
206 1201 : handlers_(ep);
207 : }
208 1198 : catch(...)
209 : {
210 599 : std::lock_guard<std::mutex> lock(state_->mtx);
211 599 : state_->ep = std::current_exception();
212 599 : state_->done = true;
213 599 : state_->cv.notify_one();
214 599 : return;
215 599 : }
216 : {
217 3 : std::lock_guard<std::mutex> lock(state_->mtx);
218 3 : state_->done = true;
219 3 : }
220 3 : state_->cv.notify_one();
221 : }
222 : };
223 :
224 : //----------------------------------------------------------
225 : //
226 : // run_blocking_wrapper
227 : //
228 : //----------------------------------------------------------
229 :
230 : /** Wrapper returned by run_blocking that accepts a task for execution.
231 :
232 : This wrapper holds the blocking state and handlers. When invoked
233 : with a task, it launches the task via `run_async` and blocks
234 : until the task completes.
235 :
236 : The rvalue ref-qualifier on `operator()` ensures the wrapper
237 : can only be used as a temporary.
238 :
239 : @tparam Ex The executor type satisfying the `Executor` concept.
240 : @tparam H1 The success handler type.
241 : @tparam H2 The error handler type.
242 :
243 : @par Thread Safety
244 : The wrapper itself should only be used from one thread.
245 : The calling thread blocks until the task completes.
246 :
247 : @par Example
248 : @code
249 : // Block until task completes
250 : int result = 0;
251 : run_blocking([&](int v) { result = v; })(my_task());
252 : @endcode
253 :
254 : @see run_blocking
255 : @see run_async
256 : */
257 : template<Executor Ex, class H1, class H2>
258 : class [[nodiscard]] run_blocking_wrapper
259 : {
260 : Ex ex_;
261 : std::stop_token st_;
262 : H1 h1_;
263 : H2 h2_;
264 :
265 : public:
266 : /** Construct wrapper with executor, stop token, and handlers.
267 :
268 : @param ex The executor to execute the task on.
269 : @param st The stop token for cooperative cancellation.
270 : @param h1 The success handler.
271 : @param h2 The error handler.
272 : */
273 1542 : run_blocking_wrapper(
274 : Ex ex,
275 : std::stop_token st,
276 : H1 h1,
277 : H2 h2)
278 1542 : : ex_(std::move(ex))
279 1542 : , st_(std::move(st))
280 1542 : , h1_(std::move(h1))
281 1542 : , h2_(std::move(h2))
282 : {
283 1542 : }
284 :
285 : run_blocking_wrapper(run_blocking_wrapper const&) = delete;
286 : run_blocking_wrapper(run_blocking_wrapper&&) = delete;
287 : run_blocking_wrapper& operator=(run_blocking_wrapper const&) = delete;
288 : run_blocking_wrapper& operator=(run_blocking_wrapper&&) = delete;
289 :
290 : /** Launch the task and block until completion.
291 :
292 : This operator accepts a task, launches it via `run_async`
293 : with wrapped handlers, and blocks until the task completes.
294 :
295 : @tparam Task The IoLaunchableTask type.
296 :
297 : @param t The task to execute.
298 : */
299 : template<IoLaunchableTask Task>
300 : void
301 1542 : operator()(Task t) &&
302 : {
303 1542 : blocking_state state;
304 :
305 3084 : auto make_handlers = [&]() {
306 : if constexpr(std::is_same_v<H2, detail::default_handler>)
307 1539 : return detail::handler_pair<H1, H2>{std::move(h1_)};
308 : else
309 3 : return detail::handler_pair<H1, H2>{std::move(h1_), std::move(h2_)};
310 : };
311 :
312 : run_async(
313 : ex_,
314 1542 : st_,
315 1542 : blocking_handler_wrapper<H1, H2>{&state, make_handlers()}
316 1542 : )(std::move(t));
317 :
318 1542 : std::unique_lock<std::mutex> lock(state.mtx);
319 3084 : state.cv.wait(lock, [&] { return state.done; });
320 1542 : if(state.ep)
321 1198 : std::rethrow_exception(state.ep);
322 2141 : }
323 : };
324 :
325 : //----------------------------------------------------------
326 : //
327 : // run_blocking Overloads
328 : //
329 : //----------------------------------------------------------
330 :
331 : // With inline_executor (default)
332 :
333 : /** Block until task completes and discard result.
334 :
335 : Executes a lazy task using the inline executor and blocks the
336 : calling thread until the task completes or throws.
337 :
338 : @par Thread Safety
339 : The calling thread is blocked. The task executes inline
340 : on the calling thread.
341 :
342 : @par Exception Safety
343 : Basic guarantee. If the task throws, the exception is
344 : rethrown to the caller.
345 :
346 : @par Example
347 : @code
348 : run_blocking()(my_void_task());
349 : @endcode
350 :
351 : @return A wrapper that accepts a task for blocking execution.
352 :
353 : @see run_async
354 : */
355 : [[nodiscard]] inline auto
356 1518 : run_blocking()
357 : {
358 : return run_blocking_wrapper<
359 : inline_executor,
360 : detail::default_handler,
361 : detail::default_handler>(
362 : inline_executor{},
363 3036 : std::stop_token{},
364 : detail::default_handler{},
365 1518 : detail::default_handler{});
366 : }
367 :
368 : /** Block until task completes and invoke handler with result.
369 :
370 : Executes a lazy task using the inline executor and blocks until
371 : completion. The handler `h1` is called with the result on success.
372 : If `h1` is also invocable with `std::exception_ptr`, it handles
373 : exceptions too. Otherwise, exceptions are rethrown.
374 :
375 : @par Thread Safety
376 : The calling thread is blocked. The task and handler execute
377 : inline on the calling thread.
378 :
379 : @par Exception Safety
380 : Basic guarantee. Exceptions from the task are passed to `h1`
381 : if it accepts `std::exception_ptr`, otherwise rethrown.
382 :
383 : @par Example
384 : @code
385 : int result = 0;
386 : run_blocking([&](int v) { result = v; })(compute_value());
387 : @endcode
388 :
389 : @param h1 Handler invoked with the result on success, and
390 : optionally with `std::exception_ptr` on failure.
391 :
392 : @return A wrapper that accepts a task for blocking execution.
393 :
394 : @see run_async
395 : */
396 : template<class H1>
397 : [[nodiscard]] auto
398 18 : run_blocking(H1 h1)
399 : {
400 : return run_blocking_wrapper<
401 : inline_executor,
402 : H1,
403 : detail::default_handler>(
404 : inline_executor{},
405 36 : std::stop_token{},
406 18 : std::move(h1),
407 18 : detail::default_handler{});
408 : }
409 :
410 : /** Block until task completes with separate handlers.
411 :
412 : Executes a lazy task using the inline executor and blocks until
413 : completion. The handler `h1` is called on success, `h2` on failure.
414 :
415 : @par Thread Safety
416 : The calling thread is blocked. The task and handlers execute
417 : inline on the calling thread.
418 :
419 : @par Exception Safety
420 : Basic guarantee. Exceptions from the task are passed to `h2`.
421 :
422 : @par Example
423 : @code
424 : int result = 0;
425 : run_blocking(
426 : [&](int v) { result = v; },
427 : [](std::exception_ptr ep) {
428 : std::rethrow_exception(ep);
429 : }
430 : )(compute_value());
431 : @endcode
432 :
433 : @param h1 Handler invoked with the result on success.
434 : @param h2 Handler invoked with the exception on failure.
435 :
436 : @return A wrapper that accepts a task for blocking execution.
437 :
438 : @see run_async
439 : */
440 : template<class H1, class H2>
441 : [[nodiscard]] auto
442 3 : run_blocking(H1 h1, H2 h2)
443 : {
444 : return run_blocking_wrapper<
445 : inline_executor,
446 : H1,
447 : H2>(
448 : inline_executor{},
449 6 : std::stop_token{},
450 3 : std::move(h1),
451 6 : std::move(h2));
452 : }
453 :
454 : // With explicit executor
455 :
456 : /** Block until task completes on the given executor.
457 :
458 : Executes a lazy task on the specified executor and blocks the
459 : calling thread until the task completes.
460 :
461 : @par Thread Safety
462 : The calling thread is blocked. The task may execute on
463 : a different thread depending on the executor.
464 :
465 : @par Exception Safety
466 : Basic guarantee. If the task throws, the exception is
467 : rethrown to the caller.
468 :
469 : @par Example
470 : @code
471 : run_blocking(my_executor)(my_void_task());
472 : @endcode
473 :
474 : @param ex The executor to execute the task on.
475 :
476 : @return A wrapper that accepts a task for blocking execution.
477 :
478 : @see run_async
479 : */
480 : template<Executor Ex>
481 : [[nodiscard]] auto
482 : run_blocking(Ex ex)
483 : {
484 : return run_blocking_wrapper<
485 : Ex,
486 : detail::default_handler,
487 : detail::default_handler>(
488 : std::move(ex),
489 : std::stop_token{},
490 : detail::default_handler{},
491 : detail::default_handler{});
492 : }
493 :
494 : /** Block until task completes on executor with handler.
495 :
496 : Executes a lazy task on the specified executor and blocks until
497 : completion. The handler `h1` is called with the result.
498 :
499 : @par Thread Safety
500 : The calling thread is blocked. The task and handler may
501 : execute on a different thread depending on the executor.
502 :
503 : @par Exception Safety
504 : Basic guarantee. Exceptions from the task are passed to `h1`
505 : if it accepts `std::exception_ptr`, otherwise rethrown.
506 :
507 : @param ex The executor to execute the task on.
508 : @param h1 Handler invoked with the result on success.
509 :
510 : @return A wrapper that accepts a task for blocking execution.
511 :
512 : @see run_async
513 : */
514 : template<Executor Ex, class H1>
515 : [[nodiscard]] auto
516 1 : run_blocking(Ex ex, H1 h1)
517 : {
518 : return run_blocking_wrapper<
519 : Ex,
520 : H1,
521 : detail::default_handler>(
522 1 : std::move(ex),
523 2 : std::stop_token{},
524 1 : std::move(h1),
525 1 : detail::default_handler{});
526 : }
527 :
528 : /** Block until task completes on executor with separate handlers.
529 :
530 : Executes a lazy task on the specified executor and blocks until
531 : completion. The handler `h1` is called on success, `h2` on failure.
532 :
533 : @par Thread Safety
534 : The calling thread is blocked. The task and handlers may
535 : execute on a different thread depending on the executor.
536 :
537 : @par Exception Safety
538 : Basic guarantee. Exceptions from the task are passed to `h2`.
539 :
540 : @param ex The executor to execute the task on.
541 : @param h1 Handler invoked with the result on success.
542 : @param h2 Handler invoked with the exception on failure.
543 :
544 : @return A wrapper that accepts a task for blocking execution.
545 :
546 : @see run_async
547 : */
548 : template<Executor Ex, class H1, class H2>
549 : [[nodiscard]] auto
550 : run_blocking(Ex ex, H1 h1, H2 h2)
551 : {
552 : return run_blocking_wrapper<
553 : Ex,
554 : H1,
555 : H2>(
556 : std::move(ex),
557 : std::stop_token{},
558 : std::move(h1),
559 : std::move(h2));
560 : }
561 :
562 : // With stop_token
563 :
564 : /** Block until task completes with stop token support.
565 :
566 : Executes a lazy task using the inline executor with the given
567 : stop token and blocks until completion.
568 :
569 : @par Thread Safety
570 : The calling thread is blocked. The task executes inline
571 : on the calling thread.
572 :
573 : @par Exception Safety
574 : Basic guarantee. If the task throws, the exception is
575 : rethrown to the caller.
576 :
577 : @param st The stop token for cooperative cancellation.
578 :
579 : @return A wrapper that accepts a task for blocking execution.
580 :
581 : @see run_async
582 : */
583 : [[nodiscard]] inline auto
584 : run_blocking(std::stop_token st)
585 : {
586 : return run_blocking_wrapper<
587 : inline_executor,
588 : detail::default_handler,
589 : detail::default_handler>(
590 : inline_executor{},
591 : std::move(st),
592 : detail::default_handler{},
593 : detail::default_handler{});
594 : }
595 :
596 : /** Block until task completes with stop token and handler.
597 :
598 : @param st The stop token for cooperative cancellation.
599 : @param h1 Handler invoked with the result on success.
600 :
601 : @return A wrapper that accepts a task for blocking execution.
602 :
603 : @see run_async
604 : */
605 : template<class H1>
606 : [[nodiscard]] auto
607 2 : run_blocking(std::stop_token st, H1 h1)
608 : {
609 : return run_blocking_wrapper<
610 : inline_executor,
611 : H1,
612 : detail::default_handler>(
613 : inline_executor{},
614 2 : std::move(st),
615 2 : std::move(h1),
616 2 : detail::default_handler{});
617 : }
618 :
619 : /** Block until task completes with stop token and separate handlers.
620 :
621 : @param st The stop token for cooperative cancellation.
622 : @param h1 Handler invoked with the result on success.
623 : @param h2 Handler invoked with the exception on failure.
624 :
625 : @return A wrapper that accepts a task for blocking execution.
626 :
627 : @see run_async
628 : */
629 : template<class H1, class H2>
630 : [[nodiscard]] auto
631 : run_blocking(std::stop_token st, H1 h1, H2 h2)
632 : {
633 : return run_blocking_wrapper<
634 : inline_executor,
635 : H1,
636 : H2>(
637 : inline_executor{},
638 : std::move(st),
639 : std::move(h1),
640 : std::move(h2));
641 : }
642 :
643 : // Executor + stop_token
644 :
645 : /** Block until task completes on executor with stop token.
646 :
647 : @param ex The executor to execute the task on.
648 : @param st The stop token for cooperative cancellation.
649 :
650 : @return A wrapper that accepts a task for blocking execution.
651 :
652 : @see run_async
653 : */
654 : template<Executor Ex>
655 : [[nodiscard]] auto
656 : run_blocking(Ex ex, std::stop_token st)
657 : {
658 : return run_blocking_wrapper<
659 : Ex,
660 : detail::default_handler,
661 : detail::default_handler>(
662 : std::move(ex),
663 : std::move(st),
664 : detail::default_handler{},
665 : detail::default_handler{});
666 : }
667 :
668 : /** Block until task completes on executor with stop token and handler.
669 :
670 : @param ex The executor to execute the task on.
671 : @param st The stop token for cooperative cancellation.
672 : @param h1 Handler invoked with the result on success.
673 :
674 : @return A wrapper that accepts a task for blocking execution.
675 :
676 : @see run_async
677 : */
678 : template<Executor Ex, class H1>
679 : [[nodiscard]] auto
680 : run_blocking(Ex ex, std::stop_token st, H1 h1)
681 : {
682 : return run_blocking_wrapper<
683 : Ex,
684 : H1,
685 : detail::default_handler>(
686 : std::move(ex),
687 : std::move(st),
688 : std::move(h1),
689 : detail::default_handler{});
690 : }
691 :
692 : /** Block until task completes on executor with stop token and handlers.
693 :
694 : @param ex The executor to execute the task on.
695 : @param st The stop token for cooperative cancellation.
696 : @param h1 Handler invoked with the result on success.
697 : @param h2 Handler invoked with the exception on failure.
698 :
699 : @return A wrapper that accepts a task for blocking execution.
700 :
701 : @see run_async
702 : */
703 : template<Executor Ex, class H1, class H2>
704 : [[nodiscard]] auto
705 : run_blocking(Ex ex, std::stop_token st, H1 h1, H2 h2)
706 : {
707 : return run_blocking_wrapper<
708 : Ex,
709 : H1,
710 : H2>(
711 : std::move(ex),
712 : std::move(st),
713 : std::move(h1),
714 : std::move(h2));
715 : }
716 :
717 : } // namespace test
718 : } // namespace capy
719 : } // namespace boost
720 :
721 : #endif
|