LCOV - code coverage report
Current view: top level - boost/capy/ex - frame_allocator.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 95.2 % 83 79
Test Date: 2026-01-15 23:24:39 Functions: 82.8 % 29 24

            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          122 :     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           61 :     aligned_offset(std::size_t n) noexcept
     120              :     {
     121           61 :         return (n + alignment - 1) & ~(alignment - 1);
     122              :     }
     123              : 
     124              : public:
     125           61 :     explicit embedding_frame_allocator(Allocator a)
     126           61 :         : alloc_(std::move(a))
     127              :     {
     128           61 :     }
     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              :     IMPORTANT: This wrapper stores a POINTER to the allocator in the
     153              :     launcher's embedder, not a copy. This is safe because Frame #1
     154              :     (where this wrapper lives) is always destroyed before Frame #2
     155              :     (the launcher, where embedder lives).
     156              : 
     157              :     @tparam Allocator The underlying allocator type satisfying frame_allocator.
     158              : */
     159              : template<frame_allocator Allocator>
     160              : class frame_allocator_wrapper : public frame_allocator_base
     161              : {
     162              :     Allocator* alloc_;  // Pointer, not copy
     163              : 
     164              :     static constexpr std::size_t alignment = alignof(void*);
     165              : 
     166              :     static std::size_t
     167          411 :     aligned_offset(std::size_t n) noexcept
     168              :     {
     169          411 :         return (n + alignment - 1) & ~(alignment - 1);
     170              :     }
     171              : 
     172              : public:
     173           61 :     explicit frame_allocator_wrapper(Allocator& a)
     174           61 :         : alloc_(&a)
     175              :     {
     176           61 :     }
     177              : 
     178              :     void*
     179          175 :     allocate(std::size_t n) override
     180              :     {
     181              :         // Layout: [frame | ptr]
     182          175 :         std::size_t ptr_offset = aligned_offset(n);
     183          175 :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     184              : 
     185          175 :         void* raw = alloc_->allocate(total);
     186              : 
     187              :         // Store untagged pointer to self at fixed offset
     188          175 :         auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
     189              :             static_cast<char*>(raw) + ptr_offset);
     190          175 :         *ptr_loc = this;
     191              : 
     192          175 :         return raw;
     193              :     }
     194              : 
     195              :     void
     196          175 :     deallocate(void* block, std::size_t user_size) override
     197              :     {
     198              :         // Child frame deallocation: layout is [frame | ptr]
     199          175 :         std::size_t ptr_offset = aligned_offset(user_size);
     200          175 :         std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
     201          175 :         alloc_->deallocate(block, total);
     202          175 :     }
     203              : 
     204              :     void
     205           61 :     deallocate_embedded(void* block, std::size_t user_size) override
     206              :     {
     207              :         // First frame deallocation: layout is [frame | ptr | wrapper]
     208           61 :         std::size_t ptr_offset = aligned_offset(user_size);
     209           61 :         std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
     210           61 :         std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper);
     211              : 
     212              :         // Safe to use alloc_ pointer because embedder (in Frame #2)
     213              :         // is guaranteed to outlive this wrapper (in Frame #1)
     214           61 :         Allocator* alloc_ptr = alloc_;  // Save pointer before destroying self
     215           61 :         this->~frame_allocator_wrapper();
     216           61 :         alloc_ptr->deallocate(block, total);
     217           61 :     }
     218              : };
     219              : 
     220              : } // namespace detail
     221              : 
     222              : /** Mixin base for promise types to support custom frame allocation.
     223              : 
     224              :     Derive your promise_type from this class to enable custom coroutine
     225              :     frame allocation via a thread-local allocator pointer.
     226              : 
     227              :     The allocation strategy:
     228              :     @li If a thread-local allocator is set, use it for allocation
     229              :     @li Otherwise, fall back to global `::operator new`/`::operator delete`
     230              : 
     231              :     A pointer is stored at the end of each allocation to enable correct
     232              :     deallocation regardless of which allocator was active at allocation time.
     233              : 
     234              :     @par Memory Layout
     235              : 
     236              :     For the first coroutine frame (allocated via embedding_frame_allocator):
     237              :     @code
     238              :     [coroutine frame | tagged_ptr | frame_allocator_wrapper]
     239              :     @endcode
     240              : 
     241              :     For subsequent frames (allocated via frame_allocator_wrapper):
     242              :     @code
     243              :     [coroutine frame | ptr]
     244              :     @endcode
     245              : 
     246              :     The tag bit (low bit) distinguishes the two cases during deallocation.
     247              : 
     248              :     @see frame_allocator
     249              : */
     250              : struct frame_allocating_base
     251              : {
     252              : private:
     253              :     static constexpr std::size_t alignment = alignof(void*);
     254              : 
     255              :     static std::size_t
     256          296 :     aligned_offset(std::size_t n) noexcept
     257              :     {
     258          296 :         return (n + alignment - 1) & ~(alignment - 1);
     259              :     }
     260              : 
     261              :     static detail::frame_allocator_base*&
     262          940 :     current_allocator() noexcept
     263              :     {
     264              :         static thread_local detail::frame_allocator_base* alloc = nullptr;
     265          940 :         return alloc;
     266              :     }
     267              : 
     268              : public:
     269              :     /** Set the thread-local frame allocator.
     270              : 
     271              :         The allocator will be used for subsequent coroutine frame
     272              :         allocations on this thread until changed or cleared.
     273              : 
     274              :         @param alloc The allocator to use. Must outlive all coroutines
     275              :                      allocated with it.
     276              :     */
     277              :     static void
     278          408 :     set_frame_allocator(detail::frame_allocator_base& alloc) noexcept
     279              :     {
     280          408 :         current_allocator() = &alloc;
     281          408 :     }
     282              : 
     283              :     /** Clear the thread-local frame allocator.
     284              : 
     285              :         Subsequent allocations will use global `::operator new`.
     286              :     */
     287              :     static void
     288           61 :     clear_frame_allocator() noexcept
     289              :     {
     290           61 :         current_allocator() = nullptr;
     291           61 :     }
     292              : 
     293              :     /** Get the current thread-local frame allocator.
     294              : 
     295              :         @return Pointer to current allocator, or nullptr if none set.
     296              :     */
     297              :     static detail::frame_allocator_base*
     298          205 :     get_frame_allocator() noexcept
     299              :     {
     300          205 :         return current_allocator();
     301              :     }
     302              : 
     303              :     static void*
     304          266 :     operator new(std::size_t size)
     305              :     {
     306          266 :         auto* alloc = current_allocator();
     307          266 :         if(!alloc)
     308              :         {
     309              :             // No allocator: allocate extra space for null pointer marker
     310           30 :             std::size_t ptr_offset = aligned_offset(size);
     311           30 :             std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*);
     312           30 :             void* raw = ::operator new(total);
     313              : 
     314              :             // Store nullptr to indicate global new/delete
     315           30 :             auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     316              :                 static_cast<char*>(raw) + ptr_offset);
     317           30 :             *ptr_loc = nullptr;
     318              : 
     319           30 :             return raw;
     320              :         }
     321          236 :         return alloc->allocate(size);
     322              :     }
     323              : 
     324              :     /** Deallocate a coroutine frame.
     325              : 
     326              :         Reads the pointer stored at the end of the frame to find
     327              :         the allocator. The tag bit (low bit) indicates whether
     328              :         this is the first frame (with embedded wrapper) or a
     329              :         child frame (with pointer to external wrapper).
     330              : 
     331              :         A null pointer indicates the frame was allocated with
     332              :         global new/delete (no custom allocator was active).
     333              :     */
     334              :     static void
     335          266 :     operator delete(void* ptr, std::size_t size)
     336              :     {
     337              :         // Pointer is always at aligned_offset(size)
     338          266 :         std::size_t ptr_offset = aligned_offset(size);
     339          266 :         auto* ptr_loc = reinterpret_cast<detail::frame_allocator_base**>(
     340              :             static_cast<char*>(ptr) + ptr_offset);
     341          266 :         auto raw_ptr = reinterpret_cast<std::uintptr_t>(*ptr_loc);
     342              : 
     343              :         // Null pointer means global new/delete
     344          266 :         if(raw_ptr == 0)
     345              :         {
     346              : #if __cpp_sized_deallocation >= 201309
     347           30 :             std::size_t total = ptr_offset + sizeof(detail::frame_allocator_base*);
     348           30 :             ::operator delete(ptr, total);
     349              : #else
     350              :             ::operator delete(ptr);
     351              : #endif
     352           30 :             return;
     353              :         }
     354              : 
     355              :         // Tag bit distinguishes first frame (embedded) from child frames
     356          236 :         bool is_embedded = raw_ptr & 1;
     357          236 :         auto* wrapper = reinterpret_cast<detail::frame_allocator_base*>(
     358          236 :             raw_ptr & ~std::uintptr_t(1));
     359              : 
     360          236 :         if(is_embedded)
     361           61 :             wrapper->deallocate_embedded(ptr, size);
     362              :         else
     363          175 :             wrapper->deallocate(ptr, size);
     364              :     }
     365              : };
     366              : 
     367              : //----------------------------------------------------------
     368              : // embedding_frame_allocator implementation
     369              : // (must come after frame_allocating_base is defined)
     370              : //----------------------------------------------------------
     371              : 
     372              : namespace detail {
     373              : 
     374              : template<frame_allocator Allocator>
     375              : void*
     376           61 : embedding_frame_allocator<Allocator>::allocate(std::size_t n)
     377              : {
     378              :     // Layout: [frame | ptr | wrapper]
     379           61 :     std::size_t ptr_offset = aligned_offset(n);
     380           61 :     std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
     381           61 :     std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper<Allocator>);
     382              : 
     383           61 :     void* raw = alloc_.allocate(total);
     384              : 
     385              :     // Construct embedded wrapper after the pointer
     386              :     // Pass REFERENCE to alloc_ so wrapper stores pointer, not copy
     387           61 :     auto* wrapper_loc = static_cast<char*>(raw) + wrapper_offset;
     388           61 :     auto* embedded = new (wrapper_loc) frame_allocator_wrapper<Allocator>(alloc_);
     389              : 
     390              :     // Store tagged pointer at fixed offset (bit 0 set = embedded)
     391           61 :     auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
     392              :         static_cast<char*>(raw) + ptr_offset);
     393           61 :     *ptr_loc = reinterpret_cast<frame_allocator_base*>(
     394           61 :         reinterpret_cast<std::uintptr_t>(embedded) | 1);
     395              : 
     396              :     // Update TLS to embedded wrapper for subsequent allocations
     397           61 :     frame_allocating_base::set_frame_allocator(*embedded);
     398              : 
     399           61 :     return raw;
     400              : }
     401              : 
     402              : } // namespace detail
     403              : 
     404              : } // namespace capy
     405              : } // namespace boost
     406              : 
     407              : #endif
        

Generated by: LCOV version 2.3