LCOV - code coverage report
Current view: top level - boost/capy/ex - frame_allocator.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 25.0 % 56 14
Test Date: 2026-01-15 18:27:21 Functions: 37.5 % 16 6

            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 <cstdint>
      18              : #include <new>
      19              : #include <utility>
      20              : 
      21              : namespace boost {
      22              : namespace capy {
      23              : 
      24              : //----------------------------------------------------------
      25              : // Public API
      26              : //----------------------------------------------------------
      27              : 
      28              : /** A frame allocator that passes through to global new/delete.
      29              : 
      30              :     This allocator provides no pooling or recycling—each allocation
      31              :     goes directly to `::operator new` and each deallocation goes to
      32              :     `::operator delete`. It serves as a baseline for comparison and
      33              :     as a fallback when pooling is not desired.
      34              : */
      35              : struct default_frame_allocator
      36              : {
      37              :     void* allocate(std::size_t n)
      38              :     {
      39              :         return ::operator new(n);
      40              :     }
      41              : 
      42              :     void deallocate(void* p, std::size_t)
      43              :     {
      44              :         ::operator delete(p);
      45              :     }
      46              : };
      47              : 
      48              : static_assert(frame_allocator<default_frame_allocator>);
      49              : 
      50              : //----------------------------------------------------------
      51              : // Implementation details
      52              : //----------------------------------------------------------
      53              : 
      54              : namespace detail {
      55              : 
      56              : /** Abstract base class for internal frame allocator wrappers.
      57              : 
      58              :     This class provides a polymorphic interface used internally
      59              :     by the frame allocation machinery. User-defined allocators
      60              :     do not inherit from this class.
      61              : */
      62              : class frame_allocator_base
      63              : {
      64              : public:
      65           44 :     virtual ~frame_allocator_base() {}
      66              : 
      67              :     /** Allocate memory for a coroutine frame.
      68              : 
      69              :         @param n The number of bytes to allocate.
      70              : 
      71              :         @return A pointer to the allocated memory.
      72              :     */
      73              :     virtual void* allocate(std::size_t n) = 0;
      74              : 
      75              :     /** Deallocate memory for a child coroutine frame.
      76              : 
      77              :         @param p Pointer to the memory to deallocate.
      78              :         @param n The user-requested size (not total allocation).
      79              :     */
      80              :     virtual void deallocate(void* p, std::size_t n) = 0;
      81              : 
      82              :     /** Deallocate the first coroutine frame (where this wrapper is embedded).
      83              : 
      84              :         This method handles the special case where the wrapper itself
      85              :         is embedded at the end of the block being deallocated.
      86              : 
      87              :         @param block Pointer to the block to deallocate.
      88              :         @param user_size The user-requested size (not total allocation).
      89              :     */
      90              :     virtual void deallocate_embedded(void* block, std::size_t user_size) = 0;
      91              : };
      92              : 
      93              : // Forward declaration
      94              : template<frame_allocator Allocator>
      95              : class frame_allocator_wrapper;
      96              : 
      97              : /** Wrapper that embeds a frame_allocator_wrapper in the first allocation.
      98              : 
      99              :     This wrapper lives on the stack (in async_run_awaitable) and is used only
     100              :     for the FIRST coroutine frame allocation. It embeds a copy of
     101              :     frame_allocator_wrapper at the end of the allocated block, then
     102              :     updates TLS to point to that embedded wrapper for subsequent
     103              :     allocations.
     104              : 
     105              :     @tparam Allocator The underlying allocator type satisfying frame_allocator.
     106              : */
     107              : template<frame_allocator Allocator>
     108              : class embedding_frame_allocator : public frame_allocator_base
     109              : {
     110              :     Allocator alloc_;
     111              : 
     112              :     static constexpr std::size_t alignment = alignof(void*);
     113              : 
     114              :     static_assert(
     115              :         alignof(frame_allocator_wrapper<Allocator>) <= alignment,
     116              :         "alignment must be at least as strict as wrapper alignment");
     117              : 
     118              :     static std::size_t
     119            0 :     aligned_offset(std::size_t n) noexcept
     120              :     {
     121            0 :         return (n + alignment - 1) & ~(alignment - 1);
     122              :     }
     123              : 
     124              : public:
     125           44 :     explicit embedding_frame_allocator(Allocator a)
     126           44 :         : alloc_(std::move(a))
     127              :     {
     128           44 :     }
     129              : 
     130              :     void*
     131              :     allocate(std::size_t n) override;
     132              : 
     133              :     void
     134            0 :     deallocate(void*, std::size_t) override
     135              :     {
     136              :         // Never called - stack wrapper not used for deallocation
     137            0 :     }
     138              : 
     139              :     void
     140            0 :     deallocate_embedded(void*, std::size_t) override
     141              :     {
     142              :         // Never called
     143            0 :     }
     144              : };
     145              : 
     146              : /** Wrapper embedded in the first coroutine frame.
     147              : 
     148              :     This wrapper is constructed at the end of the first coroutine
     149              :     frame by embedding_frame_allocator. It handles all subsequent
     150              :     allocations (storing a pointer to itself) and all deallocations.
     151              : 
     152              :     @tparam Allocator The underlying allocator type satisfying frame_allocator.
     153              : */
     154              : template<frame_allocator Allocator>
     155              : class frame_allocator_wrapper : public frame_allocator_base
     156              : {
     157              :     Allocator alloc_;
     158              : 
     159              :     static constexpr std::size_t alignment = alignof(void*);
     160              : 
     161              :     static std::size_t
     162            0 :     aligned_offset(std::size_t n) noexcept
     163              :     {
     164            0 :         return (n + alignment - 1) & ~(alignment - 1);
     165              :     }
     166              : 
     167              : public:
     168            0 :     explicit frame_allocator_wrapper(Allocator a)
     169            0 :         : alloc_(std::move(a))
     170              :     {
     171            0 :     }
     172              : 
     173              :     void*
     174            0 :     allocate(std::size_t n) override
     175              :     {
     176              :         // Layout: [frame | ptr]
     177            0 :         std::size_t ptr_offset = aligned_offset(n);
     178            0 :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     179              : 
     180            0 :         void* raw = alloc_.allocate(total);
     181              : 
     182              :         // Store untagged pointer to self at fixed offset
     183            0 :         auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
     184              :             static_cast<char*>(raw) + ptr_offset);
     185            0 :         *ptr_loc = this;
     186              : 
     187            0 :         return raw;
     188              :     }
     189              : 
     190              :     void
     191            0 :     deallocate(void* block, std::size_t user_size) override
     192              :     {
     193              :         // Child frame deallocation: layout is [frame | ptr]
     194            0 :         std::size_t ptr_offset = aligned_offset(user_size);
     195            0 :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     196            0 :         alloc_.deallocate(block, total);
     197            0 :     }
     198              : 
     199              :     void
     200            0 :     deallocate_embedded(void* block, std::size_t user_size) override
     201              :     {
     202              :         // First frame deallocation: layout is [frame | ptr | wrapper]
     203            0 :         std::size_t ptr_offset = aligned_offset(user_size);
     204            0 :         std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
     205            0 :         std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper);
     206              : 
     207              :         Allocator alloc_copy = alloc_;  // Copy before destroying self
     208            0 :         this->~frame_allocator_wrapper();
     209            0 :         alloc_copy.deallocate(block, total);
     210            0 :     }
     211              : };
     212              : 
     213              : } // namespace detail
     214              : 
     215              : /** Mixin base for promise types to support custom frame allocation.
     216              : 
     217              :     Derive your promise_type from this class to enable custom coroutine
     218              :     frame allocation via a thread-local allocator pointer.
     219              : 
     220              :     The allocation strategy:
     221              :     @li If a thread-local allocator is set, use it for allocation
     222              :     @li Otherwise, fall back to global `::operator new`/`::operator delete`
     223              : 
     224              :     A pointer is stored at the end of each allocation to enable correct
     225              :     deallocation regardless of which allocator was active at allocation time.
     226              : 
     227              :     @par Memory Layout
     228              : 
     229              :     For the first coroutine frame (allocated via embedding_frame_allocator):
     230              :     @code
     231              :     [coroutine frame | tagged_ptr | frame_allocator_wrapper]
     232              :     @endcode
     233              : 
     234              :     For subsequent frames (allocated via frame_allocator_wrapper):
     235              :     @code
     236              :     [coroutine frame | ptr]
     237              :     @endcode
     238              : 
     239              :     The tag bit (low bit) distinguishes the two cases during deallocation.
     240              : 
     241              :     @see frame_allocator
     242              : */
     243              : struct frame_allocating_base
     244              : {
     245              : private:
     246              :     static constexpr std::size_t alignment = alignof(void*);
     247              : 
     248              :     static std::size_t
     249              :     aligned_offset(std::size_t n) noexcept
     250              :     {
     251              :         return (n + alignment - 1) & ~(alignment - 1);
     252              :     }
     253              : 
     254              :     static detail::frame_allocator_base*&
     255          501 :     current_allocator() noexcept
     256              :     {
     257              :         static thread_local detail::frame_allocator_base* alloc = nullptr;
     258          501 :         return alloc;
     259              :     }
     260              : 
     261              : public:
     262              :     /** Set the thread-local frame allocator.
     263              : 
     264              :         The allocator will be used for subsequent coroutine frame
     265              :         allocations on this thread until changed or cleared.
     266              : 
     267              :         @param alloc The allocator to use. Must outlive all coroutines
     268              :                      allocated with it.
     269              :     */
     270              :     static void
     271          280 :     set_frame_allocator(detail::frame_allocator_base& alloc) noexcept
     272              :     {
     273          280 :         current_allocator() = &alloc;
     274          280 :     }
     275              : 
     276              :     /** Clear the thread-local frame allocator.
     277              : 
     278              :         Subsequent allocations will use global `::operator new`.
     279              :     */
     280              :     static void
     281           44 :     clear_frame_allocator() noexcept
     282              :     {
     283           44 :         current_allocator() = nullptr;
     284           44 :     }
     285              : 
     286              :     /** Get the current thread-local frame allocator.
     287              : 
     288              :         @return Pointer to current allocator, or nullptr if none set.
     289              :     */
     290              :     static detail::frame_allocator_base*
     291          177 :     get_frame_allocator() noexcept
     292              :     {
     293          177 :         return current_allocator();
     294              :     }
     295              : 
     296              :     // VFALCO turned off
     297              : #if 0
     298              :     static void*
     299              :     operator new(std::size_t size)
     300              :     {
     301              :         auto* alloc = current_allocator();
     302              :         if(!alloc)
     303              :         {
     304              :             // No allocator: allocate extra space for null pointer marker
     305              :             std::size_t ptr_offset = aligned_offset(size);
     306              :             std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*);
     307              :             void* raw = ::operator new(total);
     308              : 
     309              :             // Store nullptr to indicate global new/delete
     310              :             auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     311              :                 static_cast<char*>(raw) + ptr_offset);
     312              :             *ptr_loc = nullptr;
     313              : 
     314              :             return raw;
     315              :         }
     316              :         return alloc->allocate(size);
     317              :     }
     318              : 
     319              :     /** Deallocate a coroutine frame.
     320              : 
     321              :         Reads the pointer stored at the end of the frame to find
     322              :         the allocator. The tag bit (low bit) indicates whether
     323              :         this is the first frame (with embedded wrapper) or a
     324              :         child frame (with pointer to external wrapper).
     325              : 
     326              :         A null pointer indicates the frame was allocated with
     327              :         global new/delete (no custom allocator was active).
     328              :     */
     329              :     static void
     330              :     operator delete(void* ptr, std::size_t size)
     331              :     {
     332              :         // Pointer is always at aligned_offset(size)
     333              :         std::size_t ptr_offset = aligned_offset(size);
     334              :         auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     335              :             static_cast<char*>(ptr) + ptr_offset);
     336              :         auto raw_ptr = reinterpret_cast<std::uintptr_t>(*ptr_loc);
     337              : 
     338              :         // Null pointer means global new/delete
     339              :         if(raw_ptr == 0)
     340              :         {
     341              :             std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*);
     342              :             ::operator delete(ptr, total);
     343              :             return;
     344              :         }
     345              : 
     346              :         // Tag bit distinguishes first frame (embedded) from child frames
     347              :         bool is_embedded = raw_ptr & 1;
     348              :         auto* wrapper = reinterpret_cast<detail::frame_allocator_base*>(
     349              :             raw_ptr & ~std::uintptr_t(1));
     350              : 
     351              :         if(is_embedded)
     352              :             wrapper->deallocate_embedded(ptr, size);
     353              :         else
     354              :             wrapper->deallocate(ptr, size);
     355              :     }
     356              : #endif
     357              : };
     358              : 
     359              : //----------------------------------------------------------
     360              : // embedding_frame_allocator implementation
     361              : // (must come after frame_allocating_base is defined)
     362              : //----------------------------------------------------------
     363              : 
     364              : namespace detail {
     365              : 
     366              : template<frame_allocator Allocator>
     367              : void*
     368            0 : embedding_frame_allocator<Allocator>::allocate(std::size_t n)
     369              : {
     370              :     // Layout: [frame | ptr | wrapper]
     371            0 :     std::size_t ptr_offset = aligned_offset(n);
     372            0 :     std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
     373            0 :     std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper<Allocator>);
     374              : 
     375            0 :     void* raw = alloc_.allocate(total);
     376              : 
     377              :     // Construct embedded wrapper after the pointer
     378            0 :     auto* wrapper_loc = static_cast<char*>(raw) + wrapper_offset;
     379            0 :     auto* embedded = new (wrapper_loc) frame_allocator_wrapper<Allocator>(alloc_);
     380              : 
     381              :     // Store tagged pointer at fixed offset (bit 0 set = embedded)
     382            0 :     auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
     383              :         static_cast<char*>(raw) + ptr_offset);
     384            0 :     *ptr_loc = reinterpret_cast<frame_allocator_base*>(
     385            0 :         reinterpret_cast<std::uintptr_t>(embedded) | 1);
     386              : 
     387              :     // Update TLS to embedded wrapper for subsequent allocations
     388            0 :     frame_allocating_base::set_frame_allocator(*embedded);
     389              : 
     390            0 :     return raw;
     391              : }
     392              : 
     393              : } // namespace detail
     394              : 
     395              : } // namespace capy
     396              : } // namespace boost
     397              : 
     398              : #endif
        

Generated by: LCOV version 2.3