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 <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 0 : set_frame_allocator(detail::frame_allocator_base& alloc) noexcept
194 : {
195 0 : current_allocator() = &alloc;
196 0 : }
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 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 0 : 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 285 : if(!wrapper)
273 : {
274 285 : ::operator delete(ptr);
275 285 : return;
276 : }
277 :
278 0 : 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
|