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
|