GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/frame_allocator.hpp
Date: 2026-01-15 20:40:20
Exec Total Coverage
Lines: 14 56 25.0%
Functions: 6 15 40.0%
Branches: 0 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 <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 88 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 aligned_offset(std::size_t n) noexcept
120 {
121 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 deallocate(void*, std::size_t) override
135 {
136 // Never called - stack wrapper not used for deallocation
137 }
138
139 void
140 deallocate_embedded(void*, std::size_t) override
141 {
142 // Never called
143 }
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 aligned_offset(std::size_t n) noexcept
163 {
164 return (n + alignment - 1) & ~(alignment - 1);
165 }
166
167 public:
168 explicit frame_allocator_wrapper(Allocator a)
169 : alloc_(std::move(a))
170 {
171 }
172
173 void*
174 allocate(std::size_t n) override
175 {
176 // Layout: [frame | ptr]
177 std::size_t ptr_offset = aligned_offset(n);
178 std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
179
180 void* raw = alloc_.allocate(total);
181
182 // Store untagged pointer to self at fixed offset
183 auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
184 static_cast<char*>(raw) + ptr_offset);
185 *ptr_loc = this;
186
187 return raw;
188 }
189
190 void
191 deallocate(void* block, std::size_t user_size) override
192 {
193 // Child frame deallocation: layout is [frame | ptr]
194 std::size_t ptr_offset = aligned_offset(user_size);
195 std::size_t total = ptr_offset + sizeof(frame_allocator_base*);
196 alloc_.deallocate(block, total);
197 }
198
199 void
200 deallocate_embedded(void* block, std::size_t user_size) override
201 {
202 // First frame deallocation: layout is [frame | ptr | wrapper]
203 std::size_t ptr_offset = aligned_offset(user_size);
204 std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
205 std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper);
206
207 Allocator alloc_copy = alloc_; // Copy before destroying self
208 this->~frame_allocator_wrapper();
209 alloc_copy.deallocate(block, total);
210 }
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 embedding_frame_allocator<Allocator>::allocate(std::size_t n)
369 {
370 // Layout: [frame | ptr | wrapper]
371 std::size_t ptr_offset = aligned_offset(n);
372 std::size_t wrapper_offset = ptr_offset + sizeof(frame_allocator_base*);
373 std::size_t total = wrapper_offset + sizeof(frame_allocator_wrapper<Allocator>);
374
375 void* raw = alloc_.allocate(total);
376
377 // Construct embedded wrapper after the pointer
378 auto* wrapper_loc = static_cast<char*>(raw) + wrapper_offset;
379 auto* embedded = new (wrapper_loc) frame_allocator_wrapper<Allocator>(alloc_);
380
381 // Store tagged pointer at fixed offset (bit 0 set = embedded)
382 auto* ptr_loc = reinterpret_cast<frame_allocator_base**>(
383 static_cast<char*>(raw) + ptr_offset);
384 *ptr_loc = reinterpret_cast<frame_allocator_base*>(
385 reinterpret_cast<std::uintptr_t>(embedded) | 1);
386
387 // Update TLS to embedded wrapper for subsequent allocations
388 frame_allocating_base::set_frame_allocator(*embedded);
389
390 return raw;
391 }
392
393 } // namespace detail
394
395 } // namespace capy
396 } // namespace boost
397
398 #endif
399