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_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 60 : 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 61 : allocate(T& ctx, std::size_t size, std::size_t align)
214 : {
215 61 : return ctx.allocate(size, align);
216 : }
217 :
218 : template<class T>
219 : static void
220 1 : deallocate(T& ctx, void* p, std::size_t size, std::size_t align)
221 : {
222 1 : ctx.deallocate(p, size, align);
223 1 : }
224 :
225 : template<class T>
226 : static void
227 60 : submit(T& ctx, work* w)
228 : {
229 60 : ctx.submit(w);
230 60 : }
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 53 : allocate(void* obj, std::size_t size, std::size_t align)
417 : {
418 53 : return access::allocate(*static_cast<T*>(obj), size, align);
419 : }
420 :
421 : static void
422 1 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
423 : {
424 1 : access::deallocate(*static_cast<T*>(obj), p, size, align);
425 1 : }
426 :
427 : static void
428 52 : submit(void* obj, work* w)
429 : {
430 52 : access::submit(*static_cast<T*>(obj), w);
431 52 : }
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 0 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
470 : {
471 0 : access::deallocate(
472 0 : static_cast<holder*>(obj)->ex, p, size, align);
473 0 : }
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 30 : executor::
506 : executor(T& ctx) noexcept
507 30 : : ops_(
508 : &ops_for<typename std::decay<T>::type>::table,
509 : detail::null_deleter())
510 30 : , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
511 : {
512 30 : }
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 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 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 59 : 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 59 : factory fac(*this);
641 59 : void* p = fac.allocate(sizeof(callable), alignof(callable));
642 59 : callable* w = ::new(p) callable(std::forward<F>(f));
643 59 : fac.commit(w);
644 59 : }
645 :
646 : //-----------------------------------------------------------------------------
647 :
648 : template<class F, class Handler>
649 : auto
650 4 : 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 4 : handler(result_type(f()));
670 : }
671 1 : catch(...)
672 : {
673 1 : handler(result_type(std::current_exception()));
674 : }
675 4 : }
676 : };
677 :
678 4 : post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
679 4 : }
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 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
|