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