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_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 18 : 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 13 : bool has_service() const noexcept
398 : {
399 13 : 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 22 : T* find_service() const noexcept
413 : {
414 22 : std::lock_guard<std::mutex> lock(mutex_);
415 22 : return static_cast<T*>(find_impl(typeid(T)));
416 22 : }
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 17 : 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 17 : impl f;
465 34 : 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 10 : 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 20 : return std::apply([&ctx](auto&&... a) {
524 9 : return new T(ctx, std::forward<decltype(a)>(a)...);
525 21 : }, std::move(args_));
526 : }
527 : };
528 :
529 10 : impl f(std::forward<Args>(args)...);
530 17 : 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
|