GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/execution_context.hpp
Date: 2026-01-15 20:40:20
Exec Total Coverage
Lines: 31 31 100.0%
Functions: 32 32 100.0%
Branches: 6 8 75.0%

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_EXECUTION_CONTEXT_HPP
11 #define BOOST_CAPY_EXECUTION_CONTEXT_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/concept/executor.hpp>
15 #include <boost/capy/core/intrusive_queue.hpp>
16 #include <concepts>
17 #include <mutex>
18 #include <tuple>
19 #include <type_traits>
20 #include <typeindex>
21 #include <utility>
22
23 namespace boost {
24 namespace capy {
25
26 /** Base class for I/O object containers providing service management.
27
28 An execution context represents a place where function objects are
29 executed. It provides a service registry where polymorphic services
30 can be stored and retrieved by type. Each service type may be stored
31 at most once. Services may specify a nested `key_type` to enable
32 lookup by a base class type.
33
34 Derived classes such as `io_context` extend this to provide
35 execution facilities like event loops and thread pools. Derived
36 class destructors must call `shutdown()` and `destroy()` to ensure
37 proper service cleanup before member destruction.
38
39 @par Service Lifecycle
40 Services are created on first use via `use_service()` or explicitly
41 via `make_service()`. During destruction, `shutdown()` is called on
42 each service in reverse order of creation, then `destroy()` deletes
43 them. Both functions are idempotent.
44
45 @par Thread Safety
46 Service registration and lookup functions are thread-safe.
47 The `shutdown()` and `destroy()` functions are not thread-safe
48 and must only be called during destruction.
49
50 @par Example
51 @code
52 struct file_service : execution_context::service
53 {
54 protected:
55 void shutdown() override {}
56 };
57
58 struct posix_file_service : file_service
59 {
60 using key_type = file_service;
61
62 explicit posix_file_service(execution_context&) {}
63 };
64
65 class io_context : public execution_context
66 {
67 public:
68 ~io_context()
69 {
70 shutdown();
71 destroy();
72 }
73 };
74
75 io_context ctx;
76 ctx.make_service<posix_file_service>();
77 ctx.find_service<file_service>(); // returns posix_file_service*
78 ctx.find_service<posix_file_service>(); // also works
79 @endcode
80
81 @see service, is_execution_context
82 */
83 class BOOST_CAPY_DECL
84 execution_context
85 {
86 template<class T, class = void>
87 struct get_key : std::false_type
88 {};
89
90 template<class T>
91 struct get_key<T, std::void_t<typename T::key_type>> : std::true_type
92 {
93 using type = typename T::key_type;
94 };
95
96 public:
97 //------------------------------------------------
98
99 /** Abstract base class for services owned by an execution context.
100
101 Services provide extensible functionality to an execution context.
102 Each service type can be registered at most once. Services are
103 created via `use_service()` or `make_service()` and are owned by
104 the execution context for their lifetime.
105
106 Derived classes must implement the pure virtual `shutdown()` member
107 function, which is called when the owning execution context is
108 being destroyed. The `shutdown()` function should release resources
109 and cancel outstanding operations without blocking.
110
111 @par Deriving from service
112 @li Implement `shutdown()` to perform cleanup.
113 @li Accept `execution_context&` as the first constructor parameter.
114 @li Optionally define `key_type` to enable base-class lookup.
115
116 @par Example
117 @code
118 struct my_service : execution_context::service
119 {
120 explicit my_service(execution_context&) {}
121
122 protected:
123 void shutdown() override
124 {
125 // Cancel pending operations, release resources
126 }
127 };
128 @endcode
129
130 @see execution_context
131 */
132 class service
133 {
134 public:
135 36 virtual ~service() = default;
136
137 protected:
138 18 service() = default;
139
140 /** Called when the owning execution context shuts down.
141
142 Implementations should release resources and cancel any
143 outstanding asynchronous operations. This function must
144 not block and must not throw exceptions. Services are
145 shut down in reverse order of creation.
146
147 @par Exception Safety
148 No-throw guarantee.
149 */
150 virtual void shutdown() = 0;
151
152 private:
153 friend class execution_context;
154
155 service* next_ = nullptr;
156 std::type_index t0_ = typeid(void);
157 std::type_index t1_ = typeid(void);
158 };
159
160 //------------------------------------------------
161
162 /** Abstract base class for completion handlers.
163
164 Handlers are continuations that execute after an asynchronous
165 operation completes. They can be queued for deferred invocation,
166 allowing callbacks and coroutine resumptions to be posted to an
167 executor.
168
169 Handlers should execute quickly - typically just initiating
170 another I/O operation or suspending on a foreign task. Heavy
171 computation should be avoided in handlers to prevent blocking
172 the event loop.
173
174 Handlers may be heap-allocated or may be data members of an
175 enclosing object. The allocation strategy is determined by the
176 creator of the handler.
177
178 @par Ownership Contract
179
180 Callers must invoke exactly ONE of `operator()` or `destroy()`,
181 never both:
182
183 @li `operator()` - Invokes the handler. The handler is
184 responsible for its own cleanup (typically `delete this`
185 for heap-allocated handlers). The caller must not call
186 `destroy()` after invoking this.
187
188 @li `destroy()` - Destroys an uninvoked handler. This is
189 called when a queued handler must be discarded without
190 execution, such as during shutdown or exception cleanup.
191 For heap-allocated handlers, this typically calls
192 `delete this`.
193
194 @par Exception Safety
195
196 Implementations of `operator()` must perform cleanup before
197 any operation that might throw. This ensures that if the handler
198 throws, the exception propagates cleanly to the caller of
199 `run()` without leaking resources. Typical pattern:
200
201 @code
202 void operator()() override
203 {
204 auto h = h_;
205 delete this; // cleanup FIRST
206 h.resume(); // then resume (may throw)
207 }
208 @endcode
209
210 This "delete-before-invoke" pattern also enables memory
211 recycling - the handler's memory can be reused immediately
212 by subsequent allocations.
213
214 @note Callers must never delete handlers directly with `delete`;
215 use `operator()` for normal invocation or `destroy()` for cleanup.
216
217 @note Heap-allocated handlers are typically allocated with
218 custom allocators to minimize allocation overhead in
219 high-frequency async operations.
220
221 @note Some handlers (such as those owned by containers like
222 `std::unique_ptr` or embedded as data members) are not meant to
223 be destroyed and should implement both functions as no-ops
224 (for `operator()`, invoke the continuation but don't delete).
225
226 @see queue
227 */
228 class handler : public intrusive_queue<handler>::node
229 {
230 public:
231 virtual void operator()() = 0;
232 virtual void destroy() = 0;
233
234 /** Returns the user-defined data pointer.
235
236 Derived classes may set this to store auxiliary data
237 such as a pointer to the most-derived object.
238
239 @par Postconditions
240 @li Initially returns `nullptr` for newly constructed handlers.
241 @li Returns the current value of `data_` if modified by a derived class.
242
243 @return The user-defined data pointer, or `nullptr` if not set.
244 */
245 4 void* data() const noexcept
246 {
247 4 return data_;
248 }
249
250 protected:
251 ~handler() = default;
252
253 void* data_ = nullptr;
254 };
255
256 //------------------------------------------------
257
258 /** An intrusive FIFO queue of handlers.
259
260 This queue stores handlers using an intrusive linked list,
261 avoiding additional allocations for queue nodes. Handlers
262 are popped in the order they were pushed (first-in, first-out).
263
264 The destructor calls `destroy()` on any remaining handlers.
265
266 @note This is not thread-safe. External synchronization is
267 required for concurrent access.
268
269 @see handler
270 */
271 class queue
272 {
273 intrusive_queue<handler> q_;
274
275 public:
276 /** Default constructor.
277
278 Creates an empty queue.
279
280 @post `empty() == true`
281 */
282 queue() = default;
283
284 /** Move constructor.
285
286 Takes ownership of all handlers from `other`,
287 leaving `other` empty.
288
289 @param other The queue to move from.
290
291 @post `other.empty() == true`
292 */
293 queue(queue&& other) noexcept
294 : q_(std::move(other.q_))
295 {
296 }
297
298 queue(queue const&) = delete;
299 queue& operator=(queue const&) = delete;
300 queue& operator=(queue&&) = delete;
301
302 /** Destructor.
303
304 Calls `destroy()` on any remaining handlers in the queue.
305 */
306 ~queue()
307 {
308 while(auto* h = q_.pop())
309 h->destroy();
310 }
311
312 /** Return true if the queue is empty.
313
314 @return `true` if the queue contains no handlers.
315 */
316 bool
317 empty() const noexcept
318 {
319 return q_.empty();
320 }
321
322 /** Add a handler to the back of the queue.
323
324 @param h Pointer to the handler to add.
325
326 @pre `h` is not null and not already in a queue.
327 */
328 void
329 push(handler* h) noexcept
330 {
331 q_.push(h);
332 }
333
334 /** Splice all handlers from another queue to the back.
335
336 All handlers from `other` are moved to the back of this
337 queue. After this call, `other` is empty.
338
339 @param other The queue to splice from.
340
341 @post `other.empty() == true`
342 */
343 void
344 push(queue& other) noexcept
345 {
346 q_.splice(other.q_);
347 }
348
349 /** Remove and return the front handler.
350
351 @return Pointer to the front handler, or `nullptr`
352 if the queue is empty.
353 */
354 handler*
355 pop() noexcept
356 {
357 return q_.pop();
358 }
359 };
360
361 //------------------------------------------------
362
363 execution_context(execution_context const&) = delete;
364
365 execution_context& operator=(execution_context const&) = delete;
366
367 /** Destructor.
368
369 Calls `shutdown()` then `destroy()` to clean up all services.
370
371 @par Effects
372 All services are shut down and deleted in reverse order
373 of creation.
374
375 @par Exception Safety
376 No-throw guarantee.
377 */
378 ~execution_context();
379
380 /** Default constructor.
381
382 @par Exception Safety
383 Strong guarantee.
384 */
385 execution_context();
386
387 /** Return true if a service of type T exists.
388
389 @par Thread Safety
390 Thread-safe.
391
392 @tparam T The type of service to check.
393
394 @return `true` if the service exists.
395 */
396 template<class T>
397 24 bool has_service() const noexcept
398 {
399 24 return find_service<T>() != nullptr;
400 }
401
402 /** Return a pointer to the service of type T, or nullptr.
403
404 @par Thread Safety
405 Thread-safe.
406
407 @tparam T The type of service to find.
408
409 @return A pointer to the service, or `nullptr` if not present.
410 */
411 template<class T>
412 40 T* find_service() const noexcept
413 {
414 40 std::lock_guard<std::mutex> lock(mutex_);
415 40 return static_cast<T*>(find_impl(typeid(T)));
416 40 }
417
418 /** Return a reference to the service of type T, creating it if needed.
419
420 If no service of type T exists, one is created by calling
421 `T(execution_context&)`. If T has a nested `key_type`, the
422 service is also indexed under that type.
423
424 @par Constraints
425 @li `T` must derive from `service`.
426 @li `T` must be constructible from `execution_context&`.
427
428 @par Exception Safety
429 Strong guarantee. If service creation throws, the container
430 is unchanged.
431
432 @par Thread Safety
433 Thread-safe.
434
435 @tparam T The type of service to retrieve or create.
436
437 @return A reference to the service.
438 */
439 template<class T>
440 32 T& use_service()
441 {
442 static_assert(std::is_base_of<service, T>::value,
443 "T must derive from service");
444 static_assert(std::is_constructible<T, execution_context&>::value,
445 "T must be constructible from execution_context&");
446
447 struct impl : factory
448 {
449 17 impl()
450 : factory(
451 typeid(T),
452 get_key<T>::value
453 ? typeid(typename get_key<T>::type)
454 17 : typeid(T))
455 {
456 17 }
457
458 11 service* create(execution_context& ctx) override
459 {
460 11 return new T(ctx);
461 }
462 };
463
464 32 impl f;
465
1/1
✓ Branch 1 taken 17 times.
64 return static_cast<T&>(use_service_impl(f));
466 }
467
468 /** Construct and add a service.
469
470 A new service of type T is constructed using the provided
471 arguments and added to the container. If T has a nested
472 `key_type`, the service is also indexed under that type.
473
474 @par Constraints
475 @li `T` must derive from `service`.
476 @li `T` must be constructible from `execution_context&, Args...`.
477 @li If `T::key_type` exists, `T&` must be convertible to `key_type&`.
478
479 @par Exception Safety
480 Strong guarantee. If service creation throws, the container
481 is unchanged.
482
483 @par Thread Safety
484 Thread-safe.
485
486 @throws std::invalid_argument if a service of the same type
487 or `key_type` already exists.
488
489 @tparam T The type of service to create.
490
491 @param args Arguments forwarded to the constructor of T.
492
493 @return A reference to the created service.
494 */
495 template<class T, class... Args>
496 18 T& make_service(Args&&... args)
497 {
498 static_assert(std::is_base_of<service, T>::value,
499 "T must derive from service");
500 if constexpr(get_key<T>::value)
501 {
502 static_assert(
503 std::is_convertible<T&, typename get_key<T>::type&>::value,
504 "T& must be convertible to key_type&");
505 }
506
507 struct impl : factory
508 {
509 std::tuple<Args&&...> args_;
510
511 10 explicit impl(Args&&... a)
512 : factory(
513 typeid(T),
514 get_key<T>::value
515 ? typeid(typename get_key<T>::type)
516 : typeid(T))
517 10 , args_(std::forward<Args>(a)...)
518 {
519 10 }
520
521 7 service* create(execution_context& ctx) override
522 {
523
1/1
✓ Branch 1 taken 1 times.
20 return std::apply([&ctx](auto&&... a) {
524
1/3
✓ Branch 4 taken 1 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
9 return new T(ctx, std::forward<decltype(a)>(a)...);
525 21 }, std::move(args_));
526 }
527 };
528
529
2/2
✓ Branch 4 taken 2 times.
✓ Branch 2 taken 6 times.
18 impl f(std::forward<Args>(args)...);
530
1/1
✓ Branch 1 taken 7 times.
31 return static_cast<T&>(make_service_impl(f));
531 }
532
533 protected:
534 /** Shut down all services.
535
536 Calls `shutdown()` on each service in reverse order of creation.
537 After this call, services remain allocated but are in a stopped
538 state. Derived classes should call this in their destructor
539 before any members are destroyed. This function is idempotent;
540 subsequent calls have no effect.
541
542 @par Effects
543 Each service's `shutdown()` member function is invoked once.
544
545 @par Postconditions
546 @li All services are in a stopped state.
547
548 @par Exception Safety
549 No-throw guarantee.
550
551 @par Thread Safety
552 Not thread-safe. Must not be called concurrently with other
553 operations on this execution_context.
554 */
555 void shutdown() noexcept;
556
557 /** Destroy all services.
558
559 Deletes all services in reverse order of creation. Derived
560 classes should call this as the final step of destruction.
561 This function is idempotent; subsequent calls have no effect.
562
563 @par Preconditions
564 @li `shutdown()` has been called.
565
566 @par Effects
567 All services are deleted and removed from the container.
568
569 @par Postconditions
570 @li The service container is empty.
571
572 @par Exception Safety
573 No-throw guarantee.
574
575 @par Thread Safety
576 Not thread-safe. Must not be called concurrently with other
577 operations on this execution_context.
578 */
579 void destroy() noexcept;
580
581 private:
582 struct factory
583 {
584 std::type_index t0;
585 std::type_index t1;
586
587 27 factory(std::type_index t0_, std::type_index t1_)
588 27 : t0(t0_), t1(t1_)
589 {
590 27 }
591
592 virtual service* create(execution_context&) = 0;
593
594 protected:
595 ~factory() = default;
596 };
597
598 service* find_impl(std::type_index ti) const noexcept;
599 service& use_service_impl(factory& f);
600 service& make_service_impl(factory& f);
601
602 #ifdef _MSC_VER
603 # pragma warning(push)
604 # pragma warning(disable: 4251)
605 #endif
606 mutable std::mutex mutex_;
607 #ifdef _MSC_VER
608 # pragma warning(pop)
609 #endif
610 service* head_ = nullptr;
611 bool shutdown_ = false;
612 };
613
614 } // namespace capy
615 } // namespace boost
616
617 #endif
618