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 : /** 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) const;
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) const ->
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) const ->
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 :
363 : /** Submit work and return an awaitable result.
364 :
365 : The work function is executed asynchronously. The
366 : returned async_op can be awaited in a coroutine
367 : to obtain the result.
368 :
369 : @param f The work function to execute.
370 :
371 : @return An awaitable that produces the result of the work.
372 : */
373 : template<class F>
374 : auto
375 : submit(F&& f) const ->
376 : async_op<std::invoke_result_t<std::decay_t<F>>>
377 : requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>);
378 :
379 : /** Submit work and return an awaitable result.
380 :
381 : The work function is executed asynchronously. The returned
382 : async_op can be awaited in a coroutine to wait
383 : for completion.
384 :
385 : @param f The work function to execute.
386 :
387 : @return An awaitable that completes when the work finishes.
388 : */
389 : template<class F>
390 : auto
391 : submit(F&& f) const ->
392 : async_op<void>
393 : requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>;
394 :
395 : #endif
396 : };
397 :
398 : //-----------------------------------------------------------------------------
399 :
400 : /** Static vtable for type-erased executor operations.
401 : */
402 : struct executor::ops
403 : {
404 : void* (*allocate)(void* obj, std::size_t size, std::size_t align);
405 : void (*deallocate)(void* obj, void* p, std::size_t size, std::size_t align);
406 : void (*submit)(void* obj, work* w);
407 : };
408 :
409 : /** Type-specific operation implementations.
410 :
411 : For each concrete type T, this provides static functions
412 : that cast the void* back to T* and forward via access.
413 : */
414 : template<class T>
415 : struct executor::ops_for
416 : {
417 : static void*
418 53 : allocate(void* obj, std::size_t size, std::size_t align)
419 : {
420 53 : return access::allocate(*static_cast<T*>(obj), size, align);
421 : }
422 :
423 : static void
424 1 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
425 : {
426 1 : access::deallocate(*static_cast<T*>(obj), p, size, align);
427 1 : }
428 :
429 : static void
430 52 : submit(void* obj, work* w)
431 : {
432 52 : access::submit(*static_cast<T*>(obj), w);
433 52 : }
434 :
435 : static constexpr ops table = {
436 : &allocate,
437 : &deallocate,
438 : &submit
439 : };
440 : };
441 :
442 : template<class T>
443 : constexpr executor::ops executor::ops_for<T>::table;
444 :
445 : //-----------------------------------------------------------------------------
446 :
447 : /** Holder for value-type executors in owning mode.
448 :
449 : Stores the executor by value and provides the vtable
450 : implementation that forwards to the held executor.
451 : */
452 : template<class Exec>
453 : struct executor::holder
454 : {
455 : Exec ex;
456 :
457 : explicit
458 11 : holder(Exec e)
459 11 : : ex(std::move(e))
460 : {
461 11 : }
462 :
463 : static void*
464 8 : allocate(void* obj, std::size_t size, std::size_t align)
465 : {
466 8 : return access::allocate(
467 8 : static_cast<holder*>(obj)->ex, size, align);
468 : }
469 :
470 : static void
471 0 : deallocate(void* obj, void* p, std::size_t size, std::size_t align)
472 : {
473 0 : access::deallocate(
474 0 : static_cast<holder*>(obj)->ex, p, size, align);
475 0 : }
476 :
477 : static void
478 8 : submit(void* obj, work* w)
479 : {
480 8 : access::submit(
481 8 : static_cast<holder*>(obj)->ex, w);
482 8 : }
483 :
484 : static constexpr ops table = {
485 : &allocate,
486 : &deallocate,
487 : &submit
488 : };
489 : };
490 :
491 : template<class Exec>
492 : constexpr executor::ops executor::holder<Exec>::table;
493 :
494 : //-----------------------------------------------------------------------------
495 :
496 : namespace detail {
497 :
498 : // Null deleter for shared_ptr pointing to static storage
499 : struct null_deleter
500 : {
501 30 : void operator()(const void*) const noexcept {}
502 : };
503 :
504 : } // detail
505 :
506 : template<class T, class>
507 30 : executor::
508 : executor(T& ctx) noexcept
509 30 : : ops_(
510 : &ops_for<typename std::decay<T>::type>::table,
511 : detail::null_deleter())
512 30 : , obj_(const_cast<void*>(static_cast<void const*>(std::addressof(ctx))))
513 : {
514 30 : }
515 :
516 : template<class Exec>
517 : executor
518 11 : executor::
519 : wrap(Exec ex0)
520 : {
521 : typedef typename std::decay<Exec>::type exec_type;
522 : typedef holder<exec_type> holder_type;
523 :
524 11 : std::shared_ptr<holder_type> h =
525 11 : std::make_shared<holder_type>(std::move(ex0));
526 :
527 11 : executor ex;
528 : // Use aliasing constructor: share ownership with h,
529 : // but point to the static vtable
530 11 : ex.ops_ = std::shared_ptr<const ops>(h, &holder_type::table);
531 11 : ex.obj_ = h.get();
532 22 : return ex;
533 11 : }
534 :
535 : //-----------------------------------------------------------------------------
536 :
537 : /** RAII factory for constructing and submitting work.
538 :
539 : This class manages the multi-phase process of:
540 : 1. Allocating storage from the executor implementation
541 : 2. Constructing work in-place via placement-new
542 : 3. Submitting the work for execution
543 :
544 : If an exception occurs before commit(), the destructor
545 : will clean up any allocated resources.
546 :
547 : @par Exception Safety
548 : Strong guarantee. If any operation throws, all resources
549 : are properly released.
550 : */
551 : class executor::factory
552 : {
553 : ops const* ops_;
554 : void* obj_;
555 : void* storage_;
556 : std::size_t size_;
557 : std::size_t align_;
558 : bool committed_;
559 :
560 : public:
561 : /** Construct a factory bound to an executor.
562 :
563 : @param ex The executor to submit work to.
564 : */
565 : explicit
566 61 : factory(executor const& ex) noexcept
567 61 : : ops_(ex.ops_.get())
568 61 : , obj_(ex.obj_)
569 61 : , storage_(nullptr)
570 61 : , size_(0)
571 61 : , align_(0)
572 61 : , committed_(false)
573 : {
574 61 : }
575 :
576 : /** Destructor. Releases resources if not committed.
577 : */
578 61 : ~factory()
579 : {
580 61 : if(storage_ && !committed_)
581 1 : ops_->deallocate(obj_, storage_, size_, align_);
582 61 : }
583 :
584 : factory(factory const&) = delete;
585 : factory& operator=(factory const&) = delete;
586 :
587 : /** Allocate storage for work of given size and alignment.
588 :
589 : @param size The size in bytes required.
590 : @param align The alignment required.
591 : @return Pointer to uninitialized storage.
592 : */
593 : void*
594 61 : allocate(std::size_t size, std::size_t align)
595 : {
596 61 : storage_ = ops_->allocate(obj_, size, align);
597 61 : size_ = size;
598 61 : align_ = align;
599 61 : return storage_;
600 : }
601 :
602 : /** Submit constructed work for execution.
603 :
604 : After calling commit(), the factory releases ownership
605 : and the destructor becomes a no-op.
606 :
607 : @param w Pointer to the constructed work object
608 : (must reside in the allocated storage).
609 : */
610 : void
611 60 : commit(work* w)
612 : {
613 60 : committed_ = true;
614 60 : ops_->submit(obj_, w);
615 60 : }
616 : };
617 :
618 : //-----------------------------------------------------------------------------
619 :
620 : template<class F>
621 : void
622 59 : executor::
623 : post(F&& f) const
624 : {
625 : struct callable : work
626 : {
627 : typename std::decay<F>::type f_;
628 :
629 : explicit
630 59 : callable(F&& f)
631 59 : : f_(std::forward<F>(f))
632 : {
633 59 : }
634 :
635 : void
636 59 : invoke() override
637 : {
638 59 : f_();
639 59 : }
640 : };
641 :
642 59 : factory fac(*this);
643 59 : void* p = fac.allocate(sizeof(callable), alignof(callable));
644 59 : callable* w = ::new(p) callable(std::forward<F>(f));
645 59 : fac.commit(w);
646 59 : }
647 :
648 : //-----------------------------------------------------------------------------
649 :
650 : template<class F, class Handler>
651 : auto
652 4 : executor::
653 : submit(F&& f, Handler&& handler) const ->
654 : typename std::enable_if<! std::is_void<typename
655 : detail::call_traits<typename std::decay<F>::type
656 : >::return_type>::value>::type
657 : {
658 : using T = typename detail::call_traits<
659 : typename std::decay<F>::type>::return_type;
660 : using result_type = system::result<T, std::exception_ptr>;
661 :
662 : struct callable
663 : {
664 : typename std::decay<F>::type f;
665 : typename std::decay<Handler>::type handler;
666 :
667 4 : void operator()()
668 : {
669 : try
670 : {
671 4 : handler(result_type(f()));
672 : }
673 1 : catch(...)
674 : {
675 1 : handler(result_type(std::current_exception()));
676 : }
677 4 : }
678 : };
679 :
680 4 : post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
681 4 : }
682 :
683 : template<class F, class Handler>
684 : auto
685 2 : executor::
686 : submit(F&& f, Handler&& handler) const ->
687 : typename std::enable_if<std::is_void<typename
688 : detail::call_traits<typename std::decay<F>::type
689 : >::return_type>::value>::type
690 : {
691 : using result_type = system::result<void, std::exception_ptr>;
692 :
693 : struct callable
694 : {
695 : typename std::decay<F>::type f;
696 : typename std::decay<Handler>::type handler;
697 :
698 2 : void operator()()
699 : {
700 : try
701 : {
702 2 : f();
703 2 : handler(result_type());
704 : }
705 : catch(...)
706 : {
707 : handler(result_type(std::current_exception()));
708 : }
709 2 : }
710 : };
711 :
712 2 : post(callable{std::forward<F>(f), std::forward<Handler>(handler)});
713 2 : }
714 :
715 : #ifdef BOOST_CAPY_HAS_CORO
716 :
717 : template<class F>
718 : auto
719 : executor::
720 : submit(F&& f) const ->
721 : async_op<std::invoke_result_t<std::decay_t<F>>>
722 : requires (!std::is_void_v<std::invoke_result_t<std::decay_t<F>>>)
723 : {
724 : using T = std::invoke_result_t<std::decay_t<F>>;
725 :
726 : return make_async_op<T>(
727 : [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
728 : {
729 : ex.post(
730 : [f = std::move(f),
731 : on_done = std::move(on_done)]() mutable
732 : {
733 : on_done(f());
734 : });
735 : });
736 : }
737 :
738 : template<class F>
739 : auto
740 : executor::
741 : submit(F&& f) const ->
742 : async_op<void>
743 : requires std::is_void_v<std::invoke_result_t<std::decay_t<F>>>
744 : {
745 : return make_async_op<void>(
746 : [ex = *this, f = std::forward<F>(f)](auto on_done) mutable
747 : {
748 : ex.post(
749 : [f = std::move(f),
750 : on_done = std::move(on_done)]() mutable
751 : {
752 : f();
753 : on_done();
754 : });
755 : });
756 : }
757 :
758 : #endif
759 :
760 : } // capy
761 : } // boost
762 :
763 : #endif
|