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_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 60 : 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 61 : allocate(T& ctx, std::size_t size, std::size_t align)
223 : {
224 61 : return ctx.allocate(size, align);
225 : }
226 :
227 : template<class T>
228 : static void
229 1 : deallocate(T& ctx, void* p, std::size_t size, std::size_t align)
230 : {
231 1 : ctx.deallocate(p, size, align);
232 1 : }
233 :
234 : template<class T>
235 : static void
236 60 : submit(T& ctx, work* w)
237 : {
238 60 : ctx.submit(w);
239 60 : }
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 53 : allocate(void* obj, std::size_t size, std::size_t align)
428 : {
429 53 : return access::allocate(*static_cast<T*>(obj), size, align);
430 : }
431 :
432 : static void
433 1 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
434 : {
435 1 : access::deallocate(*static_cast<T*>(obj), p, size, align);
436 1 : }
437 :
438 : static void
439 52 : submit(void* obj, work* w)
440 : {
441 52 : access::submit(*static_cast<T*>(obj), w);
442 52 : }
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 0 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
481 : {
482 0 : access::deallocate(
483 0 : static_cast<holder*>(obj)->ex, p, size, align);
484 0 : }
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 30 : executor::
517 : executor(T& ctx) noexcept
518 30 : : ops_(
519 : &ops_for<typename std::decay<T>::type>::table,
520 : detail::null_deleter())
521 30 : , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
522 : {
523 30 : }
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 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 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 59 : 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 59 : factory fac(*this);
652 59 : void* p = fac.allocate(sizeof(callable), alignof(callable));
653 59 : callable* w = ::new(p) callable(std::forward<F>(f));
654 59 : fac.commit(w);
655 59 : }
656 :
657 : //-----------------------------------------------------------------------------
658 :
659 : template<class F, class Handler>
660 : auto
661 4 : 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 4 : handler(result_type(f()));
681 : }
682 1 : catch(...)
683 : {
684 1 : handler(result_type(std::current_exception()));
685 : }
686 4 : }
687 : };
688 :
689 4 : post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
690 4 : }
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 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
|