libs/capy/include/boost/capy/ex/execution_context.hpp

100.0% Lines (40/40) 100.0% Functions (18/18) 75.0% Branches (6/8)
libs/capy/include/boost/capy/ex/execution_context.hpp
Line Branch Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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_EXECUTION_CONTEXT_HPP
11 #define BOOST_CAPY_EXECUTION_CONTEXT_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/detail/frame_memory_resource.hpp>
15 #include <boost/capy/detail/type_id.hpp>
16 #include <boost/capy/concept/executor.hpp>
17 #include <concepts>
18 #include <memory>
19 #include <memory_resource>
20 #include <mutex>
21 #include <tuple>
22 #include <type_traits>
23 #include <utility>
24
25 namespace boost {
26 namespace capy {
27
28 /** Base class for I/O object containers providing service management.
29
30 An execution context represents a place where function objects are
31 executed. It provides a service registry where polymorphic services
32 can be stored and retrieved by type. Each service type may be stored
33 at most once. Services may specify a nested `key_type` to enable
34 lookup by a base class type.
35
36 Derived classes such as `io_context` extend this to provide
37 execution facilities like event loops and thread pools. Derived
38 class destructors must call `shutdown()` and `destroy()` to ensure
39 proper service cleanup before member destruction.
40
41 @par Service Lifecycle
42 Services are created on first use via `use_service()` or explicitly
43 via `make_service()`. During destruction, `shutdown()` is called on
44 each service in reverse order of creation, then `destroy()` deletes
45 them. Both functions are idempotent.
46
47 @par Thread Safety
48 Service registration and lookup functions are thread-safe.
49 The `shutdown()` and `destroy()` functions are not thread-safe
50 and must only be called during destruction.
51
52 @par Example
53 @code
54 struct file_service : execution_context::service
55 {
56 protected:
57 void shutdown() override {}
58 };
59
60 struct posix_file_service : file_service
61 {
62 using key_type = file_service;
63
64 explicit posix_file_service(execution_context&) {}
65 };
66
67 class io_context : public execution_context
68 {
69 public:
70 ~io_context()
71 {
72 shutdown();
73 destroy();
74 }
75 };
76
77 io_context ctx;
78 ctx.make_service<posix_file_service>();
79 ctx.find_service<file_service>(); // returns posix_file_service*
80 ctx.find_service<posix_file_service>(); // also works
81 @endcode
82
83 @see service, is_execution_context
84 */
85 class BOOST_CAPY_DECL
86 execution_context
87 {
88 detail::type_info const* ti_ = nullptr;
89
90 template<class T, class = void>
91 struct get_key : std::false_type
92 {};
93
94 template<class T>
95 struct get_key<T, std::void_t<typename T::key_type>> : std::true_type
96 {
97 using type = typename T::key_type;
98 };
99 protected:
100 template< typename Derived >
101 explicit execution_context( Derived* ) noexcept;
102
103 public:
104 //------------------------------------------------
105
106 /** Abstract base class for services owned by an execution context.
107
108 Services provide extensible functionality to an execution context.
109 Each service type can be registered at most once. Services are
110 created via `use_service()` or `make_service()` and are owned by
111 the execution context for their lifetime.
112
113 Derived classes must implement the pure virtual `shutdown()` member
114 function, which is called when the owning execution context is
115 being destroyed. The `shutdown()` function should release resources
116 and cancel outstanding operations without blocking.
117
118 @par Deriving from service
119 @li Implement `shutdown()` to perform cleanup.
120 @li Accept `execution_context&` as the first constructor parameter.
121 @li Optionally define `key_type` to enable base-class lookup.
122
123 @par Example
124 @code
125 struct my_service : execution_context::service
126 {
127 explicit my_service(execution_context&) {}
128
129 protected:
130 void shutdown() override
131 {
132 // Cancel pending operations, release resources
133 }
134 };
135 @endcode
136
137 @see execution_context
138 */
139 class BOOST_CAPY_DECL
140 service
141 {
142 public:
143 22 virtual ~service() = default;
144
145 protected:
146 22 service() = default;
147
148 /** Called when the owning execution context shuts down.
149
150 Implementations should release resources and cancel any
151 outstanding asynchronous operations. This function must
152 not block and must not throw exceptions. Services are
153 shut down in reverse order of creation.
154
155 @par Exception Safety
156 No-throw guarantee.
157 */
158 virtual void shutdown() = 0;
159
160 private:
161 friend class execution_context;
162
163 service* next_ = nullptr;
164
165 // warning C4251: 'std::type_index' needs to have dll-interface
166 #ifdef _MSC_VER
167 # pragma warning(push)
168 # pragma warning(disable: 4251)
169 #endif
170 detail::type_index t0_{detail::type_id<void>()};
171 detail::type_index t1_{detail::type_id<void>()};
172 #ifdef _MSC_VER
173 # pragma warning(pop)
174 #endif
175 };
176
177 //------------------------------------------------
178
179 execution_context(execution_context const&) = delete;
180
181 execution_context& operator=(execution_context const&) = delete;
182
183 /** Destructor.
184
185 Calls `shutdown()` then `destroy()` to clean up all services.
186
187 @par Effects
188 All services are shut down and deleted in reverse order
189 of creation.
190
191 @par Exception Safety
192 No-throw guarantee.
193 */
194 ~execution_context();
195
196 /** Default constructor.
197
198 @par Exception Safety
199 Strong guarantee.
200 */
201 execution_context();
202
203 /** Return true if a service of type T exists.
204
205 @par Thread Safety
206 Thread-safe.
207
208 @tparam T The type of service to check.
209
210 @return `true` if the service exists.
211 */
212 template<class T>
213 3 bool has_service() const noexcept
214 {
215 3 return find_service<T>() != nullptr;
216 }
217
218 /** Return a pointer to the service of type T, or nullptr.
219
220 @par Thread Safety
221 Thread-safe.
222
223 @tparam T The type of service to find.
224
225 @return A pointer to the service, or `nullptr` if not present.
226 */
227 template<class T>
228 5 T* find_service() const noexcept
229 {
230 5 std::lock_guard<std::mutex> lock(mutex_);
231 5 return static_cast<T*>(find_impl(detail::type_id<T>()));
232 5 }
233
234 /** Return a reference to the service of type T, creating it if needed.
235
236 If no service of type T exists, one is created by calling
237 `T(execution_context&)`. If T has a nested `key_type`, the
238 service is also indexed under that type.
239
240 @par Constraints
241 @li `T` must derive from `service`.
242 @li `T` must be constructible from `execution_context&`.
243
244 @par Exception Safety
245 Strong guarantee. If service creation throws, the container
246 is unchanged.
247
248 @par Thread Safety
249 Thread-safe.
250
251 @tparam T The type of service to retrieve or create.
252
253 @return A reference to the service.
254 */
255 template<class T>
256 27 T& use_service()
257 {
258 static_assert(std::is_base_of<service, T>::value,
259 "T must derive from service");
260 static_assert(std::is_constructible<T, execution_context&>::value,
261 "T must be constructible from execution_context&");
262
263 struct impl : factory
264 {
265 27 impl()
266 : factory(
267 detail::type_id<T>(),
268 get_key<T>::value
269 ? detail::type_id<typename get_key<T>::type>()
270 27 : detail::type_id<T>())
271 {
272 27 }
273
274 21 service* create(execution_context& ctx) override
275 {
276
1/3
✓ Branch 2 taken 19 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
21 return new T(ctx);
277 }
278 };
279
280 27 impl f;
281
1/1
✓ Branch 1 taken 27 times.
54 return static_cast<T&>(use_service_impl(f));
282 }
283
284 /** Construct and add a service.
285
286 A new service of type T is constructed using the provided
287 arguments and added to the container. If T has a nested
288 `key_type`, the service is also indexed under that type.
289
290 @par Constraints
291 @li `T` must derive from `service`.
292 @li `T` must be constructible from `execution_context&, Args...`.
293 @li If `T::key_type` exists, `T&` must be convertible to `key_type&`.
294
295 @par Exception Safety
296 Strong guarantee. If service creation throws, the container
297 is unchanged.
298
299 @par Thread Safety
300 Thread-safe.
301
302 @throws std::invalid_argument if a service of the same type
303 or `key_type` already exists.
304
305 @tparam T The type of service to create.
306
307 @param args Arguments forwarded to the constructor of T.
308
309 @return A reference to the created service.
310 */
311 template<class T, class... Args>
312 2 T& make_service(Args&&... args)
313 {
314 static_assert(std::is_base_of<service, T>::value,
315 "T must derive from service");
316 if constexpr(get_key<T>::value)
317 {
318 static_assert(
319 std::is_convertible<T&, typename get_key<T>::type&>::value,
320 "T& must be convertible to key_type&");
321 }
322
323 struct impl : factory
324 {
325 std::tuple<Args&&...> args_;
326
327 2 explicit impl(Args&&... a)
328 : factory(
329 detail::type_id<T>(),
330 get_key<T>::value
331 ? detail::type_id<typename get_key<T>::type>()
332 : detail::type_id<T>())
333 2 , args_(std::forward<Args>(a)...)
334 {
335 2 }
336
337 1 service* create(execution_context& ctx) override
338 {
339
1/1
✓ Branch 1 taken 1 time.
2 return std::apply([&ctx](auto&&... a) {
340 1 return new T(ctx, std::forward<decltype(a)>(a)...);
341 3 }, std::move(args_));
342 }
343 };
344
345
1/1
✓ Branch 2 taken 2 times.
2 impl f(std::forward<Args>(args)...);
346
1/1
✓ Branch 1 taken 1 time.
3 return static_cast<T&>(make_service_impl(f));
347 }
348
349 //------------------------------------------------
350
351 /** Return the memory resource used for coroutine frame allocation.
352
353 The returned pointer is valid for the lifetime of this context.
354 By default, this returns a pointer to the recycling memory
355 resource which pools frame allocations for reuse.
356
357 @return Pointer to the frame allocator.
358
359 @see set_frame_allocator
360 */
361 std::pmr::memory_resource*
362 2015 get_frame_allocator() const noexcept
363 {
364 2015 return frame_alloc_;
365 }
366
367 /** Set the memory resource used for coroutine frame allocation.
368
369 The caller is responsible for ensuring the memory resource
370 remains valid for the lifetime of all coroutines launched
371 using this context's executor.
372
373 @par Thread Safety
374 Not thread-safe. Must not be called while any thread may
375 be referencing this execution context or its executor.
376
377 @param mr Pointer to the memory resource.
378
379 @see get_frame_allocator
380 */
381 void
382 set_frame_allocator(std::pmr::memory_resource* mr) noexcept
383 {
384 owned_.reset();
385 frame_alloc_ = mr;
386 }
387
388 /** Set the frame allocator from a standard Allocator.
389
390 The allocator is wrapped in an internal memory resource
391 adapter owned by this context. The wrapper remains valid
392 for the lifetime of this context or until a subsequent
393 call to set_frame_allocator.
394
395 @par Thread Safety
396 Not thread-safe. Must not be called while any thread may
397 be referencing this execution context or its executor.
398
399 @tparam Allocator The allocator type satisfying the
400 standard Allocator requirements.
401
402 @param a The allocator to use.
403
404 @see get_frame_allocator
405 */
406 template<class Allocator>
407 requires (!std::is_pointer_v<Allocator>)
408 void
409 61 set_frame_allocator(Allocator const& a)
410 {
411 static_assert(
412 requires { typename std::allocator_traits<Allocator>::value_type; },
413 "Allocator must satisfy allocator requirements");
414 static_assert(
415 std::is_copy_constructible_v<Allocator>,
416 "Allocator must be copy constructible");
417
418
1/1
✓ Branch 1 taken 61 times.
61 auto p = std::make_shared<
419 detail::frame_memory_resource<Allocator>>(a);
420 61 frame_alloc_ = p.get();
421 61 owned_ = std::move(p);
422 61 }
423
424 /** Return a pointer to this context if it matches the
425 requested type.
426
427 Performs a type check and downcasts `this` when the
428 types match, or returns `nullptr` otherwise. Analogous
429 to `std::any_cast< ExecutionContext >( &a )`.
430
431 @tparam ExecutionContext The derived context type to
432 retrieve.
433
434 @return A pointer to this context as the requested
435 type, or `nullptr` if the type does not match.
436 */
437 template< typename ExecutionContext >
438 const ExecutionContext* target() const
439 {
440 if ( ti_ && *ti_ == detail::type_id< ExecutionContext >() )
441 return static_cast< ExecutionContext const* >( this );
442 return nullptr;
443 }
444
445 /// @copydoc target() const
446 template< typename ExecutionContext >
447 ExecutionContext* target()
448 {
449 if ( ti_ && *ti_ == detail::type_id< ExecutionContext >() )
450 return static_cast< ExecutionContext* >( this );
451 return nullptr;
452 }
453
454 protected:
455 /** Shut down all services.
456
457 Calls `shutdown()` on each service in reverse order of creation.
458 After this call, services remain allocated but are in a stopped
459 state. Derived classes should call this in their destructor
460 before any members are destroyed. This function is idempotent;
461 subsequent calls have no effect.
462
463 @par Effects
464 Each service's `shutdown()` member function is invoked once.
465
466 @par Postconditions
467 @li All services are in a stopped state.
468
469 @par Exception Safety
470 No-throw guarantee.
471
472 @par Thread Safety
473 Not thread-safe. Must not be called concurrently with other
474 operations on this execution_context.
475 */
476 void shutdown() noexcept;
477
478 /** Destroy all services.
479
480 Deletes all services in reverse order of creation. Derived
481 classes should call this as the final step of destruction.
482 This function is idempotent; subsequent calls have no effect.
483
484 @par Preconditions
485 @li `shutdown()` has been called.
486
487 @par Effects
488 All services are deleted and removed from the container.
489
490 @par Postconditions
491 @li The service container is empty.
492
493 @par Exception Safety
494 No-throw guarantee.
495
496 @par Thread Safety
497 Not thread-safe. Must not be called concurrently with other
498 operations on this execution_context.
499 */
500 void destroy() noexcept;
501
502 private:
503 struct BOOST_CAPY_DECL
504 factory
505 {
506 #ifdef _MSC_VER
507 # pragma warning(push)
508 # pragma warning(disable: 4251)
509 #endif
510 // warning C4251: 'std::type_index' needs to have dll-interface
511 detail::type_index t0;
512 detail::type_index t1;
513 #ifdef _MSC_VER
514 # pragma warning(pop)
515 #endif
516
517 29 factory(
518 detail::type_info const& t0_,
519 detail::type_info const& t1_)
520 29 : t0(t0_), t1(t1_)
521 {
522 29 }
523
524 virtual service* create(execution_context&) = 0;
525
526 protected:
527 ~factory() = default;
528 };
529
530 service* find_impl(detail::type_index ti) const noexcept;
531 service& use_service_impl(factory& f);
532 service& make_service_impl(factory& f);
533
534 #ifdef _MSC_VER
535 # pragma warning(push)
536 # pragma warning(disable: 4251)
537 #endif
538 // warning C4251: 'std::type_index' needs to have dll-interface
539 mutable std::mutex mutex_;
540 std::shared_ptr<void> owned_;
541 #ifdef _MSC_VER
542 # pragma warning(pop)
543 #endif
544 std::pmr::memory_resource* frame_alloc_ = nullptr;
545 service* head_ = nullptr;
546 bool shutdown_ = false;
547 };
548
549 template< typename Derived >
550 6 execution_context::
551 execution_context( Derived* ) noexcept
552 6 : execution_context()
553 {
554 6 ti_ = &detail::type_id< Derived >();
555 6 }
556
557 } // namespace capy
558 } // namespace boost
559
560 #endif
561