Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
stl_fwd.hpp
Go to the documentation of this file.
1/* Flow-IPC: Shared Memory
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
21#include <boost/move/traits.hpp>
22
23/**
24 * ipc::shm sub-module providing integration between STL-compliant components (including containers)
25 * and SHared Memory (SHM) providers.
26 *
27 * @see The guided Manual chapter @ref transport_shm covers this topic in use.
28 *
29 * @note This sub-module/namespace is not limited to working with the jemalloc-based SHM
30 * provider supplied elsewhere in ipc::shm; it can and does work with any other SHM-provider
31 * capable of allocating an uninitialized buffer in SHM and later manually deallocating it by raw pointer.
32 * In fact, due to its simplicity of setup, the SHM-classic (a/k/a boost.interprocess) SHM-provider
33 * (in ipc::shm::classic) may be used in comments as the go-to example.
34 *
35 * ### Background ###
36 * What's this trying to do? Answer: It's quite a narrow purpose in fact, and it's important to separate it
37 * from orthogonal concerns to avoid confusion. So let's slowly explain step by step. Suppose we define as
38 * a bare-bones SHM-provider via an `Arena` concept, where `Arena` is a class -- instantiated in some unspecified
39 * way -- with at least these key methods:
40 *
41 * ~~~
42 * class Arena
43 * {
44 * public:
45 * // Allocate uninitialized buffer of n bytes in this SHM arena; return locally-dereferenceable pointer
46 * // to that buffer. Throw exception if ran out of resources. (Some providers try hard to avoid this by
47 * // internally mapping more SHM pools, a/k/a mmap()ped areas, as needed. However it is OK to throw:
48 * // e.g., an STL container might throw when trying to allocate something; such is life.) Do not return
49 * // null.
50 * void* allocate(size_t n);
51 *
52 * // Undo allocate() that returned `p`; or the equivalent operation if the SHM-provider allows
53 * // process 2 to deallocate something that was allocated by process 1 (and `p` indeed was allocated
54 * // in a different process but transmitted to the current process; and was properly made
55 * // locally-dereferenceable).
56 * void deallocate(void* p);
57 * }
58 * ~~~
59 *
60 * Suppose you have a plain-old-datatype (POD) type `T`; like an `int` or a `struct` with a bunch of scalars
61 * or arrays of scalars or various combos like that. To create a `T` in SHM, one could just call
62 * `Arena::allocate(sizeof(T))` and load it up with values based off the returned pointer, having
63 * cast it to `T*`. (Or one could use placement-construction but never mind.) To destroy it, one
64 * would `Arena::deallocate()` passing-in the returned pointer from `allocate()`. To transmit to
65 * another process, one would need to somehow send over a representation of `T* p`, then make it
66 * locally-dereferenceable, if the SHM-mapped vaddrs are not synced between the 2 processes.
67 *
68 * Everything in the preceding paragraph is 100% orthogonal to ipc::shm::stl. That is not our problem.
69 * Now suppose `T` is not a POD but a bit more complex. Let's say it's `Vector<E>`, similar to `vector<E>`,
70 * with the usual semantics. Its internal representation would be very similar to:
71 *
72 * ~~~
73 * template<typename E> class Vector
74 * {
75 * private:
76 * // Currently allocated buffer starts at m_buf, is `m_buf_sz * sizeof(E)` long; and the used
77 * // range (within [0, size())) starts at m_buf also but is only m_elem_ct (<= m_buf_sz) `E`s long.
78 * E* m_buf; size_t m_buf_sz; size_t m_elem_ct;
79 * }
80 * using T = Vector<int>;
81 * ~~~
82 *
83 * Allocating a `T` itself works the same as before; but that would only allocate the outer layer -- `sizeof(T)`
84 * -- with those 3 data members themselves. But what happens when the `Vector` allocates into `m_buf`?
85 * A naive impl would just use `new`. But that wouldn't work if one tried to access the `T` in another process,
86 * even having acquired a locally-dereferenceable pointer to it (which is outside our scope completely; but
87 * doable as we established earlier): `m_buf` is never locally-dereferenceable in any process but the original
88 * one. One would have to somehow translate it and change `m_buf` to make it locally-dereferenceable; but then
89 * it would become wrong in the original process: remember that the idea is to place the `T` *itself* into SHM
90 * in the first place. One could write internal `Vector` code that would do the translation -- essentially be
91 * specifically SHM-aware -- but that's terribly onerous a requirement for a container.
92 *
93 * The good news is the STL containers, at least per standard and at least the `boost::container` impls of them,
94 * use a technique called *allocators* that resolves this problem (among others). So now consider `vector<E>`,
95 * namely an STL-standard-compliant implementation like `boost::container::vector` (and possibly your built-in
96 * `std::vector`, though that may or may not be fully STL-standard-compliant ironically). What it does
97 * essentially is:
98 *
99 * ~~~
100 * template<typename E, typename Allocator = std::allocator<E>> class Vector
101 * {
102 * private:
103 * Allocator m_alloc; // Usually initialized via default-ct `Allocator()` at ctor time.
104 *
105 * // Currently allocated buffer starts at m_buf, is `m_buf_sz * sizeof(E)` long; and the used
106 * // range (within [0, size())) starts at m_buf also but is only m_elem_ct (<= m_buf_sz) `E`s long.
107 * Allocator::pointer m_buf; size_t m_buf_sz; size_t m_elem_ct;
108 * }
109 * using T = Vector<int>;
110 * ~~~
111 *
112 * Firstly note the type of `m_buf`: it uses not a raw pointer but an allocator-type-driven type which must
113 * have certain pointer-like semantics. In `std::allocator`, it *is* simply the raw pointer `E*` after all;
114 * but for SHM we need to provide something else. Secondly when it needs to allocate `m_buf`, it no longer
115 * does `new`. Instead it does basically `m_buf = m_alloc.allocate(m_buf_sz)`. Conversely when deleting
116 * instead of `delete` it does `m_alloc.deallocate(m_buf, m_buf_sz)`.
117 *
118 * So via the allocator's (1) alloc/dealloc methods and (2) its mandated pointer type, the allocation strategy *and*
119 * pointer storage can be parameterized. As for `m_alloc` itself, it is often (usually) an empty object
120 * (`is_empty_v<Allocator> == true`; with the empty-base optimization it takes no space in the container); that's
121 * a *stateless* allocator; and it is always default-cted. It can also be stateful (in which case it must be
122 * explicitly constructed). In real `vector` you'll see support for both.
123 *
124 * How does this help our SHM use case? Firstly, of course, `allocate()` and `deallocate()` can be written to
125 * allocate/deallocate via `Arena::[de]allocate()`. Secondly, the `pointer` type can be something
126 * that, when stored in SHM, contains bits sufficient for its own methods, such as the dereference operator,
127 * compute the locally-dereferenceable location `void*` just from those bits, regardless of which process it's in.
128 * So in the case of SHM-classic, for example, `pointer` would be internally `bipc::offset_ptr<E>`, which uses a
129 * clever technique, namely storing inside the `offset_ptr` the offset compared to its own `this`. Remember
130 * this would be inside the same SHM pool, which is how the `classic` Arena works (it operates within one SHM pool).
131 *
132 * Now: the *outside* allocation of `T` (`Vector<E>`) itself is done directly by the SHM `Arena` user.
133 * Once the STL-compliant container is involved, it does it indirectly via its `Allocator`. The same holds of
134 * deallocation: *outside* deallocation would invoke the `T::~T()` dtor first, which would then deallocate
135 * via `Allocator::deallocate()`; and then `Arena::deallocate()` the raw buffer (`sizeof(T)` long).
136 *
137 * If `T` needs to allocate more objects that do yet more allocation on its behalf, then it would remember to
138 * propagate the `Allocator` to those, indefinitely. These *inside* allocations/deallocations/dereferencing
139 * all happen via `Allocator`. One must only worry about invoking the ctor or dtor of `T` within the
140 * process that is indeed allowed to allocate/deallocate. (So if `Arena` does support deallocation in
141 * not-the-original-allocating process, then the dtor could be called in any process working with the *outside* `T`.
142 * If not, then not.)
143 *
144 * ### The task ###
145 * So that's the background. The main product of this namespace ipc::shm::stl is SHM-aware allocator types that
146 * can be used as template params to STL-compliant containers (and other types at times) in order to be able
147 * to allocate nested containers-of-containers...-of-PODs-of-... directly in SHM in such a way as to be accessible
148 * in multiple processes, as long as the *outside* `T*` is properly transmitted from process to process by the user.
149 * Only the *outside* SHM handle to the container-of-... is something the user worries about; the rest "just works,"
150 * as long as all containers involved are properly parameterized to use the SHM-aware allocator types we provide.
151 *
152 * The main product, then, is Stateless_allocator. See its doc header. The short version for your convenience:
153 * - Stateless_allocator is itself parameterized on `Arena`, which must be a SHM-allocating type like the one used
154 * above. `Arena` must(^) supply: `allocate()`, `deallocate()`, and `Pointer`. The `Pointer` must
155 * be a *fancy pointer* type that can produce a locally-dereferenceable `void*` and has data member(s)
156 * that contain bits that are process-agnostic (such as an offset, or pool ID and offset, and so on) when
157 * stored in SHM.
158 * - In particular, classic::Pool_arena complies with these requirements. Its allocate/deallocate work within 1
159 * SHM pool per Arena. Its `Pointer` is internally `bipc::offset_ptr`.
160 * - (^) There is, for SHM-arena-lending providers (e.g.: SHM-jemalloc, centered on
161 * arena_lend::jemalloc::Ipc_arena), a key case where `Arena` must merely supply `Pointer`, not allocate() or
162 * deallocate(). See "Use in read-only borrowing mode" in Stateless_allocator doc header.
163 * - Stateless_allocator is stateless. It is always default-cted, so all the user must do is remember to
164 * provide Stateless_allocator as the allocator template param(s) to the container type(s) involved.
165 * Therefore it must know on which `Arena` it shall operate. This is controlled on a thread-local basis
166 * via RAII-style helper `Arena_activator`. (So the user must use `Arena_activator ctx{Arena*}` to
167 * activate the "current" `Arena` for the purposes of Stateless_allocator use, before any work with
168 * the STL-compliant container types involved in a given SHM-stored data structure.)
169 *
170 * As of this writing we just provide Stateless_allocator. `Stateful_allocator` may also be provided depending
171 * on need. It would not require the use of Arena_activator by the user; but then various difficulties inherent
172 * to working with stateful allocators come into force. (Just the fact extra bits per container instance are necessary
173 * to refer to the appropriate allocator object, which knows which `Arena` to operate-upon = not super-great.)
174 */
175namespace ipc::shm::stl
176{
177
178// Types.
179
180// Find doc headers near the bodies of these compound types.
181
182template<typename T, typename Arena_t>
183class Stateless_allocator;
184
185template<typename Arena_t>
186class Arena_activator;
187
188// Free functions.
189
190/**
191 * Returns `true` for any 2 `Stateless_allocator`s managing the same Stateless_allocator::Arena.
192 * This satisfies formal requirements of STL-compliant `Allocator` concept. See cppreference.com for those formal
193 * requirements. Since it's a stateless allocator, this always returns `true`.
194 *
195 * @relatesalso Stateless_allocator
196 *
197 * @tparam Arena_t
198 * See Stateless_allocator.
199 * @tparam T1
200 * See Stateless_allocator.
201 * @tparam T2
202 * See Stateless_allocator.
203 * @param val1
204 * An allocator.
205 * @param val2
206 * An allocator.
207 * @return `true`.
208 */
209template<typename Arena_t, typename T1, typename T2>
210bool operator==(const Stateless_allocator<T1, Arena_t>& val1, const Stateless_allocator<T2, Arena_t>& val2);
211
212/**
213 * Returns `false` for any 2 `Stateless_allocator`s managing the same Stateless_allocator::Arena.
214 * This satisfies formal requirements of STL-compliant `Allocator` concept. See cppreference.com for those formal
215 * requirements. Since it's a stateless allocator, this always returns `false`.
216 *
217 * @relatesalso Stateless_allocator
218 *
219 * @tparam Arena_t
220 * See Stateless_allocator.
221 * @tparam T1
222 * See Stateless_allocator.
223 * @tparam T2
224 * See Stateless_allocator.
225 * @param val1
226 * An allocator.
227 * @param val2
228 * An allocator.
229 * @return `false`.
230 */
231template<typename Arena_t, typename T1, typename T2>
232bool operator!=(const Stateless_allocator<T1, Arena_t>& val1, const Stateless_allocator<T2, Arena_t>& val2);
233
234} // namespace ipc::shm::stl
235
236namespace boost
237{
238
239// Types.
240
241/**
242 * Specializes this boost.move trait for every ipc::shm::stl::Stateless_allocator, so as to disable the optimization
243 * it controls -- in turn working around a boost.container bug in the Boost versions we support (as of this writing).
244 *
245 * Background: `has_trivial_destructor_after_move<X>::value == true` tells boost.container that a moved-from `X`
246 * need not be destroyed. E.g., boost.container `vector<X>::erase()` (or for `flat_map<K, X>`) then skips destroying
247 * the moved-from elements. For `X` = boost.container `basic_string`, `deque`, and `stable_vector` that trait is
248 * computed as `true` if it is `true` for the `X`'s allocator type (which by default it is for any trivially
249 * destructible allocator type, including ours); yet `X`'s move-assignment can leave the moved-from source owning
250 * memory. So that memory leaks. The bug is fixed in boost.container `develop` branch but not
251 * in any release through Boost-1.92 (past that version: to be seen as of this writing). With this specialization
252 * those `X` traits are `false` for `X`s using `Stateless_allocator`; so the moved-from elements are destroyed,
253 * and nothing leaks.
254 *
255 * @note This is a legal technique; we are imposing this on `Stateless_allocator` only. It's arguably a hack, but
256 * it is a legal hack.
257 *
258 * @see ticket we filed: https://github.com/boostorg/container/issues/351. It applies at least through Boost-1.92.
259 *
260 * The cost of this: An extra (no-op) destructor call per moved-from element *in such cases*. It affects only
261 * containers whose elements are themselves containers using Stateless_allocator; for example
262 * `vector<int, Stateless_allocator<...>>` is not affected.
263 *
264 * @todo Once our minimum supported Boost (per our build script(s)) version includes the fix for the bug
265 * worked-around by `boost::has_trivial_destructor_after_move<ipc::shm::stl::Stateless_allocator>` specialization:
266 * remove that specialization. As of Boost-1.92 the bug is there.
267 *
268 * @tparam T
269 * See ipc::shm::stl::Stateless_allocator.
270 * @tparam Arena_t
271 * See ipc::shm::stl::Stateless_allocator.
272 */
273template<typename T, typename Arena_t>
274struct has_trivial_destructor_after_move<::ipc::shm::stl::Stateless_allocator<T, Arena_t>>
275{
276 // Constants.
277
278 /// Means: do not skip destroying moved-from objects whose type uses this allocator type. See above.
279 static const bool value = false;
280};
281
282} // namespace boost
Stateless allocator usable with STL-compliant containers to store (or merely read) them directly in S...
ipc::shm sub-module providing integration between STL-compliant components (including containers) and...
bool operator==(const Stateless_allocator< T1, Arena_t > &, const Stateless_allocator< T2, Arena_t > &)
Returns true for any 2 Stateless_allocators managing the same Stateless_allocator::Arena.
bool operator!=(const Stateless_allocator< T1, Arena_t > &, const Stateless_allocator< T2, Arena_t > &)
Returns false for any 2 Stateless_allocators managing the same Stateless_allocator::Arena.