LCOV - code coverage report
Current view: top level - boost/capy/ex - frame_allocator.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 81.5 % 27 22
Test Date: 2026-01-18 18:26:31 Functions: 83.3 % 6 5

            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_FRAME_ALLOCATOR_HPP
      11              : #define BOOST_CAPY_FRAME_ALLOCATOR_HPP
      12              : 
      13              : #include <boost/capy/detail/config.hpp>
      14              : #include <boost/capy/concept/frame_allocator.hpp>
      15              : 
      16              : #include <cstddef>
      17              : #include <new>
      18              : #include <utility>
      19              : 
      20              : namespace boost {
      21              : namespace capy {
      22              : 
      23              : //----------------------------------------------------------
      24              : // Public API
      25              : //----------------------------------------------------------
      26              : 
      27              : /** A frame allocator that passes through to global new/delete.
      28              : 
      29              :     This allocator provides no pooling or recycling—each allocation
      30              :     goes directly to `::operator new` and each deallocation goes to
      31              :     `::operator delete`. It serves as a baseline for comparison and
      32              :     as a fallback when pooling is not desired.
      33              : */
      34              : struct default_frame_allocator
      35              : {
      36              :     void* allocate(std::size_t n)
      37              :     {
      38              :         return ::operator new(n);
      39              :     }
      40              : 
      41              :     void deallocate(void* p, std::size_t)
      42              :     {
      43              :         ::operator delete(p);
      44              :     }
      45              : };
      46              : 
      47              : static_assert(FrameAllocator<default_frame_allocator>);
      48              : 
      49              : //----------------------------------------------------------
      50              : // Implementation details
      51              : //----------------------------------------------------------
      52              : 
      53              : namespace detail {
      54              : 
      55              : /** Abstract base class for internal frame allocator wrappers.
      56              : 
      57              :     This class provides a polymorphic interface used internally
      58              :     by the frame allocation machinery. User-defined allocators
      59              :     do not inherit from this class.
      60              : */
      61              : class frame_allocator_base
      62              : {
      63              : public:
      64              :     virtual ~frame_allocator_base() {}
      65              : 
      66              :     /** Allocate memory for a coroutine frame.
      67              : 
      68              :         @param n The number of bytes to allocate.
      69              : 
      70              :         @return A pointer to the allocated memory.
      71              :     */
      72              :     virtual void* allocate(std::size_t n) = 0;
      73              : 
      74              :     /** Deallocate memory for a coroutine frame.
      75              : 
      76              :         @param p Pointer to the memory to deallocate.
      77              :         @param n The user-requested size (not total allocation).
      78              :     */
      79              :     virtual void deallocate(void* p, std::size_t n) = 0;
      80              : };
      81              : 
      82              : /** Frame allocator wrapper that lives in the launcher frame.
      83              : 
      84              :     This wrapper is stored in the run_async launcher's promise and
      85              :     handles all coroutine frame allocations. Because the launcher
      86              :     frame is destroyed LAST (after all inner coroutines), this
      87              :     wrapper is guaranteed to outlive all frames that reference it.
      88              : 
      89              :     All allocated frames have the layout: [frame | ptr]
      90              :     where ptr points back to this wrapper for deallocation.
      91              : 
      92              :     @tparam Allocator The underlying allocator type satisfying FrameAllocator.
      93              : */
      94              : template<FrameAllocator Allocator>
      95              : class frame_allocator_wrapper : public frame_allocator_base
      96              : {
      97              :     Allocator alloc_;
      98              : 
      99              :     static constexpr std::size_t alignment = alignof(void*);
     100              : 
     101              :     static std::size_t
     102              :     aligned_offset(std::size_t n) noexcept
     103              :     {
     104              :         return (n + alignment - 1) & ~(alignment - 1);
     105              :     }
     106              : 
     107              : public:
     108              :     explicit frame_allocator_wrapper(Allocator a)
     109              :         : alloc_(std::move(a))
     110              :     {
     111              :     }
     112              : 
     113              :     void*
     114              :     allocate(std::size_t n) override
     115              :     {
     116              :         // Layout: [frame | ptr]
     117              :         std::size_t ptr_offset = aligned_offset(n);
     118              :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     119              : 
     120              :         void* raw = alloc_.allocate(total);
     121              : 
     122              :         // Store pointer to self at fixed offset
     123              :         auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
     124              :             static_cast<char*>(raw) + ptr_offset);
     125              :         *ptr_loc = this;
     126              : 
     127              :         return raw;
     128              :     }
     129              : 
     130              :     void
     131              :     deallocate(void* block, std::size_t user_size) override
     132              :     {
     133              :         std::size_t ptr_offset = aligned_offset(user_size);
     134              :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     135              :         alloc_.deallocate(block, total);
     136              :     }
     137              : };
     138              : 
     139              : } // namespace detail
     140              : 
     141              : /** Mixin base for promise types to support custom frame allocation.
     142              : 
     143              :     Derive your promise_type from this class to enable custom coroutine
     144              :     frame allocation via a thread-local allocator pointer.
     145              : 
     146              :     The allocation strategy:
     147              :     @li If a thread-local allocator is set, use it for allocation
     148              :     @li Otherwise, fall back to global `::operator new`/`::operator delete`
     149              : 
     150              :     A pointer is stored at the end of each allocation to enable correct
     151              :     deallocation regardless of which allocator was active at allocation time.
     152              : 
     153              :     @par Memory Layout
     154              : 
     155              :     All coroutine frames have the same layout:
     156              :     @code
     157              :     [coroutine frame | ptr]
     158              :     @endcode
     159              : 
     160              :     Where ptr points to the frame_allocator_wrapper in the launcher frame,
     161              :     or is nullptr if allocated with global new/delete.
     162              : 
     163              :     @see frame_allocator
     164              : */
     165              : struct frame_allocating_base
     166              : {
     167              : private:
     168              :     static constexpr std::size_t alignment = alignof(void*);
     169              : 
     170              :     static std::size_t
     171          570 :     aligned_offset(std::size_t n) noexcept
     172              :     {
     173          570 :         return (n + alignment - 1) & ~(alignment - 1);
     174              :     }
     175              : 
     176              :     static detail::frame_allocator_base*&
     177          509 :     current_allocator() noexcept
     178              :     {
     179              :         static thread_local detail::frame_allocator_base* alloc = nullptr;
     180          509 :         return alloc;
     181              :     }
     182              : 
     183              : public:
     184              :     /** Set the thread-local frame allocator.
     185              : 
     186              :         The allocator will be used for subsequent coroutine frame
     187              :         allocations on this thread until changed or cleared.
     188              : 
     189              :         @param alloc The allocator to use. Must outlive all coroutines
     190              :                      allocated with it.
     191              :     */
     192              :     static void
     193            0 :     set_frame_allocator(detail::frame_allocator_base& alloc) noexcept
     194              :     {
     195            0 :         current_allocator() = &alloc;
     196            0 :     }
     197              : 
     198              :     /** Clear the thread-local frame allocator.
     199              : 
     200              :         Subsequent allocations will use global `::operator new`.
     201              :     */
     202              :     static void
     203              :     clear_frame_allocator() noexcept
     204              :     {
     205              :         current_allocator() = nullptr;
     206              :     }
     207              : 
     208              :     /** Get the current thread-local frame allocator.
     209              : 
     210              :         @return Pointer to current allocator, or nullptr if none set.
     211              :     */
     212              :     static detail::frame_allocator_base*
     213          224 :     get_frame_allocator() noexcept
     214              :     {
     215          224 :         return current_allocator();
     216              :     }
     217              : 
     218              :     // GCC 11+ emits -Wmismatched-new-delete because it tracks that
     219              :     // operator new returns a pointer, and operator delete should be
     220              :     // the one to free it. Our design intentionally over-allocates to
     221              :     // store a pointer at the end of each frame. The deallocation is
     222              :     // correct: we recalculate the total size and free the full block.
     223              :     // This warning is suppressed for these two functions only.
     224              : #if defined(__GNUC__)
     225              : #pragma GCC diagnostic push
     226              : #pragma GCC diagnostic ignored "-Wmismatched-new-delete"
     227              : #endif
     228              : 
     229              :     /** Allocate a coroutine frame.
     230              : 
     231              :         If a thread-local allocator is set, delegates to it.
     232              :         Otherwise, allocates with extra space for a null pointer marker.
     233              :     */
     234              :     static void*
     235          285 :     operator new(std::size_t size)
     236              :     {
     237          285 :         auto* alloc = current_allocator();
     238          285 :         if(!alloc)
     239              :         {
     240              :             // No allocator: allocate extra space for null pointer marker
     241          285 :             std::size_t ptr_offset = aligned_offset(size);
     242          285 :             std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*);
     243          285 :             void* raw = ::operator new(total);
     244              : 
     245              :             // Store nullptr to indicate global new/delete
     246          285 :             auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     247              :                 static_cast<char*>(raw) + ptr_offset);
     248          285 :             *ptr_loc = nullptr;
     249              : 
     250          285 :             return raw;
     251              :         }
     252            0 :         return alloc->allocate(size);
     253              :     }
     254              : 
     255              :     /** Deallocate a coroutine frame.
     256              : 
     257              :         Reads the pointer stored at the end of the frame to find
     258              :         the allocator wrapper. A null pointer indicates the frame
     259              :         was allocated with global new/delete (no custom allocator
     260              :         was active).
     261              :     */
     262              :     static void
     263          285 :     operator delete(void* ptr, std::size_t size)
     264              :     {
     265              :         // Pointer is always at aligned_offset(size)
     266          285 :         std::size_t ptr_offset = aligned_offset(size);
     267          285 :         auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     268              :             static_cast<char*>(ptr) + ptr_offset);
     269          285 :         auto* wrapper = *ptr_loc;
     270              : 
     271              :         // Null pointer means global new/delete
     272          285 :         if(!wrapper)
     273              :         {
     274          285 :             ::operator delete(ptr);
     275          285 :             return;
     276              :         }
     277              : 
     278            0 :         wrapper->deallocate(ptr, size);
     279              :     }
     280              : 
     281              : #if defined(__GNUC__)
     282              : #pragma GCC diagnostic pop
     283              : #endif
     284              : };
     285              : 
     286              : } // namespace capy
     287              : } // namespace boost
     288              : 
     289              : #endif
        

Generated by: LCOV version 2.3