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