GCC Code Coverage Report


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