GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/executor.hpp
Date: 2026-01-08 17:57:28
Exec Total Coverage
Lines: 94 98 95.9%
Functions: 77 87 88.5%
Branches: 9 11 81.8%

Line Branch Exec Source
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_EXECUTOR_HPP
11 #define BOOST_CAPY_EXECUTOR_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/detail/call_traits.hpp>
15 #include <boost/capy/async_op.hpp>
16 #include <boost/system/result.hpp>
17 #include <cstddef>
18 #include <exception>
19 #include <memory>
20 #include <new>
21 #include <type_traits>
22 #include <utility>
23
24 namespace boost {
25 namespace capy {
26
27 #if 0
28 class execution_context
29 {
30 public:
31 private:
32 void post(work* w);
33 };
34 #endif
35
36 /** A lightweight handle for submitting work to an execution context.
37
38 This class provides a value-type interface for submitting
39 work to be executed asynchronously. It supports two modes:
40
41 @li **Reference mode**: Non-owning reference to an execution
42 context. The caller must ensure the context outlives all
43 executors that reference it. Created via the constructor.
44
45 @li **Owning mode**: Shared ownership of a value-type executor.
46 The executor is stored internally and its lifetime is
47 managed automatically. Created via the `wrap()` factory.
48
49 @par Thread Safety
50 Distinct objects may be accessed concurrently. Shared objects
51 require external synchronization.
52
53 @par Implementing an Execution Context
54
55 Both execution contexts (for reference mode) and value-type
56 executors (for owning mode) must declare
57 `friend struct executor::access` and provide three private
58 member functions:
59
60 @li `void* allocate(std::size_t size, std::size_t align)` —
61 Allocate storage for a work item. May throw.
62
63 @li `void deallocate(void* p, std::size_t size, std::size_t align)` —
64 Free storage previously returned by allocate. Must not throw.
65
66 @li `void submit(executor::work* w)` —
67 Take ownership of the work item and arrange for execution.
68 The context must eventually call `w->invoke()`, then
69 `w->~work()`, then deallocate the storage.
70
71 All three functions must be safe to call concurrently.
72
73 @par Example (Reference Mode)
74 @code
75 class my_pool
76 {
77 friend struct executor::access;
78
79 std::mutex mutex_;
80 std::queue<executor::work*> queue_;
81
82 public:
83 void run_one()
84 {
85 executor::work* w = nullptr;
86 {
87 std::lock_guard<std::mutex> lock(mutex_);
88 if(!queue_.empty())
89 {
90 w = queue_.front();
91 queue_.pop();
92 }
93 }
94 if(w)
95 {
96 w->invoke();
97 std::size_t size = w->size;
98 std::size_t align = w->align;
99 w->~work();
100 deallocate(w, size, align);
101 }
102 }
103
104 private:
105 void* allocate(std::size_t size, std::size_t)
106 {
107 return std::malloc(size);
108 }
109
110 void deallocate(void* p, std::size_t, std::size_t)
111 {
112 std::free(p);
113 }
114
115 void submit(executor::work* w)
116 {
117 std::lock_guard<std::mutex> lock(mutex_);
118 queue_.push(w);
119 }
120 };
121
122 // Usage: reference mode
123 my_pool pool;
124 executor ex(pool); // pool must outlive ex
125 @endcode
126
127 @par Example (Owning Mode)
128 @code
129 struct my_strand
130 {
131 friend struct executor::access;
132
133 // ... internal state ...
134
135 private:
136 void* allocate(std::size_t size, std::size_t)
137 {
138 return std::malloc(size);
139 }
140
141 void deallocate(void* p, std::size_t, std::size_t)
142 {
143 std::free(p);
144 }
145
146 void submit(executor::work* w)
147 {
148 // ... queue and serialize work ...
149 }
150 };
151
152 // Usage: owning mode
153 executor ex = executor::from(my_strand{}); // executor owns the strand
154 @endcode
155 */
156 class executor
157 {
158 struct ops;
159
160 template<class T>
161 struct ops_for;
162
163 template<class Exec>
164 struct holder;
165
166 std::shared_ptr<const ops> ops_;
167 void* obj_;
168
169 public:
170 /** Abstract base for type-erased work.
171
172 Implementations derive from this to wrap callable
173 objects for submission through the executor.
174
175 @par Lifecycle
176
177 When work is submitted via an executor:
178 @li Storage is allocated via the context's allocate()
179 @li A work-derived object is constructed in place
180 @li Ownership transfers to the context via submit()
181 @li The context calls invoke() to execute the work
182 @li The context destroys and deallocates the work
183
184 @note Work objects must not be copied or moved after
185 construction. They are always destroyed in place.
186
187 @note Execution contexts are responsible for tracking
188 the size and alignment of allocated work objects for
189 deallocation. A common pattern is to prepend metadata
190 to the allocation.
191 */
192 struct BOOST_SYMBOL_VISIBLE work
193 {
194 120 virtual ~work() = default;
195 virtual void invoke() = 0;
196 };
197
198 class factory;
199
200 /** Accessor for execution context private members.
201
202 Execution contexts should declare this as a friend to
203 allow the executor machinery to call their private
204 allocate, deallocate, and submit members:
205
206 @code
207 class my_context
208 {
209 friend struct executor::access;
210 // ...
211 private:
212 void* allocate(std::size_t, std::size_t);
213 void deallocate(void*, std::size_t, std::size_t);
214 void submit(executor::work*);
215 };
216 @endcode
217 */
218 struct access
219 {
220 template<class T>
221 static void*
222 84 allocate(T& ctx, std::size_t size, std::size_t align)
223 {
224 84 return ctx.allocate(size, align);
225 }
226
227 template<class T>
228 static void
229 2 deallocate(T& ctx, void* p, std::size_t size, std::size_t align)
230 {
231 2 ctx.deallocate(p, size, align);
232 2 }
233
234 template<class T>
235 static void
236 82 submit(T& ctx, work* w)
237 {
238 82 ctx.submit(w);
239 82 }
240 };
241
242 /** Construct an executor referencing an execution context.
243
244 Creates an executor in reference mode. The executor holds
245 a non-owning reference to the context.
246
247 The implementation type must provide:
248 - `void* allocate(std::size_t size, std::size_t align)`
249 - `void deallocate(void* p, std::size_t size, std::size_t align)`
250 - `void submit(executor::work* w)`
251
252 @param ctx The execution context to reference.
253 The context must outlive this executor and all copies.
254
255 @see from
256 */
257 template<
258 class T,
259 class = typename std::enable_if<
260 !std::is_same<
261 typename std::decay<T>::type,
262 executor>::value>::type>
263 executor(T& ctx) noexcept;
264
265 /** Constructor
266
267 Default-constructed executors are empty.
268 */
269 17 executor() noexcept
270 17 : ops_()
271 17 , obj_(nullptr)
272 {
273 17 }
274
275 /** Create an executor with shared ownership of a value-type executor.
276
277 Creates an executor in owning mode. The provided executor
278 is moved into shared storage and its lifetime is managed
279 automatically via reference counting.
280
281 The executor type must provide:
282 - `void* allocate(std::size_t size, std::size_t align)`
283 - `void deallocate(void* p, std::size_t size, std::size_t align)`
284 - `void submit(executor::work* w)`
285
286 @param ex The executor to wrap (moved).
287
288 @return An executor that shares ownership of the wrapped executor.
289
290 @par Example
291 @code
292 // Wrap a value-type executor
293 executor ex = executor::wrap(my_strand{});
294
295 // Copies share ownership (reference counted)
296 executor exec2 = ex; // both reference the same strand
297 @endcode
298 */
299 template<class Exec>
300 static executor
301 wrap(Exec ex);
302
303 /** Return true if the executor references an execution context.
304 */
305 explicit
306 22 operator bool() const noexcept
307 {
308 22 return ops_ != nullptr;
309 }
310
311 /** Submit work for execution (fire-and-forget).
312
313 This overload uses the allocation-aware factory
314 mechanism, allowing the implementation to control
315 memory allocation strategy.
316
317 @param f The callable to execute.
318 */
319 template<class F>
320 void
321 post(F&& f) const;
322
323 /** Submit work and invoke a handler on completion.
324
325 The work function is executed asynchronously. When it
326 completes, the handler is invoked with the result or
327 any exception that was thrown.
328
329 The handler must be invocable with the signature:
330 @code
331 void handler( system::result<T, std::exception_ptr> );
332 @endcode
333 where `T` is the return type of `f`.
334
335 @param f The work function to execute.
336
337 @param handler The completion handler invoked with
338 the result or exception.
339 */
340 template<class F, class Handler>
341 auto
342 submit(F&& f, Handler&& handler) const ->
343 typename std::enable_if<! std::is_void<
344 typename detail::call_traits<typename
345 std::decay<F>::type>::return_type>::value>::type;
346
347 /** Submit work and invoke a handler on completion.
348
349 The work function is executed asynchronously. When it
350 completes, the handler is invoked with success or any
351 exception that was thrown.
352
353 The handler must be invocable with the signature:
354 @code
355 void handler( system::result<void, std::exception_ptr> );
356 @endcode
357
358 @param f The work function to execute.
359
360 @param handler The completion handler invoked with
361 the result or exception.
362 */
363 template<class F, class Handler>
364 auto
365 submit(F&& f, Handler&& handler) const ->
366 typename std::enable_if<std::is_void<typename
367 detail::call_traits<typename std::decay<F>::type
368 >::return_type>::value>::type;
369
370 #ifdef BOOST_CAPY_HAS_CORO
371
372 /** Submit work and return an awaitable result.
373
374 The work function is executed asynchronously. The
375 returned async_op can be awaited in a coroutine
376 to obtain the result.
377
378 @param f The work function to execute.
379
380 @return An awaitable that produces the result of the work.
381 */
382 template<class F>
383 auto
384 submit(F&& f) const ->
385 async_op<std::invoke_result_t<std::decay_t<F>>>
386 requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>);
387
388 /** Submit work and return an awaitable result.
389
390 The work function is executed asynchronously. The returned
391 async_op can be awaited in a coroutine to wait
392 for completion.
393
394 @param f The work function to execute.
395
396 @return An awaitable that completes when the work finishes.
397 */
398 template<class F>
399 auto
400 submit(F&& f) const ->
401 async_op<void>
402 requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>;
403
404 #endif
405 };
406
407 //-----------------------------------------------------------------------------
408
409 /** Static vtable for type-erased executor operations.
410 */
411 struct executor::ops
412 {
413 void* (*allocate)(void* obj, std::size_t size, std::size_t align);
414 void (*deallocate)(void* obj, void* p, std::size_t size, std::size_t align);
415 void (*submit)(void* obj, work* w);
416 };
417
418 /** Type-specific operation implementations.
419
420 For each concrete type T, this provides static functions
421 that cast the void* back to T* and forward via access.
422 */
423 template<class T>
424 struct executor::ops_for
425 {
426 static void*
427 68 allocate(void* obj, std::size_t size, std::size_t align)
428 {
429 68 return access::allocate(*static_cast<T*>(obj), size, align);
430 }
431
432 static void
433 2 deallocate(void* obj, void* p, std::size_t size, std::size_t align)
434 {
435 2 access::deallocate(*static_cast<T*>(obj), p, size, align);
436 2 }
437
438 static void
439 66 submit(void* obj, work* w)
440 {
441 66 access::submit(*static_cast<T*>(obj), w);
442 66 }
443
444 static constexpr ops table = {
445 &allocate,
446 &deallocate,
447 &submit
448 };
449 };
450
451 template<class T>
452 constexpr executor::ops executor::ops_for<T>::table;
453
454 //-----------------------------------------------------------------------------
455
456 /** Holder for value-type executors in owning mode.
457
458 Stores the executor by value and provides the vtable
459 implementation that forwards to the held executor.
460 */
461 template<class Exec>
462 struct executor::holder
463 {
464 Exec ex;
465
466 explicit
467 11 holder(Exec e)
468 11 : ex(std::move(e))
469 {
470 11 }
471
472 static void*
473 8 allocate(void* obj, std::size_t size, std::size_t align)
474 {
475 8 return access::allocate(
476 8 static_cast<holder*>(obj)->ex, size, align);
477 }
478
479 static void
480 deallocate(void* obj, void* p, std::size_t size, std::size_t align)
481 {
482 access::deallocate(
483 static_cast<holder*>(obj)->ex, p, size, align);
484 }
485
486 static void
487 8 submit(void* obj, work* w)
488 {
489 8 access::submit(
490 8 static_cast<holder*>(obj)->ex, w);
491 8 }
492
493 static constexpr ops table = {
494 &allocate,
495 &deallocate,
496 &submit
497 };
498 };
499
500 template<class Exec>
501 constexpr executor::ops executor::holder<Exec>::table;
502
503 //-----------------------------------------------------------------------------
504
505 namespace detail {
506
507 // Null deleter for shared_ptr pointing to static storage
508 struct null_deleter
509 {
510 30 void operator()(const void*) const noexcept {}
511 };
512
513 } // detail
514
515 template<class T, class>
516 46 executor::
517 executor(T& ctx) noexcept
518 46 : ops_(
519 &ops_for<typename std::decay<T>::type>::table,
520 detail::null_deleter())
521 46 , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
522 {
523 46 }
524
525 template<class Exec>
526 executor
527 11 executor::
528 wrap(Exec ex0)
529 {
530 typedef typename std::decay<Exec>::type exec_type;
531 typedef holder<exec_type> holder_type;
532
533
1/1
✓ Branch 1 taken 11 times.
11 std::shared_ptr<holder_type> h =
534 11 std::make_shared<holder_type>(std::move(ex0));
535
536 11 executor ex;
537 // Use aliasing constructor: share ownership with h,
538 // but point to the static vtable
539 11 ex.ops_ = std::shared_ptr<const ops>(h, &holder_type::table);
540 11 ex.obj_ = h.get();
541 22 return ex;
542 11 }
543
544 //-----------------------------------------------------------------------------
545
546 /** RAII factory for constructing and submitting work.
547
548 This class manages the multi-phase process of:
549 1. Allocating storage from the executor implementation
550 2. Constructing work in-place via placement-new
551 3. Submitting the work for execution
552
553 If an exception occurs before commit(), the destructor
554 will clean up any allocated resources.
555
556 @par Exception Safety
557 Strong guarantee. If any operation throws, all resources
558 are properly released.
559 */
560 class executor::factory
561 {
562 ops const* ops_;
563 void* obj_;
564 void* storage_;
565 std::size_t size_;
566 std::size_t align_;
567 bool committed_;
568
569 public:
570 /** Construct a factory bound to an executor.
571
572 @param ex The executor to submit work to.
573 */
574 explicit
575 61 factory(executor const& ex) noexcept
576 61 : ops_(ex.ops_.get())
577 61 , obj_(ex.obj_)
578 61 , storage_(nullptr)
579 61 , size_(0)
580 61 , align_(0)
581 61 , committed_(false)
582 {
583 61 }
584
585 /** Destructor. Releases resources if not committed.
586 */
587 61 ~factory()
588 {
589
3/4
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 60 times.
61 if(storage_ && !committed_)
590 1 ops_->deallocate(obj_, storage_, size_, align_);
591 61 }
592
593 factory(factory const&) = delete;
594 factory& operator=(factory const&) = delete;
595
596 /** Allocate storage for work of given size and alignment.
597
598 @param size The size in bytes required.
599 @param align The alignment required.
600 @return Pointer to uninitialized storage.
601 */
602 void*
603 61 allocate(std::size_t size, std::size_t align)
604 {
605 61 storage_ = ops_->allocate(obj_, size, align);
606 61 size_ = size;
607 61 align_ = align;
608 61 return storage_;
609 }
610
611 /** Submit constructed work for execution.
612
613 After calling commit(), the factory releases ownership
614 and the destructor becomes a no-op.
615
616 @param w Pointer to the constructed work object
617 (must reside in the allocated storage).
618 */
619 void
620 60 commit(work* w)
621 {
622 60 committed_ = true;
623 60 ops_->submit(obj_, w);
624 60 }
625 };
626
627 //-----------------------------------------------------------------------------
628
629 template<class F>
630 void
631 118 executor::
632 post(F&& f) const
633 {
634 struct callable : work
635 {
636 typename std::decay<F>::type f_;
637
638 explicit
639 59 callable(F&& f)
640 59 : f_(std::forward<F>(f))
641 {
642 59 }
643
644 void
645 59 invoke() override
646 {
647 59 f_();
648 59 }
649 };
650
651 118 factory fac(*this);
652
1/1
✓ Branch 1 taken 59 times.
118 void* p = fac.allocate(sizeof(callable), alignof(callable));
653 118 callable* w = ::new(p) callable(std::forward<F>(f));
654
1/1
✓ Branch 1 taken 59 times.
118 fac.commit(w);
655 118 }
656
657 //-----------------------------------------------------------------------------
658
659 template<class F, class Handler>
660 auto
661 7 executor::
662 submit(F&& f, Handler&& handler) const ->
663 typename std::enable_if<! std::is_void<typename
664 detail::call_traits<typename std::decay<F>::type
665 >::return_type>::value>::type
666 {
667 using T = typename detail::call_traits<
668 typename std::decay<F>::type>::return_type;
669 using result_type = system::result<T, std::exception_ptr>;
670
671 struct callable
672 {
673 typename std::decay<F>::type f;
674 typename std::decay<Handler>::type handler;
675
676 4 void operator()()
677 {
678 try
679 {
680
1/1
✓ Branch 3 taken 1 times.
4 handler(result_type(f()));
681 }
682 1 catch(...)
683 {
684
0/1
✗ Branch 3 not taken.
1 handler(result_type(std::current_exception()));
685 }
686 4 }
687 };
688
689
1/1
✓ Branch 3 taken 4 times.
7 post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
690 7 }
691
692 template<class F, class Handler>
693 auto
694 2 executor::
695 submit(F&& f, Handler&& handler) const ->
696 typename std::enable_if<std::is_void<typename
697 detail::call_traits<typename std::decay<F>::type
698 >::return_type>::value>::type
699 {
700 using result_type = system::result<void, std::exception_ptr>;
701
702 struct callable
703 {
704 typename std::decay<F>::type f;
705 typename std::decay<Handler>::type handler;
706
707 2 void operator()()
708 {
709 try
710 {
711 2 f();
712 2 handler(result_type());
713 }
714 catch(...)
715 {
716 handler(result_type(std::current_exception()));
717 }
718 2 }
719 };
720
721
1/1
✓ Branch 3 taken 2 times.
2 post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
722 2 }
723
724 #ifdef BOOST_CAPY_HAS_CORO
725
726 template<class F>
727 auto
728 executor::
729 submit(F&& f) const ->
730 async_op<std::invoke_result_t<std::decay_t<F>>>
731 requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>)
732 {
733 using T = std::invoke_result_t<std::decay_t<F>>;
734
735 return make_async_op<T>(
736 [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
737 {
738 ex.post(
739 [f = std::move(f),
740 on_done = std::move(on_done)]() mutable
741 {
742 on_done(f());
743 });
744 });
745 }
746
747 template<class F>
748 auto
749 executor::
750 submit(F&& f) const ->
751 async_op<void>
752 requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>
753 {
754 return make_async_op<void>(
755 [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
756 {
757 ex.post(
758 [f = std::move(f),
759 on_done = std::move(on_done)]() mutable
760 {
761 f();
762 on_done();
763 });
764 });
765 }
766
767 #endif
768
769 } // capy
770 } // boost
771
772 #endif
773