GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/frame_allocator.hpp
Date: 2026-01-18 18:26:31
Exec Total Coverage
Lines: 22 27 81.5%
Functions: 5 6 83.3%
Branches: 2 4 50.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_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 set_frame_allocator(detail::frame_allocator_base& alloc) noexcept
194 {
195 current_allocator() = &alloc;
196 }
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
1/2
✓ Branch 0 taken 285 times.
✗ Branch 1 not taken.
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 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
1/2
✓ Branch 0 taken 285 times.
✗ Branch 1 not taken.
285 if(!wrapper)
273 {
274 285 ::operator delete(ptr);
275 285 return;
276 }
277
278 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
290