Flow-IPC 1.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/**
22 * ipc::shm sub-module providing integration between STL-compliant components (including containers)
23 * and SHared Memory (SHM) providers.
24 *
25 * @note This sub-module/namespace is not limited to working with the jemalloc-based SHM
26 * provider supplied elsewhere in ipc::shm; it can and does work with any other SHM provider
27 * capable allocating an uninitialized buffer in SHM and later manually deallocating it by raw pointer.
28 * In fact, due to its simplicity of setup, the SHM-classic (a/k/a boost.interprocess) SHM provider
29 * (in ipc::shm::classic) may be used in comments as the go-to example.
30 *
31 * ### Background ###
32 * What's this trying to do? Answer: It's quite a narrow purpose in fact, and it's important to separate it
33 * from orthogonal concerns to avoid confusion. So let's slowly explain step by step. Suppose we define as
34 * a bare-bones SHM provider via an `Arena` concept, where `Arena` is a class -- instantiated in some unspecified
35 * way -- with at least these key methods:
36 *
37 * ~~~
38 * class Arena
39 * {
40 * public:
41 * // Allocate uninitialized buffer of n bytes in this SHM arena; return locally-dereferenceable pointer
42 * // to that buffer. Throw exception if ran out of resources. (Some providers try hard to avoid this by
43 * // internally mapping more SHM pools, a/k/a mmap()ped areas, as needed. However it is OK to throw:
44 * // e.g., an STL container might throw when trying to allocate something; such is life.) Do not return
45 * // null.
46 * void* allocate(size_t n);
47 *
48 * // Undo allocate() that returned `p`; or the equivalent operation if the SHM provider allows
49 * // process 2 to deallocate something that was allocated by process 1 (and `p` indeed was allocated
50 * // in a different process but transmitted to the current process; and was properly made
51 * // locally-dereferenceable).
52 * void deallocate(void* p);
53 * }
54 * ~~~
55 *
56 * Suppose you have a plain-old-datatype (POD) type `T`; like an `int` or a `struct` with a bunch of scalars
57 * or arrays of scalars or various combos like that. To create an `T` in SHM, one could just call
58 * `Arena::allocate(sizeof(S))` and load it up with values based off the returned pointer, having
59 * cast it to `T*`. (Or one could use placement-construction but never mind.) To destroy it, one
60 * would `Arena::deallocate()` passing-in the returned pointer from `allocate()`. To transmit to
61 * another process, one would need to somehow send over a representation of `T* p`, then make it
62 * locally-dereferenceable, if the SHM-mapped vaddrs are not synced between the 2 processes.
63 *
64 * Everything in the preceding paragraph is 100% orthogonal to ipc::shm::stl. That is not our problem.
65 * Now suppose `T` is not a POD but a bit more complex. Let's say it's `Vector<E>`, similar to `vector<E>`,
66 * with the usual semantics. Its internal representation would be very similar to:
67 *
68 * ~~~
69 * template<typename E> class Vector
70 * {
71 * private:
72 * // Currently allocated buffer starts at m_buf, is `m_buf_sz * sizeof(E)` long; and the used
73 * // range (within [0, size()) starts at m_buf also but is only m_elem_ct (<= m_buf_sz) `E`s long.
74 * E* m_buf; size_t m_buf_sz; size_t m_elem_ct;
75 * }
76 * using T = Vector<int>;
77 * ~~~
78 *
79 * Allocating a `T` itself works the same as before; but that would only allocate the outer layer -- `sizeof(T)`
80 * -- with those 3 data members themselves. But what happens when the `Vector` allocates into `m_buf`?
81 * A naive impl would just use `new`. But that wouldn't work if one tried to access the `T` in another process,
82 * even having acquired a locally-dereferenceable pointer to it (which is outside our scope completely; but
83 * doable as we established earlier): `m_buf` is never locally-dereferenceable in any process but the original
84 * one. One would have to somehow translate it and change `m_buf` to make it locally-dereferenceable; but then
85 * it would become wrong in the original process: remember that the idea is to place the `T` *itself* into SHM
86 * in the first place. One could write internal `Vector` code that would do the translation -- essentially be
87 * SHM-aware -- but that's terribly onerous a requirement for a container.
88 *
89 * The good news is the STL containers, at least per standard and at least the `boost::container` impls of them,
90 * use a technique called *allocators* that resolves this problem (among others). So now consider `vector<E>`,
91 * namely an STL-standard-compliant implementation like `boost::container::vector` (and possibly your built-in
92 * `std::vector`, though that may or may not be fully STL-standard-compliant ironically). What it does
93 * essentially is:
94 *
95 * ~~~
96 * template<typename E, typename Allocator = std::allocator<E>> class Vector
97 * {
98 * private:
99 * Allocator m_alloc; // Usually initialized via default-ct `Allocator()` at ctor time.
100 *
101 * // Currently allocated buffer starts at m_buf, is `m_buf_sz * sizeof(E)` long; and the used
102 * // range (within [0, size()) starts at m_buf also but is only m_elem_ct (<= m_buf_sz) `E`s long.
103 * Allocator::pointer m_buf; size_t m_buf_sz; size_t m_elem_ct;
104 * }
105 * using T = Vector<int>;
106 * ~~~
107 *
108 * Firstly note the type of `m_buf`: it uses not a raw pointer but an allocator-type-driven type which must
109 * have certain pointer-like semantics. In `std::allocator`, it *is* simply the raw pointer `E*` after all;
110 * but for SHM we need to provide something else. Secondly, when it needs to allocate `m_buf`, it no longer
111 * does `new`. Instead it does basically `m_buf = m_alloc.allocate(sizeof(E) * m_buf_sz)`. And when deleting
112 * instead of `delete` it does `m_alloc.deallocate(p)`.
113 *
114 * So via the allocator's (1) alloc/dealloc methods and (2) its mandated pointer type, the allocation strategy
115 * *and* pointer storage can be parameterized. As for `m_alloc` itself, it is often (usually) an empty object
116 * (`sizeof(Allocator) == 0`); that's a *stateless* allocator; and it is always default-cted. It can also be
117 * stateful (in which case it must be explicitly constructed). In real `vector` you'll see support for both.
118 *
119 * How does this help our SHM use case? Firstly, of course, `allocate()` and `deallocate()` can be written to
120 * allocate/deallocate via `Arena::[de]allocate()`. Secondly, the `pointer` type can be something
121 * that, when stored in SHM, contains bits sufficient for its own methods, such as the dereference operator,
122 * compute the locally-dereferenceable location `void*` just from those bits, regardless of which process it's in.
123 * So in the case of SHM-classic, for example, `pointer` would be internally `bipc::offset_ptr<E>`, which uses a
124 * clever technique, namely storing inside the `offset_ptr` the offset compared to its own `this`. Remember
125 * this would be inside the same SHM pool, which is how the `classic` Arena works (it operates within one SHM pool).
126 *
127 * Now: the *outside* allocation of `T` (`Vector<E>`) itself is done directly by the SHM `Arena` user.
128 * Once the STL-compliant container is involved, it does it indirectly via its `Allocator`. The same holds of
129 * deallocation: *outside* deallocation would invoke the `T::~T()` dtor first, which would then deallocate
130 * via `Allocator::deallocate()`; and then `Arena::deallocate()` the raw buffer (`sizeof(T)` long).
131 *
132 * If `T` needs to allocate more objects that do yet more allocation on its behalf, then it would remember to
133 * propagate the `Allocator` to those, indefinitely. These *inside* allocations/deallocations/dereferencing
134 * all happen via `Allocator`. One must only only worry about invoking the ctor or dtor of `T` within the
135 * process that is indeed allowed to allocate/deallocate. (So if `Arena` does support deallocation in
136 * not-the-original-allocating process, then the dtor could be called in any process working with the *outside* `T`.
137 * If not, then not.)
138 *
139 * ### The task ###
140 * So that's the background. The main product of this namespace ipc::shm::stl is SHM-aware allocator types that
141 * can be used as template params to STL-compliant containers (and other types at times) in order to be able
142 * to allocate nested containers-of-containers...-of-PODs directly in SHM in such a way as to be accessible
143 * in multiple processes, as long as the *outside* `T*` is properly trasmitted from process to process by the user.
144 * Only the *outside* SHM handle to the container-of-... is something the user worries about; the rest "just works,"
145 * as long as all containers involved are properly parameterized to use the SHM-aware allocator types we provide.
146 *
147 * The main product, then, is Stateless_allocator. See its doc header. The short version for your convenience:
148 * - Stateless_allocator is itself parameterized on `Arena`, which must be a SHM-allocating type like the one used
149 * above. `Arena` must supply: `allocate()`, `deallocate()`, and `Pointer`. The `Pointer` must
150 * be a *fancy pointer* type that can produce a locally-dereferenceable `void*` and has data member(s)
151 * that contain bits that are process-agnostic (such as an offset, or pool ID and offset, and so on) when
152 * stored in SHM.
153 * - In particular, classic::Pool_arena complies with these requirements. Its allocate/deallocate work within 1
154 * SHM pool per Arena. Its `Pointer` is internally `bipc::offset_ptr`.
155 * - Stateless_allocator is stateless. It is always default-cted, so all the user must do is remember to
156 * provide Stateless_allocator as the allocator template param(s) to the container type(s) involved.
157 * Therefore it must know which `Arena` it shall operate on. This is controlled on a thread-local basis
158 * via RAII-style helper `Arena_activator`. (So the user must use `Arena_activator ctx(Arena*)` to
159 * activate the "current" Arena for the purposes of Stateless_allocator use, before any work with
160 * the STL-compliant container types involved in a given SHM-stored data structure.)
161 *
162 * As of this writing we just provide Stateless_allocator. `Stateful_allocator` may also be provided depending
163 * on need. It would not require the use of Arena_activator by the user; but then various difficulties inherent
164 * to working with stateful allocators come into force. (Just the fact extra bits per container instance are necessary
165 * to refer to the appropriate allocator object, which knows which `Arena` to operate-upon = not super-great.)
166 */
167namespace ipc::shm::stl
168{
169
170// Types.
171
172// Find doc headers near the bodies of these compound types.
173
174template<typename T, typename Arena>
175class Stateless_allocator;
176
177template<typename Arena>
178class Arena_activator;
179
180// Free functions.
181
182/**
183 * Returns `true` for any 2 `Stateless_allocator`s managing the same Stateless_allocator::Arena_obj.
184 * This satisfies formal requirements of STL-compliant `Allocator` concept. See cppreference.com for those formal
185 * requirements. Since it's a stateless allocator, this always returns `true`.
186 *
187 * @relatesalso Stateless_allocator
188 *
189 * @tparam Arena
190 * See Stateless_allocator.
191 * @tparam T1
192 * See Stateless_allocator.
193 * @tparam T2
194 * See Stateless_allocator.
195 * @param val1
196 * An allocator.
197 * @param val2
198 * An allocator.
199 * @return See above.
200 */
201template<typename Arena, typename T1, typename T2>
202bool operator==(const Stateless_allocator<T1, Arena>& val1, const Stateless_allocator<T2, Arena>& val2);
203
204/**
205 * Returns `false` for any 2 `Stateless_allocator`s managing the same Stateless_allocator::Arena_obj.
206 * This satisfies formal requirements of STL-compliant `Allocator` concept. See cppreference.com for those formal
207 * requirements. Since it's a stateless allocator, this always returns `false`.
208 *
209 * @relatesalso Stateless_allocator
210 *
211 * @tparam Arena
212 * See Stateless_allocator.
213 * @tparam T1
214 * See Stateless_allocator.
215 * @tparam T2
216 * See Stateless_allocator.
217 * @param val1
218 * An allocator.
219 * @param val2
220 * An allocator.
221 * @return See above.
222 */
223template<typename Arena, typename T1, typename T2>
224bool operator!=(const Stateless_allocator<T1, Arena>& val1, const Stateless_allocator<T2, Arena>& val2);
225
226} // namespace ipc::shm::stl
ipc::shm sub-module providing integration between STL-compliant components (including containers) and...
bool operator==(const Stateless_allocator< T1, Arena > &, const Stateless_allocator< T2, Arena > &)
Returns true for any 2 Stateless_allocators managing the same Stateless_allocator::Arena_obj.
bool operator!=(const Stateless_allocator< T1, Arena > &, const Stateless_allocator< T2, Arena > &)
Returns false for any 2 Stateless_allocators managing the same Stateless_allocator::Arena_obj.