TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Michael Vandeberg
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/capy
9 : //
10 :
11 : #ifndef BOOST_CAPY_TEST_RUN_BLOCKING_HPP
12 : #define BOOST_CAPY_TEST_RUN_BLOCKING_HPP
13 :
14 : #include <boost/capy/detail/config.hpp>
15 : #include <boost/capy/concept/execution_context.hpp>
16 : #include <boost/capy/concept/executor.hpp>
17 : #include <boost/capy/error.hpp>
18 : #include <boost/capy/ex/run_async.hpp>
19 :
20 : #include <coroutine>
21 : #include <exception>
22 : #include <stop_token>
23 : #include <system_error>
24 : #include <type_traits>
25 : #include <utility>
26 :
27 : namespace boost {
28 : namespace capy {
29 : namespace test {
30 :
31 : class blocking_context;
32 :
33 : /** Dispatches work inline for symmetric transfer, or enqueues it into the owning `blocking_context`.
34 :
35 : This executor is used internally by @ref run_blocking to
36 : execute coroutine tasks on the calling thread. Work submitted
37 : via `dispatch()` from within the context's event loop is
38 : returned for symmetric transfer; from elsewhere it is queued. Work
39 : submitted via `post()` is enqueued and processed by the
40 : @ref blocking_context event loop.
41 :
42 : Users do not construct this type directly. It is obtained
43 : from @ref blocking_context::get_executor.
44 :
45 : @par Thread Safety
46 : All member functions are safe to call from any thread.
47 :
48 : @see blocking_context, run_blocking
49 : */
50 : struct BOOST_CAPY_DECL blocking_executor
51 : {
52 : /** Construct from a context pointer.
53 :
54 : @param ctx The owning execution context.
55 : */
56 HIT 1126 : explicit blocking_executor(
57 : blocking_context* ctx) noexcept
58 1126 : : ctx_(ctx)
59 : {
60 1126 : }
61 :
62 : /** Compare two blocking executors for equality.
63 :
64 : Two executors are equal if they share the same context.
65 :
66 : @param other The executor to compare against.
67 :
68 : @return `true` if both executors share the same context.
69 : */
70 : bool
71 : operator==(blocking_executor const& other) const noexcept;
72 :
73 : /** Return the associated execution context.
74 :
75 : @return A reference to the owning `blocking_context`.
76 : */
77 : blocking_context&
78 : context() const noexcept;
79 :
80 : /// Called when work is submitted (no-op).
81 : void on_work_started() const noexcept;
82 :
83 : /// Called when work completes (no-op).
84 : void on_work_finished() const noexcept;
85 :
86 : /** Dispatch work, running it inline when already in the loop.
87 :
88 : Called from within the context's event loop, returns the
89 : handle for the caller to resume by symmetric transfer.
90 : Called from anywhere else, including while the context is
91 : shutting down, queues the continuation as @ref post does.
92 :
93 : @param c The continuation to execute.
94 :
95 : @return `c.h` for symmetric transfer, or
96 : `std::noop_coroutine()` if the continuation was queued.
97 : */
98 : std::coroutine_handle<>
99 : dispatch(continuation& c) const;
100 :
101 : /** Post work for deferred execution.
102 :
103 : Enqueues the coroutine handle into the context's work
104 : queue. The handle is resumed when the blocking event
105 : loop processes it.
106 :
107 : @param c The continuation to enqueue.
108 : */
109 : void
110 : post(continuation& c) const;
111 :
112 : private:
113 : blocking_context* ctx_;
114 : };
115 :
116 : /** Runs a work queue and event loop on the calling thread until the task completes.
117 :
118 : Provides a work queue and event loop that runs on the
119 : calling thread. Coroutines dispatched through the
120 : associated @ref blocking_executor have their `post()`
121 : calls enqueued and processed by @ref run, which blocks
122 : until @ref signal_done is called.
123 :
124 : This context is created internally by @ref run_blocking.
125 : Users do not interact with it directly.
126 :
127 : Coroutines still queued when the context is destroyed are
128 : passed to @ref abandon rather than resumed.
129 :
130 : @par Thread Safety
131 : The event loop runs on the thread that calls `run()`.
132 : `signal_done()`, `signal_abandoned()`, and `enqueue()` are safe
133 : to call from any thread.
134 :
135 : @see blocking_executor, run_blocking
136 : */
137 : class BOOST_CAPY_DECL blocking_context
138 : : public execution_context
139 : {
140 : struct impl;
141 : impl* impl_;
142 :
143 : public:
144 : /// Names `blocking_executor` as the type `get_executor()` returns.
145 : using executor_type = blocking_executor;
146 :
147 : /** Construct a blocking context.
148 :
149 : Allocates the internal work queue and
150 : synchronization state.
151 : */
152 : blocking_context();
153 :
154 : /** Destroy the blocking context. */
155 : ~blocking_context();
156 :
157 : /** Return an executor bound to this context.
158 :
159 : @return A `blocking_executor` that enqueues work
160 : into this context's queue.
161 : */
162 : blocking_executor
163 : get_executor() noexcept;
164 :
165 : /** Signal that the task has completed.
166 :
167 : Wakes the event loop so that @ref run returns.
168 : */
169 : void
170 : signal_done() noexcept;
171 :
172 : /** Signal that the task has completed with an error.
173 :
174 : Stores the exception and wakes the event loop
175 : so that @ref run rethrows it.
176 :
177 : @param ep The exception to propagate.
178 : */
179 : void
180 : signal_done(std::exception_ptr ep) noexcept;
181 :
182 : /** Signal that the task was abandoned without a result.
183 :
184 : Wakes the event loop so that @ref run throws
185 : `std::system_error` with `error::canceled`.
186 : */
187 : void
188 : signal_abandoned() noexcept;
189 :
190 : /** Run the event loop until done.
191 :
192 : Blocks the calling thread, processing posted
193 : coroutine handles until @ref signal_done is called.
194 : After draining remaining work, rethrows any stored
195 : exception.
196 :
197 : @par Exception Safety
198 : Basic guarantee. If the completed task stored an
199 : exception via `signal_done(ep)`, it is rethrown.
200 : */
201 : void
202 : run();
203 :
204 : /** Enqueue a continuation for processing.
205 :
206 : A copy is queued, so the owner it carries reaches @ref abandon
207 : if the context is destroyed first.
208 :
209 : @param c The continuation to enqueue.
210 : */
211 : void
212 : enqueue(continuation const& c);
213 : };
214 :
215 : /** Wrapper that signals completion after invoking the handler.
216 :
217 : Forwards invocations to the contained handler_pair, then
218 : signals the `blocking_context` so that its event loop
219 : unblocks. Exceptions thrown by the handler are captured
220 : and stored for later rethrow.
221 :
222 : @tparam H1 The success handler type.
223 : @tparam H2 The error handler type.
224 :
225 : @par Thread Safety
226 : Safe to invoke from any thread.
227 :
228 : @see run_blocking, blocking_context
229 : */
230 : template<class H1, class H2>
231 : struct blocking_handler_wrapper
232 : {
233 : /// The context signalled once the handler returns.
234 : blocking_context* ctx_;
235 :
236 : /// The success and error handlers to forward to.
237 : detail::handler_pair<H1, H2> handlers_;
238 :
239 : /** Destroy the wrapper.
240 :
241 : A wrapper destroyed without being invoked belongs to an
242 : abandoned chain, which delivers no result. The context is
243 : signalled with `error::canceled`, so its loop neither waits
244 : forever nor reports success.
245 : */
246 5565 : ~blocking_handler_wrapper()
247 : {
248 5565 : if(ctx_)
249 1 : ctx_->signal_abandoned();
250 5565 : }
251 :
252 : /** Construct from a context and handlers.
253 :
254 : @param ctx The context to signal.
255 :
256 : @param handlers The handlers to forward to.
257 : */
258 1113 : blocking_handler_wrapper(
259 : blocking_context* ctx,
260 : detail::handler_pair<H1, H2> handlers)
261 1113 : : ctx_(ctx)
262 1113 : , handlers_(std::move(handlers))
263 : {
264 1113 : }
265 :
266 : /** Construct by moving, leaving `other` unable to signal.
267 :
268 : @param other The wrapper to move from.
269 : */
270 4452 : blocking_handler_wrapper(blocking_handler_wrapper&& other) noexcept
271 4452 : : ctx_(std::exchange(other.ctx_, nullptr))
272 4452 : , handlers_(std::move(other.handlers_))
273 : {
274 4452 : }
275 :
276 : /** Invoke the handler with a non-void result.
277 :
278 : @param v The result value to forward to the handler.
279 : */
280 : template<class T>
281 55 : void operator()(T&& v)
282 : {
283 : try
284 : {
285 55 : handlers_(std::forward<T>(v));
286 : }
287 : catch(...)
288 : {
289 : std::exchange(ctx_, nullptr)->signal_done(
290 : std::current_exception());
291 : return;
292 : }
293 55 : std::exchange(ctx_, nullptr)->signal_done();
294 : }
295 :
296 : /** Invoke the handler for a void result. */
297 718 : void operator()()
298 : {
299 : try
300 : {
301 718 : handlers_();
302 : }
303 : catch(...)
304 : {
305 : std::exchange(ctx_, nullptr)->signal_done(
306 : std::current_exception());
307 : return;
308 : }
309 718 : std::exchange(ctx_, nullptr)->signal_done();
310 : }
311 :
312 : /** Invoke the handler with an exception.
313 :
314 : @param ep The exception to forward to the error handler.
315 : */
316 339 : void operator()(std::exception_ptr ep)
317 : {
318 : try
319 : {
320 675 : handlers_(ep);
321 : }
322 336 : catch(...)
323 : {
324 336 : std::exchange(ctx_, nullptr)->signal_done(
325 : std::current_exception());
326 336 : return;
327 : }
328 3 : std::exchange(ctx_, nullptr)->signal_done();
329 : }
330 : };
331 :
332 : /** Starts a `blocking_context`, runs the task on it, and pumps the event loop until it completes.
333 :
334 : Holds the handlers and optional stop token. When invoked
335 : with a task, creates a @ref blocking_context, starts
336 : the task via `run_async`, and pumps the event loop until
337 : the task completes.
338 :
339 : The rvalue ref-qualifier on `operator()` restricts invocation
340 : to rvalues, so `run_blocking(h)(task)` is the supported spelling.
341 : `operator()` moves `h1_` out of the wrapper, and `h2_` too unless
342 : `H2` is `default_handler`. The stop token is copied, not moved.
343 : The wrapper is single-use regardless. A stored wrapper needs an
344 : explicit `std::move` to invoke:
345 : `auto w = run_blocking(h); std::move(w)(task);`. That explicit
346 : `std::move` surfaces the single-use hazard that a bare `w(task)`
347 : on an lvalue would otherwise hide.
348 :
349 : @tparam H1 The success handler type.
350 : @tparam H2 The error handler type.
351 :
352 : @par Thread Safety
353 : The wrapper itself should only be used from one thread.
354 : The calling thread blocks until the task completes.
355 :
356 : @par Example
357 : @code
358 : int result = 0;
359 : run_blocking([&](int v) { result = v; })(my_task());
360 : @endcode
361 :
362 : @see run_blocking, run_async
363 : */
364 : template<class H1, class H2>
365 : class [[nodiscard]] run_blocking_wrapper
366 : {
367 : std::stop_token st_;
368 : H1 h1_;
369 : H2 h2_;
370 :
371 : public:
372 : /** Construct wrapper with stop token and handlers.
373 :
374 : @param st The stop token for cooperative cancellation.
375 : @param h1 The success handler.
376 : @param h2 The error handler.
377 : */
378 1113 : run_blocking_wrapper(
379 : std::stop_token st,
380 : H1 h1,
381 : H2 h2)
382 1113 : : st_(std::move(st))
383 1113 : , h1_(std::move(h1))
384 1113 : , h2_(std::move(h2))
385 : {
386 1113 : }
387 :
388 : /** Copy construction is disabled; the wrapper is single-use.
389 :
390 : @param other The wrapper that would be copied.
391 : */
392 : run_blocking_wrapper(run_blocking_wrapper const& other) = delete;
393 :
394 : /** Move construction is disabled; the wrapper is single-use.
395 :
396 : @param other The wrapper that would be moved from.
397 : */
398 : run_blocking_wrapper(run_blocking_wrapper&& other) = delete;
399 :
400 : /** Copy assignment is disabled; the wrapper is single-use.
401 :
402 : @param other The wrapper that would be assigned from.
403 :
404 : @return A reference to `*this`.
405 : */
406 : run_blocking_wrapper& operator=(run_blocking_wrapper const& other) = delete;
407 :
408 : /** Move assignment is disabled; the wrapper is single-use.
409 :
410 : @param other The wrapper that would be moved from.
411 :
412 : @return A reference to `*this`.
413 : */
414 : run_blocking_wrapper& operator=(run_blocking_wrapper&& other) = delete;
415 :
416 : /** Start the task and block until completion.
417 :
418 : Creates a blocking_context with a single-threaded
419 : event loop, starts the task via `run_async`, then
420 : pumps the loop until the task completes or throws.
421 :
422 : @throws std::system_error with `error::canceled` if the task
423 : is abandoned before it completes.
424 :
425 : @tparam Task The ContextRunnable type.
426 :
427 : @param t The task to execute.
428 : */
429 : template<ContextRunnable Task>
430 : void
431 1113 : operator()(Task t) &&
432 : {
433 1113 : blocking_context ctx;
434 :
435 2226 : auto make_handlers = [&]() {
436 : if constexpr(
437 : std::is_same_v<H2, detail::default_handler>)
438 : return detail::handler_pair<H1, H2>{
439 1108 : std::move(h1_)};
440 : else
441 : return detail::handler_pair<H1, H2>{
442 5 : std::move(h1_), std::move(h2_)};
443 : };
444 :
445 1113 : run_async(
446 : ctx.get_executor(),
447 1113 : st_,
448 : blocking_handler_wrapper<H1, H2>{
449 : &ctx, make_handlers()}
450 1113 : )(std::move(t));
451 :
452 1113 : ctx.run();
453 1113 : }
454 : };
455 :
456 : /** Block until task completes and discard result.
457 :
458 : Executes a lazy task on a single-threaded event loop
459 : and blocks the calling thread until the task completes
460 : or throws.
461 :
462 : @par Exception Safety
463 : Basic guarantee. If the task throws, the exception is
464 : rethrown to the caller.
465 :
466 : @par Example
467 : @par !example example_2
468 :
469 :
470 : @return A wrapper that accepts a task for blocking execution.
471 :
472 : @see run_async
473 : */
474 : [[nodiscard]] inline auto
475 1047 : run_blocking()
476 : {
477 : return run_blocking_wrapper<
478 : detail::default_handler,
479 : detail::default_handler>(
480 2094 : std::stop_token{},
481 : detail::default_handler{},
482 1047 : detail::default_handler{});
483 : }
484 :
485 : /** Block until task completes and invoke handler with result.
486 :
487 : Executes a lazy task on a single-threaded event loop
488 : and blocks until completion. The handler `h1` is called
489 : with the result on success. If `h1` is also invocable
490 : with `std::exception_ptr`, it handles exceptions too.
491 : Otherwise, exceptions are rethrown.
492 :
493 : @par Exception Safety
494 : Basic guarantee. Exceptions from the task are passed
495 : to `h1` if it accepts `std::exception_ptr`, otherwise
496 : rethrown.
497 :
498 : @par Example
499 : @par !example example_3
500 :
501 :
502 : @param h1 Handler invoked with the result on success,
503 : and optionally with `std::exception_ptr` on failure.
504 :
505 : @return A wrapper that accepts a task for blocking execution.
506 :
507 : @see run_async
508 : */
509 : template<class H1>
510 : [[nodiscard]] auto
511 51 : run_blocking(H1 h1)
512 : {
513 : return run_blocking_wrapper<
514 : H1,
515 : detail::default_handler>(
516 102 : std::stop_token{},
517 51 : std::move(h1),
518 51 : detail::default_handler{});
519 : }
520 :
521 : /** Block until task completes with separate handlers.
522 :
523 : Executes a lazy task on a single-threaded event loop
524 : and blocks until completion. The handler `h1` is called
525 : on success, `h2` on failure.
526 :
527 : @par Exception Safety
528 : Basic guarantee. Exceptions from the task are passed
529 : to `h2`.
530 :
531 : @par Example
532 : @par !example example_1
533 :
534 :
535 : @param h1 Handler invoked with the result on success.
536 : @param h2 Handler invoked with the exception on failure.
537 :
538 : @return A wrapper that accepts a task for blocking execution.
539 :
540 : @see run_async
541 : */
542 : template<class H1, class H2>
543 : [[nodiscard]] auto
544 4 : run_blocking(H1 h1, H2 h2)
545 : {
546 : return run_blocking_wrapper<
547 : H1,
548 : H2>(
549 8 : std::stop_token{},
550 4 : std::move(h1),
551 8 : std::move(h2));
552 : }
553 :
554 : /** Block until task completes with stop token support.
555 :
556 : Executes a lazy task on a single-threaded event loop
557 : with the given stop token and blocks until completion.
558 :
559 : @par Exception Safety
560 : Basic guarantee. If the task throws, the exception is
561 : rethrown to the caller.
562 :
563 : @param st The stop token for cooperative cancellation.
564 :
565 : @return A wrapper that accepts a task for blocking execution.
566 :
567 : @see run_async
568 : */
569 : [[nodiscard]] inline auto
570 7 : run_blocking(std::stop_token st)
571 : {
572 : return run_blocking_wrapper<
573 : detail::default_handler,
574 : detail::default_handler>(
575 7 : std::move(st),
576 : detail::default_handler{},
577 7 : detail::default_handler{});
578 : }
579 :
580 : /** Block until task completes with stop token and handler.
581 :
582 : @param st The stop token for cooperative cancellation.
583 : @param h1 Handler invoked with the result on success.
584 :
585 : @return A wrapper that accepts a task for blocking execution.
586 :
587 : @see run_async
588 : */
589 : template<class H1>
590 : [[nodiscard]] auto
591 3 : run_blocking(std::stop_token st, H1 h1)
592 : {
593 : return run_blocking_wrapper<
594 : H1,
595 : detail::default_handler>(
596 3 : std::move(st),
597 3 : std::move(h1),
598 3 : detail::default_handler{});
599 : }
600 :
601 : /** Block until task completes with stop token and handlers.
602 :
603 : @param st The stop token for cooperative cancellation.
604 : @param h1 Handler invoked with the result on success.
605 : @param h2 Handler invoked with the exception on failure.
606 :
607 : @return A wrapper that accepts a task for blocking execution.
608 :
609 : @see run_async
610 : */
611 : template<class H1, class H2>
612 : [[nodiscard]] auto
613 1 : run_blocking(std::stop_token st, H1 h1, H2 h2)
614 : {
615 : return run_blocking_wrapper<
616 : H1,
617 : H2>(
618 1 : std::move(st),
619 1 : std::move(h1),
620 2 : std::move(h2));
621 : }
622 :
623 : } // namespace test
624 : } // namespace capy
625 : } // namespace boost
626 :
627 : #endif
|