Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
native_handle_transport.hpp
Go to the documentation of this file.
1/* Flow-IPC: Core
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
20// Not compiled: for documentation only. Contains concept docs as of this writing.
21#ifndef IPC_DOXYGEN_ONLY
22static_assert(false, "As of this writing this is a documentation-only \"header\" "
23 "(the \"source\" is for humans and Doxygen only).");
24#else // ifdef IPC_DOXYGEN_ONLY
25
26namespace ipc::transport
27{
28
29// Types.
30
31/**
32 * A documentation-only *concept* defining the behavior of an object capable of reliably/in-order *sending* of
33 * discrete messages, each containing a native handle, a binary blob, or both. This is paired with
34 * the Native_handle_receiver concept which defines reception of such messages.
35 *
36 * ### Concept contents ###
37 * The concept defines the following behaviors/requirements.
38 * - The object has at least 2 states:
39 * - NULL: Object is not a peer: is not connected/capable of transmission. Essentially it is not a useful
40 * state. A default-cted object is in NULL state; and a moved-from object is as-if default-cted, therefore
41 * also in a NULL state. In this state all the transmission methods return `false` and no-op.
42 * - PEER: Object is or has been a peer (connected/capable of transmission). It is not possible to exit PEER
43 * state, except via being moved-from. A pipe-hosing error retains PEER state for instance.
44 * - Other states are allowed but outside the scope of the concept. For example a CONNECTING state may or may
45 * not be relevant (but again not relevant to the concept).
46 * - The (outgoing) transmission-of-messages methods, including transmission of graceful-close message.
47 * See their doc headers.
48 * - Behavior when the destructor is invoked. See ~Native_handle_sender()
49 * doc header.
50 * - Default ctor (which creates a NULL-state object that can be moved-to and to which "as-if"
51 * state any moved-from object is changed).
52 * - `sync_io`-core adopting ctor (which creates PEER-state object from an idle, as-if-just cted PEER-state
53 * sync_io::Native_handle_sender).
54 * - Move ctor, move assignment operator; these do the reasonable thing including setting the moved-from object
55 * to NULL state. Various satellite APIs (e.g., Native_socket_stream_acceptor) shall
56 * need these. That is such APIs do not rely on the factory/shared-ownership pattern.
57 *
58 * The concept (intentionally) does *not* define the following behaviors:
59 * - How to create a Native_handle_sender, except the default, `sync_io`-core-adopting, and move ctors.
60 * That is it does not define how to create a *new* (not moved-from) and *functioning* (PEER state: transmitting,
61 * connected) peer object, except the latter from an existing `sync_io`-pattern core. It only defines behavior
62 * once the user has access to a Native_handle_sender that is already in PEER state: connected to a opposing
63 * peer Native_handle_receiver.
64 *
65 * @see Native_socket_stream: as of this writing one key class that implements this concept (and also
66 * Native_handle_receiver) -- using the Unix domain socket transport.
67 * @see Channel, a pipe-composing class template that potentially implements this concept.
68 * @see Blob_sender: a degenerate version of the present concept: capable of transmitting only blobs, not
69 * `Native_handle`s.
70 *
71 * @todo In C++20, if/when we upgrade to that, Native_handle_sender (and other such doc-only classes) can become an
72 * actual concept formally implemented by class(es) that, today, implement it via the "honor system."
73 * Currently it is a class `#ifdef`-ed out from actual compilation but still participating in doc generation.
74 * Note that Doxygen (current version as of this writing: 1.9.3) claims to support doc generation from formal
75 * C++20 concepts.
76 *
77 * ### Rationale: Why is send_native_handle() not asynchronous? ###
78 * send_native_handle(), as noted in its doc header (and similarly Blob_sender::send_blob()) has 2 key properties:
79 * - It will *never* return would-block despite *always* being non-blocking.
80 * - It takes no completion handler; and it returns pipe-hosing errors synchronously via an out-arg or
81 * exception.
82 * - async_end_sending() does take a completion handler.
83 *
84 * That is, it is "somehow" both non-blocking/synchronous -- yet requires no would-block contingency on the user's
85 * part. Magical! How can this possibly work? After all we know that all known low-level transports will yield
86 * would-block eventually (as of this writing -- Unix domain sockets, POSIX MQs, bipc SHM-based MQs), if the
87 * opposing side is not popping data off the pipe. Yet, as we look at the opposing Native_handle_receiver concept
88 * we see that it makes no requirement on its impls to pop everything off the low-level transport in-pipe as soon
89 * as it arrives. (In actual fact all existing impls in Flow-IPC refuse to do any such thing.)
90 *
91 * Naturally the only answer as to how it can work is: Any given Native_handle_sender (and native Blob_sender) impl
92 * shall be ready to encounter would-block internally; if this occurs it will need to make a copy of the offending
93 * message (the meta-blob part specifically included -- that's the part that involves significant copying),
94 * enqueue it internally -- and keep enqueuing any further send_native_handle()d messages until the would-block
95 * clears, and the queue is eventually emptied.
96 *
97 * Fair enough: But isn't this a performance hazard? Answer: yes but only in a necessary way. We reached this
98 * decision after considering the entire system end-to-end:
99 * -# user prepares message blob B and tells `Native_handle_sender` S to send B;
100 * -# S tells low-level transport (possibly kernel) to accept B;
101 * -# `Native_handle_receiver` R pops B from low-level transport;
102 * -# user tells R it wants to accept B, and R does give the latter to the user.
103 *
104 * It was a no-go to have Flow-IPC be in charge of allocating either-end buffer for B on the user's behalf:
105 * the user may want to use their own allocator, or the stack, or ??? -- possibly with zero-copy and prefix/postfix
106 * data nearby. The user buffer has to be ready, and S and R need to work with the memory areas provided by the
107 * user.
108 *
109 * Given all that, *one* or more of the following "people" has to be ready to keep B, or a copy of B, around, until
110 * the whole thing (1-4) has gone through: the sending user (1 above), S (2 above), or R (3 above). (If
111 * the receiving user (4 above) is always receiving things ASAP, without that process being processor-pegged or
112 * something, then it won't be a problem: no copying is necessary, except in step 2 when B is copied into
113 * the low-level process. Indeed that should be the case usually; we are discussing what to do *if* something goes
114 * wrong, or the receivier application is mis-coded, or who knows -- the receiver just isn't quick enough.)
115 *
116 * The answer, as already noted: *One* or more of 1, 2, 3 has to make a copy of B long enough until 4 has received it.
117 * At that point, in terms of overall performance, the question is *which* one of 1, 2, 3 it should be.
118 * Upon deliberating a few options occurred, but it seemed clear enough that 1 (original user) should not be the one,
119 * if it can be helped. IPC is not networking; as an end user I expect sending an out-message to work synchronously.
120 * Making me worry about a completion handler complicates the outgoing-direction API hugely -- and not in a way
121 * that makes the incoming-direction API any simpler, since that one always has to be ready for would-block
122 * (i.e., no in-messages immediately ready -- an async-receive API is required).
123 *
124 * So that left 2 or 3. I (ygoldfel) simply made the decision that one has to be chosen, and of those 2 is earlier
125 * and (internally for the concept implementer, also me at the time) more straightforward. If 3 were always
126 * reading items out of the low-level transport -- even when the user was not invoking `async_receive_*()` --
127 * then it would have to at least sometimes make copies of incoming data before the user chose to pop them out.
128 * By contrast, queueing it -- only on would-block -- in 2 would effectively make it a last-resort activity, as
129 * it should be.
130 *
131 * To summarize: Perfect opposing-receiver behavior cannot be guaranteed, and the low-level transport will eventually
132 * have a limit as to how much data it can buffer => in that case some entity must make a copy of the overflowing data
133 * => we want to make life easier for end user, so that entity should not be them => it must be either
134 * the `*_sender` or `*_receiver` => we chose `*_sender`, because it is safer and easier and faster and a last resort.
135 *
136 * One arguable drawback of this API design is that the Native_handle_sender (and Blob_sender) user won't be
137 * informed of an out-pipe-hosing error quite as soon as that object itself would know of it -- in some (rare)
138 * cases. I.e., if some data are queued up during would-block, and the user has stopped doing `send_*()`
139 * (as far as she is concerned, they're all working fine and have succeeded), and some timer later
140 * `*this` determines (in the background now) that the transport is hosed, the user would only find out about it
141 * upon the next `send_*()` call (it would *then* return the `Error_code` immediately), if any, or via
142 * async_end_sending(). Is this really a problem? Answer: Just... no. Exercise in reasoning this out (granted
143 * it is *somewhat* subjective) left to the reader. (Note we *could* supply some kind of out-of-band
144 * on-any-send-error API, and we considered this. It just was not worth it: inelegant, annoying. async_end_sending()
145 * with its completion handler is well sufficient.)
146 *
147 * It should be noted that this decision to make send_native_handle() synchronous/non-blocking/never-would-blocking
148 * has percolated through much of ipc::transport and even ipc::session: all our impls; Blob_sender and all its
149 * impls; Channel; and quite significantly struc::Channel. You'll note struc::Channel::send() is
150 * also synchronous/non-blocking/never-would-blocking, and even its implementation at the stage of actually
151 * sending binary data via Native_handle_sender/Blob_sender is quite straightforward. In Flow-IPC a
152 * `send*()` "just works."
153 *
154 * @todo Comparing Blob_sender::send_blob_max_size() (and similar checks in that family of concepts) to test
155 * whether the object is in PEER state is easy enough, but perhaps we can have a utility that would more
156 * expressively describe this check: `in_peer_state()` free function or something? It can still use the same
157 * technique internally.
158 */
160{
161public:
162 // Constants.
163
164 /// Shared_name relative-folder fragment (no separators) identifying this resource type. Equals `_receiver`'s.
166
167 // Types.
168
169 /**
170 * Return type, typically a `struct`, for native_handle_send_stats().
171 *
172 * In addition: the impl must choose one of the following paths. Note that stat::Blob_snd_stats defines statistics
173 * relevant to any concept impl of Native_handle_sender.
174 * -# This type *is* stat::Blob_snd_stats. Therefore, given a value `S` of this type, you can count on this:
175 * `using ipc::transport::stat::blob_snd_stats; assert(&(blob_snd_stats(S)) == &S)`.
176 * - That is: your impl adds no stats beyond the concept-level ones.
177 * -# This type *is not* stat::Blob_snd_stats but contains one via some type of composition. Therefore your
178 * Native_handle_sender impl must be accompanied (in the same namespace, for ADL) with a
179 * free function such that this works:
180 * `using ipc::transport::stat::blob_snd_stats; const stat::Blob_snd_stats& core_stats = blob_snd_stats(S)`.
181 * - That is: your impl adds some stats on top of the concept-level ones.
182 */
184
185 // Constructors/destructor.
186
187 /**
188 * Default ctor: Creates a peer object in NULL (neither connected nor connecting) state. In this state,
189 * all transmission-related methods (e.g., send_native_handle()) shall return `false` and otherwise no-op
190 * (aside from possible logging).
191 *
192 * This ctor is informally intended for the following uses:
193 * - A moved-from Native_handle_sender (i.e., the `src` arg for move-ctor and move-assignment operator)
194 * becomes as-if defaulted-constructed.
195 * - A target Native_handle_sender for a factory-like method (such as Native_socket_stream_acceptor::async_accept())
196 * shall typically be default-cted by the calling user. (E.g.: Native_socket_stream_acceptor shall asynchronously
197 * move-assign a logger-appointed, nicely-nicknamed peer object into that target `*this`, typically default-cted.)
198 *
199 * ### Informal corollary -- ctor that begins in PEER state ###
200 * Any functioning Native_handle_sender shall need at least one ctor that starts `*this` directly in PEER state.
201 * In that state the transmission-related methods (e.g., send_native_handle()) shall *not* return `false` and
202 * will in fact attempt transmission. However the form and function of such ctors is entirely dependent on
203 * the nature of the low-level transmission medium being encapsulated and is hence *intentionally* not a part
204 * of the concept. That said there is the `sync_io`-core-adopting ctor as well which *is* part of the concept.
205 *
206 * ### Informal corollary -- NULL-state ctors with 1+ args ###
207 * Other, 1+ arg, ctors that similarly create a NULL-state peer object are allowed/encouraged
208 * where relevant. In particular one taking a `Logger*` and a `nickname` string -- so as to subsequently enter a
209 * connecting phase via an `*_connect()` method, on the way to PEER state -- is reasonable.
210 * However this is not a formal part of this concept and is arguably not a general behavior.
211 * Such a ctor is informally intended for the following use at least:
212 * - One creates a Native_handle_sender that is `Logger`-appointed and nicely-`nickname`d; then one calls
213 * `*_connect()` on it in order to move it to a connecting phase and, hopefully, PEER state in that order.
214 * It will retain the logger and nickname (or whatever) throughout.
215 */
217
218 /**
219 * `sync_io`-core-adopting ctor: Creates a peer object in PEER state by subsuming a `sync_io` core in that state.
220 * That core must be as-if-just-cted (having performed no work and not been configured via `.start_*_ops()`).
221 * That core object becomes as-if default-cted (therefore in NULL state).
222 *
223 * @param sync_io_core_in_peer_state_moved
224 * See above.
225 */
226 Native_handle_sender(sync_io::Native_handle_sender&& sync_io_core_in_peer_state_moved);
227
228 /**
229 * Move-constructs from `src`; `src` becomes as-if default-cted (therefore in NULL state).
230 *
231 * @param src
232 * Source object. For reasonable uses of `src` after this ctor returns: see default ctor doc header.
233 */
235
236 /// Disallow copying.
238
239 /**
240 * Destroys this peer endpoint which will end the conceptual outgoing-direction pipe (in PEER state, and if it's
241 * still active) and cancels any pending completion handlers by invoking them ASAP with
242 * error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER.
243 * As of this writing these are the completion handlers that would therefore be called:
244 * - The handler passed to async_end_sending(), if it was not `.empty()` and has not yet been invoked.
245 * Since it is not valid to call async_end_sending() more than once, there is at most 1 of these.
246 *
247 * The pending completion handler will be called from an unspecified thread that is not the calling thread.
248 * Any associated captured state for that handler will be freed shortly after the handler returns.
249 *
250 * We informally but very strongly recommend that your completion handler immediately return if the `Error_code`
251 * passed to it is `error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER`. This is similar to what one should
252 * do when using boost.asio and receiving the conceptually identical `operation_aborted` error code to an
253 * `async_...()` completion handler. In both cases, this condition means, "we have decided to shut this thing down,
254 * so the completion handlers are simply being informed of this."
255 *
256 * In NULL state the dtor shall no-op.
257 *
258 * ### Thread safety ###
259 * Same as for the transmission methods. *Plus* it is not safe to call this from within a completion handler supplied
260 * to one of those methods on the same `*this`. An implication of the latter is as follows:
261 *
262 * Any user source code line that nullifies a `Ptr` (`shared_ptr`) handle to `*this` should be seen as potentially
263 * *synchronously* invoking this dtor. (By nullification we mean `.reset()` and anything else that makes the
264 * `Ptr` null. For example destroying a `vector<Ptr>` that contains a `Ptr` pointing to `*this` = nullification.)
265 * Therefore, if a nullification statement can possibly make the ref-count reach 0, then the user must zealously
266 * protect that statement from running concurrently with another send/receive API call on `*this`. That is to say,
267 * informally: When ensuring thread safety with respect to concurrent access to a Native_handle_sender, don't forget
268 * that the destructor also is a write-type of access, and that nullifying a `Ptr` handle to it can synchronously
269 * invoke it.
270 *
271 * @see Native_handle_receiver::~Native_handle_receiver(): sister concept with essentially equal requirements.
272 * @see Native_socket_stream::~Native_socket_stream(): implements concept (also implements just-mentioned sister
273 * concept).
274 */
276
277 // Methods.
278
279 /**
280 * Move-assigns from `src`; `*this` acts as if destructed; `src` becomes as-if default-cted (therefore in NULL state).
281 * No-op if `&src == this`.
282 *
283 * @see ~Native_handle_sender().
284 *
285 * @param src
286 * Source object. For reasonable uses of `src` after this ctor returns: see default ctor doc header.
287 * @return `*this`.
288 */
290
291 /// Disallow copying.
293
294 /**
295 * In PEER state: Returns max `meta_blob.size()` such that send_native_handle() shall not fail due to too-long
296 * payload with error::Code::S_INVALID_ARGUMENT. Always the same value once in PEER state. The opposing
297 * Native_handle_receiver::receive_meta_blob_max_size() shall return the same value (in the opposing object
298 * potentially in a different process).
299 *
300 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns zero; else
301 * a positive value.
302 *
303 * @return See above.
304 */
306
307 /**
308 * In PEER state: Synchronously, non-blockingly sends one discrete message, reliably/in-order, to the opposing peer;
309 * the message consists of the provided native handle (if supplied); or the provided binary blob (if supplied);
310 * or both (if both supplied). The opposing peer's paired Native_handle_receiver shall receive it reliably and
311 * in-order via Native_handle_receiver::async_receive_native_handle().
312 *
313 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns `false` immediately
314 * instead and otherwise no-ops (logging aside).
315 *
316 * Providing neither a handle nor a blob results in undefined behavior (assertion may trip).
317 * To *not* supply a handle, provide object with `.null() == true`. To *not* supply a blob, provide
318 * a buffer with `.size() == 0`.
319 *
320 * ### Blob and native-handle copying behavior; synchronicity/blockingness guarantees ###
321 * If a blob is provided, it need only be valid until this method returns. The implementation shall, informally,
322 * strive to *not* save a copy of the blob (except into the low-level transport mechanism); but it is *allowed* to
323 * save it if required, such as if the low-level transport mechanism encounters a would-block condition.
324 *
325 * The same goes for the native handle, if provided: it need only be valid until this method returns; the
326 * implementation shall duplicate it (a-la Native_handle::dup()) if it must defer transmission. Hence the user
327 * may close their handle immediately after this returns.
328 *
329 * @note Subtlety inherent in that: in POSIX, classic `fcntl()` record locks on a file are released when the
330 * process closes *any* descriptor to that file -- including such an internal duplicate. Do not send handles
331 * to files on which you hold such locks; open-file-description (OFD) locks are immune.
332 *
333 * This means, to the user, that this method is *both* non-blocking *and* synchronous *and* it must not
334 * refuse to send and return any conceptual would-block error (unlike, say, with a typical networked TCP socket API).
335 * Informally, the implementation must deal with any (typically quite rare) internal low-level would-block condition
336 * internally.
337 *
338 * However! Suppose send_native_handle() returns without error. This concept does *not* guarantee the message has
339 * been passed to the low-level transport at *this* point in time (informally it shall strive to make that
340 * happen in most cases however). A low-level would-block condition may cause it to be deferred. If one
341 * invokes ~Native_handle_sender() (dtor), the message may never be sent.
342 *
343 * However (part II)! It *is* guaranteed to have been passed to the transport once `*end_sending()`
344 * has been invoked, *and* its `on_done_func()` (if any) callback has been called. Therefore the expected use
345 * pattern is as follows:
346 * - Obtain a connected `Native_handle_sender` via factory.
347 * - Invoke send_native_handle() synchronously, hopefully without error.
348 * - (Repeat as needed for more such messages.)
349 * - Invoke `async_end_sending(F)` or `end_sending()` (to send graceful-close).
350 * - In `F()` destroy the `Native_handle_sender` (its dtor is invoked).
351 * - `F()` is invoked only once all queued messages, and the graceful-close, have been sent.
352 *
353 * ### Error semantics ###
354 * Firstly: See `flow::Error_code` docs for error reporting semantics (including the exception/code dichotomy).
355 *
356 * Secondly: If the generated #Error_code is error::Code::S_INVALID_ARGUMENT:
357 * - This refers to an invalid argument to *this* method invocation. This does *not* hose the pipe:
358 * further sends, etc., may be attempted with reasonable hope they will succeed.
359 * - send_meta_blob_max_size() is the limit for `meta_blob.size()`. Exceeding it leads to `S_INVALID_ARGUMENT`.
360 * - This interacts with the opposing Native_handle_receiver::async_receive_native_handle() in a subtle
361 * way. See "Blob underflow semantics" in that class concept's doc header.
362 * - At the impl's discretion, other conditions *may* lead to `S_INVALID_ARGUMENT`. If so they must
363 * be documented.
364 *
365 * Thirdly: All other non-success `Error_code`s generated should indicate the pipe is now indeed hosed; the user
366 * can count on the pipe being unusable from that point on. (It is recommended they cease to use the pipe
367 * and invoke the destructor to free resources.) If such a non-success code is emitted, the same one shall be emitted
368 * by all further send_native_handle() and async_end_sending() on `*this`.
369 *
370 * In particular error::Code::S_SENDS_FINISHED_CANNOT_SEND shall indicate that *this* method invocation
371 * occurred after invoking `*end_sending()` earlier.
372 *
373 * No would-block-like error condition shall be emitted.
374 *
375 * Lastly: The implementation is allowed to return a non-`INVALID_ARGUMENT` #Error_code in *this* invocation even
376 * though the true cause is a *previous* invocation of send_native_handle().
377 * Informally this may occur (probably in rare circumstances) if internally the handling of a previous
378 * call had to be deferred due to a low-level would-block condition; in that case the resulting problem is to be
379 * reported opportunistically with the next call. This allows for the non-blocking/synchronous semantics without
380 * the need to make send_native_handle() asynchronous.
381 *
382 * ### Thread safety ###
383 * You may call this from any thread. It is *not* required to be safe to call concurrently with
384 * any Native_handle_sender API, including this one and the destructor, invoked on `*this`.
385 *
386 * @param hndl_or_null
387 * A native handle (a/k/a FD) to send; or none if `hndl_or_null.null() == true`.
388 * @param meta_blob
389 * A binary blob to send; or none if `meta_blob.size() == 0`.
390 * @param err_code
391 * See above.
392 * @return `false` if `*this` is not in PEER (connected, transmitting) state;
393 * otherwise `true`.
394 */
395 bool send_native_handle(Native_handle hndl_or_null, const util::Blob_const& meta_blob,
396 Error_code* err_code = nullptr);
397
398 /**
399 * Equivalent to send_native_handle() but sends a graceful-close message instead of the usual payload; the opposing
400 * peer's Native_handle_receiver shall receive it reliably and in-order via
401 * Native_handle_receiver::async_receive_native_handle() in the form of
402 * #Error_code = error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE. If invoked after already invoking
403 * `*end_sending()`, the method shall no-op and return `false` and neither an exception nor truthy
404 * `*err_code`. Otherwise it shall return `true` -- but potentially emit a truthy #Error_code (if an error is
405 * detected).
406 *
407 * In addition: if `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns
408 * `false` immediately instead and otherwise no-ops (logging aside).
409 *
410 * Informally one should think of async_end_sending() as sending just another message, which is queued after
411 * preceding ones; but: it is the *last* message to be queued by definition. It's like an EOF or a TCP-FIN.
412 *
413 * ### Synchronicity/blockingness guarantees ###
414 * async_end_sending() is, unlike send_native_handle(), formally asynchronous. The reason for this,
415 * and the expected use pattern of async_end_sending() in the context of preceding send_native_handle()
416 * calls, is found in the doc header for Native_handle_sender::send_native_handle(). We omit further discussion here.
417 *
418 * ### Error semantics ###
419 * This section discusses the case where `true` is returned, meaning `*end_sending()` had not already
420 * been called.
421 *
422 * An #Error_code is generated and passed as the sole arg to `on_done_func()`.
423 * A falsy one indicates success. A truthy one indicates failure; but in particular:
424 * - error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER (destructor called, canceling all pending ops;
425 * spiritually identical to `boost::asio::error::operation_aborted`);
426 * - other arbitrary codes are possible, as with send_native_handle().
427 *
428 * Reminder: By the nature of `on_done_func()`, the pipe is finished regardless of whether it receives a
429 * success or non-success #Error_code.
430 *
431 * ### Thread safety ###
432 * The notes for send_native_handle() apply.
433 *
434 * @internal
435 * The semantic re. calling `*end_sending()` after already having called it and having that exclusively
436 * return `false` and do nothing was a judgment call. As of this writing there's a long-ish comment at the top
437 * of `"sync_io::Native_socket_stream_impl::*end_sending()"` body discussing why I (ygoldfel) went that way.
438 * @endinternal
439 *
440 * @tparam Task_err
441 * A functor type with signature identical to `flow::async::Task_asio_err`.
442 * @param on_done_func
443 * See above. This shall be invoked from an unspecified thread that is not the calling thread.
444 * @return `false` if and only if *either* `*this` is not in PEER (connected, transmitting) state, *or*
445 * `*end_sending()` has already been called before (so this is a no-op, in both cases).
446 * Otherwise `true`.
447 */
448 template<typename Task_err>
449 bool async_end_sending(Task_err&& on_done_func);
450
451 /**
452 * Equivalent to `async_end_sending(F)` wherein `F()` does nothing.
453 *
454 * Informally: If one uses this overload, it is impossible to guarantee everything queued has actually been sent,
455 * as `on_done_func()` is the only mechanism for this. Most likely this is only useful for test or proof-of-concept
456 * code; production-level robustness typically requires one to ensure everything queued has been sent. Alternatively
457 * the user's application-level protocol may already guarantee the message exchange has completed (e.g., by receiving
458 * an acknowledgment message of some sort) -- but in that case one can simply not call `*end_sending()`
459 * at all.
460 *
461 * @return Same as in async_end_sending().
462 */
464
465 /**
466 * In PEER state: Irreversibly enables periodic auto-pinging of opposing receiver with low-level messages that
467 * are ignored except that they reset any idle timer as enabled via Native_handle_receiver::idle_timer_run()
468 * (or similar). Auto-pings, at a minimum, shall be sent (subject to internal low-level transport would-block
469 * conditions) at the following times:
470 * - as soon as possible after a successful auto_ping() call; and subsequently:
471 * - at such times as to ensure that the time between 2 adjacent sends of *any* message is no more than
472 * `period`, until `*end_sending()` (if any) or error (if any).
473 * To clarify the 2nd point: If user messages are being sent already, an auto-ping may not be required or may be
474 * delayed until the last-sent message plus `period`). The implementation shall strive not to wastefully
475 * add auto-ping traffic unnecessary to this goal but is not *required* to do so. However, per the 1st point,
476 * an auto-ping shall be sent near auto_ping() time to establish a baseline.
477 *
478 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns `false` immediately
479 * instead and otherwise no-ops (logging aside). If auto_ping() has already been called successfully,
480 * subsequently it will return `false` and no-op (logging aside). If `*end_sending()` has been called successfully,
481 * auto_ping() will return `false` and no-op (logging aside).
482 *
483 * ### Behavior past `*end_sending()` ###
484 * As noted: auto_ping() returns `false` and no-ops if invoked after successful `*end_sending()`.
485 * Rationale: If `*end_sending()` has been called, then the receiver shall (assuming no other error) receive
486 * graceful-close (and hose the in-pipe as a result) as soon as possible, or it has already received it
487 * (and hosed the pipe). Therefore any potential "further" hosing of the pipe due to idle timer would be redundant
488 * and ignored anyway given the nature of the Native_handle_receiver (and similar) API.
489 *
490 * ### Thread safety ###
491 * The notes for send_native_handle() apply.
492 *
493 * @param period
494 * Pinging occurs so as to cause the opposing receiver to receive *a* message (whether auto-ping or user
495 * message) at least this frequently, subject to limitations of the low-level transport.
496 * The optional default is chosen by the impl to most reasonably work with the opposing `idle_timer_run()`
497 * to detect a zombified/overloaded peer.
498 * @return `false` if `*this` is not in PEER (connected, transmitting) state, or if already called successfully
499 * in `PEER` state, or if `*end_sending()` was called successfully in `PEER` state; otherwise `true`.
500 */
501 bool auto_ping(util::Fine_duration period = default_value);
502
503 /**
504 * Returns the accumulated transport statistics as of this call.
505 * If not in PEER state returns a zeroed-out stats object.
506 *
507 * @see #Native_handle_snd_stats doc header about how to obtain a transport::stat::Blob_snd_stats core from the
508 * returned `struct`.
509 *
510 * Thread-safe: can be called concurrently with any other method including `send_*()`.
511 * The returned copy is a consistent snapshot.
512 *
513 * @return Stats snapshot by value.
514 */
516
517 /**
518 * Resets the transport statistics as of this call. The formal meaning of a reset is discussed in
519 * `flow::util::stat` doc header. If not in PEER state this is a no-op.
520 *
521 * Thread-safe: can be called concurrently with any other method including `send_*()`.
522 */
524}; // class Native_handle_sender
525
526/**
527 * A documentation-only *concept* defining the behavior of an object capable of reliably/in-order *receiving* of
528 * discrete messages, each containing a native handle, a binary blob, or both. This is paired with
529 * the Native_handle_sender concept which defines sending of such messages.
530 *
531 * Concept contents
532 * ----------------
533 * The concept defines the following behaviors/requirements.
534 * - The object has at least 2 states, NULL and PEER. See notes in Native_handle_sender w/r/t this;
535 * they apply equally here.
536 * - The (incoming) transmission-of-messages methods, including reception of graceful-close message and
537 * cutting off any further receiving. See their doc headers.
538 * - Behavior when the destructor is invoked. See ~Native_handle_receiver() doc header.
539 * - Default ctor. See notes in Native_handle_sender w/r/t this; they apply equally here.
540 * - `sync_io`-core adopting ctor. Same deal.
541 * - Move ctor, move assigment operator. Same deal.
542 *
543 * The concept (intentionally) does *not* define the following behaviors:
544 * - How to create a Native_handle_receiver, except the default, `sync_io`-core-adopting, and move ctors.
545 * Notes for Native_handle_sender apply equally here.
546 *
547 * @see Native_socket_stream: as of this writing one key class that implements this concept (and also
548 * Native_handle_sender) -- using the Unix domain socket transport.
549 * @see Channel, a pipe-composing class template that potentially implements this concept.
550 * @see Blob_receiver: a degenerate version of the present concept: capable of transmitting only blobs, not
551 * `Native_handle`s.
552 *
553 * Batch-receiving
554 * ---------------
555 * Native_handle_receiver has async_receive_native_handle() which is the basic request to receive exactly one
556 * in-message, as soon as one is available. It also has async_receive_native_handle_batch() which is a request
557 * to receive an **in-batch** of messages. A **batch** is, in a sense, a container with a *capacity* of up to N
558 * messages; a received **in-batch** is that same container that stores at least 1 and at most N actually-received
559 * in-messages. Therefore, async_receive_native_handle_batch() (with arg `assume_would_block = false`) is a request
560 * to receive [1, N] messages, as soon as at least 1 in-message is available.
561 * - Receiving up to (N - 1) messages means that many were available, and the in-pipe is in would-block state.
562 * Typically, for best performance, one would then issue another async_receive_native_handle_batch() but set
563 * the arg `assume_would_block = true`.
564 * - Receiving N messages means *either* that that many were available, and the in-pipe is in would-block state,
565 * *or* at least (N + 1) messages were available. In that case one would
566 * typically attempt another read to determine which was the case; hence
567 * async_receive_native_handle_batch() with `assume_would_block = false`.
568 *
569 * Why does batch-receiving exist? Answer in short: for performance. For some OS + transport combos there may
570 * exist a low-level API (namely `recvmmsg()` in Linux; note the extra `m`) capable of receiving 1+ in-datagrams
571 * with just one OS-call which is faster than attempting potentially many single-message receive OS-calls, until
572 * one yields would-block. Moreover the common case where such a batch-receive yields fewer-than-N in-messages
573 * implies the in-pipe is in would-block; therefore one need not try another receive-op right then and instead
574 * wait for readability; hence yet another OS-call is typically saved.
575 *
576 * That said a number of important caveats apply. These are:
577 * - By definition, batch-receiving is only helpful perf-wise if indeed a low-level batch-receive API is provided
578 * by the OS. More to the point, if it *is* provided, then (again by definition) it is only conceptually relevant
579 * if the low-level transport maintains message boundaries; that is it views the pipe as a stream of *datagrams*
580 * as opposed to stream of *bytes*.
581 * - For clarity consider the specific example of Linux's `recvmmsg()`. It exists; check. However it works only
582 * for the socket-types `SOCK_DGRAM` (the UDP-like connection-less Unix domain socket) and `SOCK_SEQPACKET`
583 * (similar but with TCP-like connection semantics). For `SOCK_STREAM` -- informally speaking the much
584 * more popular Unix-domain (local) socket type -- `recvmmsg()` is useless/inapplicable.
585 * - A single batch-receive *in and of itself* is faster than the M single-message-receives that obtain
586 * (M - 1) in-messages (plus one to detect the would-block state after that). However in order for this
587 * op to work, one must set up an array-like data structure consisting of N in-message slots. Even ignoring
588 * the alloc cost itself, it is typical that one perform some per-slot work as preparation (so, usually a cost
589 * linear in N). It is easy to conceive of a situation where, actually, in-batches tend to be small (e.g.,
590 * M = 2), and the linear-in-N (where N=64 or N=32 is typical) prep cost dwarfs the benefit as a result.
591 * - Not to worry! For this reason this concept requires the use of the Msg_batch_in concept
592 * (see #Native_handle_batch_in alias member). A #Native_handle_batch_in pointer
593 * shall be passed to async_receive_native_handle_batch() method. To efficiently
594 * perform receive-batching, the user shall create (typically) a *single* `Native_handle_batch_in` per `*this`; so
595 * the linear-in-N original prep is performed only once. On successful async_receive_native_handle_batch(),
596 * the user shall (by dealing directly with the `Native_handle_batch_in` object) *consume* the 1+ in-msgs; this
597 * automatically re-preps each of those 1+ slots for the next batch-receive. Meanwhile the remaining
598 * slots (of the N total) simply sit untouched. Thus, the total amount of compute is close to identical
599 * to the scenario without any batch-receiving. Only the initial #Native_handle_batch_in ctor call is
600 * somewhat expensive; but this is only as frequent as creating the entire Native_handle_receiver; therefore
601 * negligible.
602 * - Formally speaking, the behavior of batch-receiving is defined by a combination of three concepts:
603 * - Method Native_handle_receiver::async_receive_native_handle_batch(). See its doc header here.
604 * - Class template `Msg_batch_in<..., NO_HNDLS>` with `NO_HNDLS = false`. See its docs nearby.
605 * - Alias member Native_handle_receiver::Native_handle_batch_in, which points to an impl of
606 * concept `Msg_batch_in<..., false>`.
607 *
608 * What about the first caveat though? Given that it helps perf only if the OS+transport actually support some
609 * (informally speaking somewhat obscure in practice) features, should the user even make use of batch-receiving?
610 * It depends on the specifics, of course, but generally speaking:
611 * - If there is any possibility that for any of your target OS/environments a low-level transport with datagram
612 * support *and* built-in batch-receiving in facts exists, *and* you care about perf at this level, then:
613 * Yes, you should consider coding for batch-receiving. Otherwise it's simpler to stick with
614 * basic-receiving 1 in-message at a time (async_receive_native_handle()).
615 * - Native_handle_receiver concept's impls *must* support batch-receiving. What if it's not available at the
616 * transport layer? Yes, they still must. In practice all available Flow-IPC impls are written in such a way
617 * as to *emulate* batch-receiving (internally via basic one-message receive-ops) if needed; and in doing so they
618 * as of this writing go to great lengths to make this essentially no slower than the M 1-message receives had they
619 * been performed by the user directly via M async_receive_native_handle() calls. So you aren't risking much by
620 * coding for it and letting the Flow-IPC impl handle it as best it can. So at that point the only real cost
621 * is the added complexity of your code in having to deal with a #Native_handle_batch_in.
622 * - Impl note: The class template Generic_msg_batch_in and helper free-function
623 * sync_io::async_receive_batch_emulation() are supplied for these emulation needs. Flow-IPC concept impls use
624 * them, but so can the user, if interested in implementing their own Native_handle_receiver (and/or
625 * Blob_receiver).
626 *
627 * Blob underflow semantics
628 * ------------------------
629 * This potentially important subtlety is regarding the interplay between
630 * Native_handle_receiver::S_META_BLOB_UNDERFLOW_ALLOWED, async_receive_native_handle() `meta_blob.size()`,
631 * and Native_handle_sender::send_native_handle() `meta_blob.size()`.
632 *
633 * Consider a `*this` named `R` and an opposing `*_sender` named `S`. Note that `R.receive_meta_blob_max_size()
634 * == S.send_meta_blob_max_size()`; call this limit `L`. `L` shall always be in practice small enough to where
635 * allocating a buffer of this size as the receive target is reasonable; hence kilobytes at most, not megabytes.
636 * (In some cases, such as with `Blob_stream_mq_*`, it is configurable at runtime. In others, as with
637 * Native_socket_stream, it is a constant: Native_socket_stream_cfg::S_MAX_META_BLOB_LENGTH.)
638 *
639 * As noted in that concept's docs, naturally, `S.send_native_handle()` shall yield immediate, non-pipe-hosing
640 * error::Code::S_INVALID_ARGUMENT, if `meta_blob.size() > L`. This is called overflow and is always enforced.
641 *
642 * Now consider `N = meta_blob.size()` for `R.async_receive_native_handle()`. This is the size of the target
643 * blob: how many bytes it *can* receive. How `L` is enforced by `R` is not uniform among this concept's impls:
644 * rather it depends on the compile-time constant Native_handle_receiver::S_META_BLOB_UNDERFLOW_ALLOWED.
645 *
646 * ### Semantics: `META_BLOB_UNDERFLOW_ALLOWED` is `false` ###
647 * Suppose it is `false` (as is the case for Blob_stream_mq_receiver). In that case `N < L` is *disallowed*:
648 * `R.async_receive_native_handle()` *shall always* fail with non-pipe-hosing error::Code::S_INVALID_ARGUMENT
649 * for that reason. Certainly `N` cannot be zero -- not even if the incoming message is expected to
650 * contain only a non-`.null() Native_handle`.
651 *
652 * @note Formally it's as written. Informally the rationale for allowing this restriction is that some low-level
653 * transports have it; and working around it would probably affect performance and is unnatural.
654 * In particular Persistent_mq_handle::try_receive() (MQ receive) shall immediately fail if a target buffer
655 * is supplied smaller than Persistent_mq_handle::max_msg_size(). For both POSIX (`man mq_receive`) and
656 * bipc (`boost::interprocess::message_queue::try_receive()` docs and source code) it is disallowed at the
657 * low-level API level.
658 *
659 * Thus since `N >= L` is guaranteed before any actual receipt attempt is made, it is *impossible* for
660 * a message to arrive such that its meta-blob's size would overflow the target blob (buffer) (of size `N`).
661 * Therefore error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE shall not be emitted.
662 *
663 * #### Sub-case: Batch-receiving ####
664 * While the basic (single-message) APIs async_receive_native_handle() (also Blob_receiver::async_receive_blob())
665 * are *required* to detect `N < L` and emit `INVALID_ARGUMENT` if so, the rule for
666 * async_receive_native_handle_batch() (also Blob_receiver:async_receive_blob_batch()) simply repeats this
667 * for *each slot* in `*batch`.
668 *
669 * @warning Impl note: You should not in fact perform this check for each slot. Instead do so only for slot 0.
670 * This would be a linear-time operation, repeated potentially many times
671 * for a given `*batch`; as typically one reuses a single `*batch` object, typically over an entire `*this`
672 * lifetime. Doing it for only slot 0 is also redundant in that case but overall much less expensive.
673 * Msg_batch_in concept requires that each slot's capacity is the same across the Msg_batch_in object, or
674 * behavior is undefined. Therefore this slot-0-only check is sufficient for the required behavior.
675 *
676 * ### Semantics: `META_BLOB_UNDERFLOW_ALLOWED` is `true` ###
677 * Suppose it is `true` (as is the case for Native_socket_stream if and only if
678 * Native_socket_stream_cfg::S_USE_OS_DGRAM_SUPPORT is `false`. In that case `N < L` is allowed, in the sense
679 * that `R.async_receive_native_handle()` *shall not* fail with non-pipe-hosing error::Code::S_INVALID_ARGUMENT
680 * for that reason. In particular, in that case, `N` can even be zero: this implies the incoming message *must*
681 * contain a non-`.null() Native_handle` (it is not allowed to send a message with no content at all).
682 * (In the case of degenerate concept Blob_receiver, `N` must exceed zero, as it cannot transmit anything but
683 * blobs.)
684 *
685 * However suppose a message has arrived, and its meta-blob's size -- which cannot exceed `L` -- is larger than
686 * `N`. I.e., it would *underflow* the user-supplied target buffer (blob). In that case:
687 * - async_receive_native_handle() shall emit error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE.
688 * - This error shall be pipe-hosing (further receive attempts will yield the same error).
689 *
690 * Informal tips: Any blob-size error can be completely avoided by always supplying target blob with `N == L`.
691 * (`N > L` will also work but may be considered wasteful, all else being equal.) In addition this tactic will
692 * ensure the code will generically work with a different Native_handle_receiver impl for which
693 * `S_META_BLOB_UNDERFLOW_ALLOWED == false`. Alternatively, the user protocol may be such that you simply know
694 * that `S` will never send messages beyond some limit (smaller than `L`), perhaps for particular messages.
695 * In that case `N` can be set to this smaller-than-`L` value. However -- such code will fail
696 * if the concept impl is later to switched to one with `S_META_BLOB_UNDERFLOW_ALLOWED == true`. This may or
697 * may not be an issue depending on your future dev plans.
698 *
699 * ### Summary ###
700 * This dichotomy of possible semantics is a conscious choice. Mode `false` is a necessity: some low-level transports
701 * enforce it, period. Mode `true` is to support a possible algorithmic desire to allocate smaller-than-`L`
702 * target buffers. The problem is some low-level transports allow this; but others don't. We did not want to
703 * impose one type's limitations on users of the other type. Therefore, if your code is agnostic to the type
704 * of transport, then code as-if `S_META_BLOB_UNDERFLOW_ALLOWED == false`. Otherwise you may code otherwise.
705 *
706 * Rationale: Why no `end_receiving()`, given opposing concept has `*end_sending()`?
707 * ---------------------------------------------------------------------------------
708 * An early version of Native_handle_receiver and Blob_receiver did have an `end_receiving()`.
709 * The idea was it would have 2 effects (plus 1 bonus):
710 * -# Any ongoing `async_receive_*()`s would be interrupted with in-pipe-hosing error
711 * error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE -- the same one as when receing a graceful-close
712 * from opposing `*end_sending()`.
713 * -# There would be no further reading from the low-level transport for any reason.
714 * -# There was also the *thought*, but not the action, that an impl could inform the *opposing sender* of the
715 * pipe being closed by the reader. This would be conceptually similar to SIGPIPE in POSIX. In practice, of
716 * the available low-level transports as of this writing, only Native_socket_stream could actually implement it,
717 * as the otherwise-full-duplex independent pipes do live in this same peer (Unix domain socket), so the out-pipe
718 * could be used to inform the other guy that *his* out-pipe (our in-pipe) is now pointless. `Blob_stream_mq_*`
719 * are fully independent, operating on separate MQ kernel objects, so it was not realistically possible in
720 * that case.
721 *
722 * In order: (1) is fairly dubious: the local user should know when they've stopped receiving and don't need to
723 * use this mechanism to inform themselves. (3), as stated, sounded like a possible future improvement, but time
724 * told us it was not going to work out: if only a 2-pipe transport can do it in any case, organizing a concept
725 * around such a thing is just complicated. If necessary the user can (and we think likely will) just arrange
726 * their own protocol. So that leaves (2).
727 *
728 * To begin with -- in reality user protocols tend to be designed to not need such measures. If the reader is no
729 * longer interested, writer will probably know that. This is not networking after all: it is IPC.
730 * That aside, though, it only makes sense in any case if an impl *chooses* to greedily cache all low-level
731 * in-traffic in user RAM, even while no `async_receive_*()`s are pending to read it. Early on we actually did do
732 * that; but with the move to `sync_io`-pattern cores, and after contemplating
733 * "Rationale: Why is send-native-handle not asynchronous?" (Native_handle_sender concept doc header),
734 * we got rid of this waste of compute, letting unwanted in-traffic accumulate within a sender peer object
735 * (Native_handle_sender, Blob_sender) only. So in practice:
736 *
737 * If `end_receiving()` does not signal anything of value ((1) and (3)), and does not actually prevent any
738 * low-level reading that would otherwise occur, then it is a pointless complication. So that is why we got rid of it.
739 */
741{
742public:
743 // Constants.
744
745 /// Shared_name relative-folder fragment (no separators) identifying this resource type. Equals `_sender`'s.
747
748 /**
749 * If `false` then `meta_blob.size() < receive_meta_blob_max_size()` in PEER-state async_receive_native_handle()
750 * or `batch->target_payload_size() < receive_meta_blob_max_size()` in PEER-state async_receive_native_handle_batch()
751 * shall yield non-pipe-hosing error::Code::INVALID_ARGUMENT, and it shall never yield
752 * pipe-hosing error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE; else the latter may occur, while the former
753 * shall never occur for that reason.
754 *
755 * @see "Blob underflow semantics" in class concept doc header.
756 */
757 static constexpr bool S_META_BLOB_UNDERFLOW_ALLOWED = value;
758
759 /**
760 * A positive number: the suggested value to pass for the `max_msg_count` arg to Msg_batch_in ctor of
761 * object(s) passed to `async_receive_..._batch()` invocations; the value `1` indicates in particular that
762 * there is no/poor native support for batch-receiving. The value `1` also implies that
763 * code that avoids `async_receive_..._batch()` entirely and uses only `async_receive_...()` will be no slower
764 * (possibly a bit faster) and easier to read/maintain. Conversely a value exceeding `1` implies that using
765 * batch-receiving may be beneficial for perf. Below we expand on this.
766 *
767 * This is "only" a recommendation, though other parts of Flow-IPC may well take it to heart
768 * (struc::Channel does in particular, as of this writing, when deciding whether to even use the batching code
769 * path -- when this is not set to 1 -- and if so then the batch-size value is taken from here).
770 * A different value chosen for `max_msg_count` must still be supported by any `..._receiver` and
771 * Msg_batch_in impls. (Flow-IPC provides all necessary tools for such support to be maximally performant, even
772 * when your particular transport does not support native batch-receiving, and the user chose
773 * `max_msg_count >= 2`. For info on that see this concept class doc header, section "Batch-receiving.")
774 *
775 * The most important piece of information carried in this value is whether it equals `1` or is `> 1`.
776 * If and only if it equals one, essentially that means that however the impl implements `async_receive_..._batch()`,
777 * it will be not significantly better than performing a series of single-message `async_receive_...()`s.
778 * Instead it'll be about the same (perhaps a tiny bit worse due to minor book-keeping overhead).
779 * Speaking less formally, in all known situations this is set to `1` if and only if the host OS/environment
780 * provides no native batch-receiving capability. E.g., supposing `*this` impl (e.g., Native_socket_stream)
781 * works over local-sockets, then in a POSIX-y OS there will be `recvmsg()` (single-message
782 * read, or byte-stream read) but may or may not be a `recvmmsg()` (batch-read of messages). If not then this
783 * would be set to one; otherwise to 2 or more.
784 *
785 * If this equals 1, and you indeed follow that recommendation, then general batch-receive-supporting code
786 * will work; in some cases a `*this` impl might take internal code paths optimized for this case (e.g.,
787 * use `recvmsg()` instead of `recvmmsg()`, even when the latter is available). Moreover, it is
788 * possible for a `*this` *user* to write code that takes a different path (avoiding `async_receive_..._batch()`)
789 * in that case, and such code might be simpler and feature less overhead and thus be somewhat faster and easier to
790 * read/maintain. For example struc::sync_io::Channel internal impl takes this approach, if at compile-time
791 * the relevant pipe's `S_RCV_NATIVE_HANDLE_BATCH_SZ_RECOMMENDATION == 1` (as of this writing).
792 *
793 * If this exceeds 1, then the particular value shall strive to be on the high side to capture available perf
794 * benefits (i.e., larger than a big % of in-batches in production) but avoid excessive or pointless memory use.
795 * (E.g., if a given transport cannot contain more than N unread messages on the "wire" at any time,
796 * then `S_RCV_NATIVE_HANDLE_BATCH_SZ_RECOMMENDATION <= N` is sensible.) The user can use their judgment and
797 * ignore the recommendation in either direction depending on specifics.
798 *
799 * To restate: The Boolean value `(S_RCV_NATIVE_HANDLE_BATCH_SZ_RECOMMENDATION == 1)` is the most significant
800 * piece of info communicated.
801 */
802 static constexpr size_t S_RCV_NATIVE_HANDLE_BATCH_SZ_RECOMMENDATION = value;
803
804 // Types.
805
806 /// See `async_receive_*_batch()` argument `batch`.
807 template<typename Msg_resource>
809
810 /**
811 * Return type, typically a `struct`, for native_handle_receive_stats().
812 *
813 * In addition: the impl must choose one of the following paths. Note that stat::Blob_rcv_stats defines statistics
814 * relevant to any concept impl of Native_handle_receiver.
815 * -# This type *is* stat::Blob_rcv_stats. Therefore, given a value `S` of this type, you can count on this:
816 * `using ipc::transport::stat::blob_rcv_stats; assert(&(blob_rcv_stats(S)) == &S)`.
817 * - That is: your impl adds no stats beyond the concept-level ones.
818 * -# This type *is not* stat::Blob_rcv_stats but contains one via some type of composition. Therefore your
819 * Native_handle_receiver impl must be accompanied (in the same namespace, for ADL) with a
820 * free function such that this works:
821 * `using ipc::transport::stat::blob_rcv_stats; const stat::Blob_rcv_stats& core_stats = blob_rcv_stats(S)`.
822 * - That is: your impl adds some stats on top of the concept-level ones.
823 */
825
826 // Constructors/destructor.
827
828 /**
829 * Default ctor: Creates a peer object in NULL (neither connected nor connecting) state. In this state,
830 * all transmission-related methods (e.g., async_receive_native_handle()) shall return `false` and otherwise no-op
831 * (aside from possible logging).
832 *
833 * All notes from Native_handle_sender() default ctor doc header apply here analogously. They are helpful so
834 * please read Native_handle_sender() doc header.
835 */
837
838 /**
839 * `sync_io`-core-adopting ctor: Creates a peer object in PEER state by subsuming a `sync_io` core in that state.
840 * That core must be as-if-just-cted (having performed no work and not been configured via `.start_*_ops()`).
841 * That core object becomes as-if default-cted (therefore in NULL state).
842 *
843 * @param sync_io_core_in_peer_state_moved
844 * See above.
845 */
846 Native_handle_receiver(sync_io::Native_handle_receiver&& sync_io_core_in_peer_state_moved);
847
848 /**
849 * Move-constructs from `src`; `src` becomes as-if default-cted (therefore in NULL state).
850 *
851 * @param src
852 * Source object. For reasonable uses of `src` after this ctor returns: see default ctor doc header.
853 */
855
856 /// Disallow copying.
858
859 /**
860 * Destroys this peer endpoint which will end the conceptual incoming-direction pipe (in PEER state, and if it's
861 * still active) and cancels any pending completion handlers by invoking them ASAP with
862 * error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER.
863 * As of this writing these are the completion handlers that would therefore be called:
864 * - Any handler passed to async_receive_native_handle() that has not yet been invoked.
865 * There can be 0 or more of these.
866 *
867 * The pending completion handler(s) (if any) will be called from an unspecified thread that is not the calling
868 * thread. Any associated captured state for that handler will be freed shortly after the handler returns.
869 *
870 * The rest of the notes in ~Native_handle_sender() apply equally here.
871 *
872 * @see Native_handle_sender::~Native_handle_sender(): sister concept with essentially equal requirements.
873 * @see Native_socket_stream::~Native_socket_stream(): implements concept (also implements just-mentioned sister
874 * concept).
875 */
877
878 // Methods.
879
880 /**
881 * Move-assigns from `src`; `*this` acts as if destructed; `src` becomes as-if default-cted (therefore in NULL state).
882 * No-op if `&src == this`.
883 *
884 * @see ~Native_handle_receiver().
885 *
886 * @param src
887 * Source object. For reasonable uses of `src` after this ctor returns: see default ctor doc header.
888 * @return `*this`.
889 */
891
892 /// Disallow copying.
894
895 /**
896 * In PEER state: Returns min target-meta-blob size such that (1) async_receive_native_handle() shall not fail
897 * with error::Code::S_INVALID_ARGUMENT (only if #S_META_BLOB_UNDERFLOW_ALLOWED is `false`; otherwise not relevant),
898 * and (2) async_receive_native_handle_batch() shall not similarly fail,
899 * and (3) neither shall *ever* fail with error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE. Please see
900 * "Blob underflow semantics" for explanation of these semantics.
901 *
902 * Always the same value once in PEER state. The opposing
903 * Native_handle_sender::send_meta_blob_max_size() shall return the same value (in the opposing object potentially
904 * in a different process).
905 *
906 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns zero; else
907 * a positive value.
908 *
909 * @return See above.
910 */
912
913 /**
914 * In PEER state: Asynchronously awaits one discrete message -- as sent by the opposing peer via
915 * Native_handle_sender::send_native_handle() or `"Native_handle_sender::*end_sending()"` -- and
916 * receives it into the given target locations, reliably and in-order. The message is, therefore, one of the
917 * following:
918 * - A binary blob; a native handle; or both. This is indicated by `on_done_func(Error_code{}, N)`.
919 * The falsy code indicates success; `N <= target_meta_blob.size()` indicates the number of bytes received into
920 * `target_meta_blob.data()` (zero means no blob was sent in the message). `*target_hndl` is set
921 * (`target_hndl->null() == true` means no handle was sent in the message).
922 * - Graceful-close. This is indicated by `on_done_func(error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE, 0)`;
923 * neither the target blob nor target native handle are touched.
924 *
925 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns `false` immediately
926 * instead and otherwise no-ops (logging aside).
927 *
928 * ### Blob copying behavior; synchronicity/blockingness guarantees ###
929 * `*target_hndl` and the area described by `target_meta_blob` must both remain valid until `on_done_func()`
930 * executes. The method itself shall be non-blocking.
931 *
932 * Moreover -- and this applies to *all* `async_receive_*()` forms including `*_batch()` -- while a given
933 * async-receive request is outstanding (its completion handler not yet invoked), its target areas
934 * (`*target_hndl` and the `target_meta_blob` area here; the entire `*batch` object for the batch forms)
935 * belong to `*this`: accessing them, or issuing another `async_receive_*()` whose targets overlap them,
936 * yields undefined behavior. (In particular: to issue 2+ concurrently-outstanding requests -- which is
937 * allowed -- use disjoint targets: distinct buffers; distinct batch objects.)
938 *
939 * The implementation shall, informally, strive to *not* copy the received blob (if any) into
940 * `target_meta_blob.data()...` except from the low-level transport mechanism. That is: it shall strive to not
941 * store the blob data in some internal buffer before ending up in `target_meta_blob.data()...`.
942 *
943 * ### Error semantics ###
944 * An #Error_code is generated and passed as the 1st arg to `on_done_func()`.
945 * A falsy one indicates success. A truthy one indicates failure; but in particular:
946 * - error::Code::S_OBJECT_SHUTDOWN_ABORTED_COMPLETION_HANDLER (destructor called, canceling all pending ops;
947 * spiritually identical to `boost::asio::error::operation_aborted`);
948 * - only if #S_META_BLOB_UNDERFLOW_ALLOWED is `true`:
949 * - error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE (opposing peer has sent a message with a meta-blob exceeding
950 * `target_meta_blob.size()` in length; in particular one can give an empty buffer if no meta-blob expected);
951 * - error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE (peer gracefully closed pipe via `*end_sending()`);
952 * - error::Code::S_RECEIVER_IDLE_TIMEOUT (idle timeout: see idle_timer_run());
953 * - other arbitrary codes are possible.
954 * - No would-block-like error condition shall be emitted.
955 *
956 * A non-success on-done invocation means the incoming pipe is hosed. (It is recommended they cease to use the pipe
957 * and invoke the destructor to free resources.) If such a non-success code is emitted, the same one shall be emitted
958 * by all further async_receive_native_handle() calls on `*this`. Key exception:
959 * - error::Code::S_INVALID_ARGUMENT:
960 * - This refers to an invalid argument to *this* method invocation.
961 * - If and only if #S_META_BLOB_UNDERFLOW_ALLOWED is `false`: A length floor on the target blob is imposed.
962 * Hence if `target_meta_blob.size() < receive_meta_blob_max_size()` then `S_INVALID_ARGUMENT` is emitted.
963 * - This shall *not* hose the pipe. Subsequent async-receives may be attempted with reasonable hope of
964 * success.
965 * - At the impl's discretion, other conditions *may* lead to `S_INVALID_ARGUMENT`. If so they must
966 * be documented.
967 *
968 * ### Thread safety ###
969 * You may call this either from any thread including the unspecified thread running within another call's
970 * `on_done_func()`. It is *not* required to be safe to call concurrently with any Native_handle_receiver API,
971 * including this one and the destructor, invoked on `*this`.
972 *
973 * @tparam Task_err_sz
974 * A functor type with signature identical to `flow::async::Task_asio_err_sz`.
975 * @param target_hndl
976 * `*target_hndl` shall be set by the time `on_done_func()` is executed with a falsy code. See above.
977 * @param target_meta_blob
978 * `target_meta_blob.data()...` shall be written to by the time `on_done_func()` is executed with a falsy
979 * code, bytes numbering `N`, where `N` is passed to that callback. `N` shall not exceed
980 * `target_meta_blob.size()`. See above.
981 * @param on_done_func
982 * See above. This shall be invoked from an unspecified thread that is not the calling thread.
983 * @return `false` if `*this` is not in PEER (connected, transmitting) state;
984 * otherwise `true`.
985 */
986 template<typename Task_err_sz>
988 const util::Blob_mutable& target_meta_blob,
989 Task_err_sz&& on_done_func);
990
991 /**
992 * In PEER state: Asynchronously awaits 1+ discrete message(s) -- as sent by the opposing peer via
993 * Native_handle_sender::send_native_handle() or `"Native_handle_sender::*end_sending()"` -- and
994 * receives them into the target locations as described by `*batch` slots, reliably and in-order.
995 *
996 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns
997 * `false` immediately instead and otherwise no-ops (logging aside). Otherwise:
998 *
999 * ### Semantics of the async-op ###
1000 * The result of the async-op is one of:
1001 * - `batch->n_used()` grows by at least 1, namely the number of in-messages obtained;
1002 * no error has occurred/is emitted.
1003 * - An error has occurred/is emitted; `batch->n_used()` remains unchanged. Graceful-close in particular is
1004 * counted as an error, namely error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE.
1005 *
1006 * @see This concept class doc header, section "Batch-receiving." It provides essential background.
1007 *
1008 * @note Reminder (see async_receive_native_handle() "Blob copying behavior" section): an outstanding
1009 * async-receive's targets belong to `*this`; hence 2+ concurrently-outstanding batch-receives must
1010 * target *distinct* batch objects.
1011 *
1012 * @note It is not possible that the result of the async-op is 1+ messages received *and* an error is emitted.
1013 * Even if internally this occurs, `*this` must first emit the 1+ messages; and report the error
1014 * in the next `async_receive_*()` call. For example, if 3 messages and graceful-close are simultaneously
1015 * detected as having arrived, this call shall emit the former; next call shall emit the graceful-close.
1016 *
1017 * @note Be aware of the *implied would-block* condition. See Msg_batch_in::full() doc header. Spoiler alert:
1018 * if we yielded data/no error (`batch->n_used()` increased), then it wasn't would-block... but if
1019 * `batch->full() == false` post-op, then the pipe is nevertheless in would-block state. In this case
1020 * for performance the next (typically immediate) `async_receive_*_batch()` call should set
1021 * `assume_would_block = true`; otherwise to `false`.
1022 *
1023 * ### Error semantics ###
1024 * Same notes as for async_receive_native_handle(). Additionally: If `(!batch->initialized()) || batch->full()`
1025 * at entry to this call, then error::Code::S_INVALID_ARGUMENT is emitted. Informally speaking: not only would
1026 * one not typically invoke us in that case, but actually typically `batch->n_used() == 0` at entry.
1027 *
1028 * ### Error semantics: Delayed error ###
1029 * Consider the scenario wherein one performs N single-receives (async_receive_native_handle() N times), and
1030 * all but the last of the N result in success (each one emitting a received message, for simplicity of discussion
1031 * without any would-blocks in there), but the Nth one yields a (non-`INVALID_ARGUMENT`, at least in-pipe-hosing,
1032 * possibly out-pipe-hosing too) error.
1033 *
1034 * Now consider the analogous *single* batch-receive, async_receive_native_handle_batch(), where `*batch` capacity
1035 * is at least N (and pre-condition `batch->n_used() == 0` for simplicity of discussion). Should the
1036 * batch-receive yield success with `(batch->n_used() == N - 1)`, while the next async-receive yields error
1037 * (*delayed error*)? Or should the batch-receive yield the error immediately, eating the `(N - 1)` in-messages?
1038 * - Option 1: *Delayed error* outcome. (This mirrors the single-receives experience.)
1039 * - Option 2: *Instant error* outcome. (This contradicts the single-receives experience; it eats in-messages.)
1040 *
1041 * A compliant impl must follow this policy:
1042 * - The *delayed error* outcome *must* occur, if the error is error::Code::S_RECEIVES_FINISHED_CANNOT_RECEIVE.
1043 * (A protocol-implemented graceful-close, from opposing-side `*_sender::*end_sending()`) shall not result in
1044 * invalidation of any preceding in-messages.)
1045 * - The *delayed error* outcome *must* occur, if the error is spiritually similar to `boost::asio::error::eof`.
1046 * (A transport-implemented graceful-close shall not result in invalidation of any preceding in-messages.
1047 * E.g., a Native_socket_stream will observe a native "EOF," if the opposing side neglects to
1048 * `*end_sending()` (which would would `RECEIVES_FINISHED_CANNOT_RECEIVE`) and ends the connection via dtor,
1049 * or if that side crashes before `*end_sending()`).)
1050 * - Informally: recommend issuing actual `boost::asio::error::eof` in this case, even if your transport's
1051 * precipitating actual system error was something different but still indicating a non-exceptional
1052 * end of pipe. This helps, as of this writing, use/be consistent with related Flow-IPC tools including
1053 * sync_io::async_receive_batch_emulation() which will specially treat that specific error as of this writing.
1054 * - For any other error outcome, including but not limited to `S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE`,
1055 * the *instant error* outcome *must* occur.
1056 * - In this outcome any `Native_handle`s received with the eaten in-messages shall be closed (returned
1057 * to the OS) -- not leaked.
1058 * - Rationale: It would have been not-unreasonable to allow the concept impl to do *delayed error* here
1059 * (but not mandate it). In particular that would mirror the experience of using single-receives only.
1060 * The reason we do not allow it is for consistency of expected behavior across impls. (For example
1061 * test scripts covering multiple `..._receiver` impls would not need special cases to cover the range
1062 * of possible behaviors. At least, it is conceivably so.)
1063 *
1064 * ### Thread safety ###
1065 * You may call this either from any thread including the unspecified thread running within another call's
1066 * `on_done_func()`. It is *not* required to be safe to call concurrently with any Native_handle_receiver API,
1067 * including this one and the destructor, invoked on `*this`.
1068 *
1069 * @tparam Msg_resource
1070 * See Msg_batch_in docs; this is the eponymous template parameter to `decltype(*batch)`.
1071 * @tparam Task_err
1072 * A functor type with signature identical to `flow::async::Task_asio_err`. Note that information about
1073 * the number of messages received (if no error) is indicated via `batch->n_used()` change.
1074 * Furthermore the number of bytes in each received blob is indicated inside `*batch` as well;
1075 * use Msg_batch_in::result_payload_blob() to get these values; pass-in `idx` in range
1076 * [`0`, `batch->n_used()`).
1077 * @param batch
1078 * Pointer (non-null, or behavior undefined) to Msg_batch_in impl storing the current batch state;
1079 * if the async-op results in no error (1+ message(s) received), `*batch` state is updated accordingly.
1080 * See above and/or concept Msg_batch_in docs for details.
1081 * @param assume_would_block
1082 * If `true`, for performance `*this` can assume the pipe is in would-block and proceed accordingly.
1083 * If `false` it cannot make this assumption, so in particular it must attempt to synchronously obtain
1084 * any available in-messages (or pipe error).
1085 * @param on_done_func
1086 * See above. This shall be invoked from an unspecified thread that is not the calling thread.
1087 * @return Same notes as for async_receive_native_handle().
1088 */
1089 template<typename Msg_resource, typename Task_err>
1091 Task_err&& on_done_func);
1092
1093 /**
1094 * In PEER state: Irreversibly enables a conceptual idle timer whose potential side effect is, once at least
1095 * the specified time has passed since the last received low-level traffic (or this call, whichever most
1096 * recently occurred), to emit the pipe-hosing error error::Code::S_RECEIVER_IDLE_TIMEOUT. The implementation
1097 * shall guarantee the observer idle timeout is at least the provided value but may exceed this value for
1098 * internal reasons, as long as it's by a less than human-perceptible period (roughly speaking -- milliseconds,
1099 * not seconds).
1100 *
1101 * If `*this` is not in PEER state (in particular if it is default-cted or moved-from), returns `false` immediately
1102 * instead and otherwise no-ops (logging aside). If idle_timer_run() has already been called successfully,
1103 * subsequently it will return `false` and no-op (logging aside).
1104 *
1105 * ### Important: Relationship between idle_timer_run() and async_receive_native_handle() ###
1106 * idle_timer_run() is optional: if you have not called it, then the following does not apply. If you *have* called
1107 * it:
1108 *
1109 * It will work usefully if and only if subsequently an async_receive_native_handle() is outstanding at least
1110 * once each `timeout`. Informally this means it's best to immediately issue async_receive_native_handle() --
1111 * unless one is already outstanding -- and then as soon as that completes (sans error) issue at least one more;
1112 * and so on. Why? What are we talking about? Simple: async_receive_native_handle() impl is *not* required
1113 * to read any more data from the low-level transport than is sufficient to satisfy the current
1114 * async_receive_native_handle() deficit. (If it were required to do so, it would've been required to store copies
1115 * of incoming meta-blobs when there's no user-request deficit; we've consciously avoided unnecessary copying.)
1116 * So while there's no async_receive_native_handle() outstanding, `*this` will be considered idle; and once
1117 * that continues long enough for `timeout` to be exceeded, the idle timer will hose the in-pipe.
1118 *
1119 * So: If you plan to use idle_timer_run(), then you need to be ~always async-receiving. Otherwise you'll risk
1120 * hitting idle-timeout, even as the other side diligently sends stuff (`auto_ping()` or otherwise).
1121 *
1122 * ### Error semantics ###
1123 * If and only if the timeout does occur down the line, the aforementioned error will be emitted via
1124 * async_receive_native_handle() (or similar) handler. It shall be treated as the reason to hose the pipe
1125 * (assuming it was not hosed by something else earlier).
1126 *
1127 * ### Thread safety ###
1128 * You may call this either from any thread including the unspecified thread running within another call's
1129 * `on_done_func()`. It is *not* required to be safe to call concurrently with any API, including this one
1130 * and the destructor, invoked on `*this`.
1131 *
1132 * ### Suggested use ###
1133 * Informally: There are roughly two approaches to using idle_timer_run().
1134 *
1135 * Firstly it can be used to gauge the state of the opposing process; if no auto-pings are arriving regularly,
1136 * then the opposing Native_handle_sender (or similar) must be zombified or overloaded. To use it in this
1137 * capacity, typically one must use Native_handle_sender::auto_ping() (or similar) on the opposing side.
1138 * Typically one would then leave `timeout` at its suggested default value.
1139 *
1140 * Secondly it can be used to more generally ensure some application algorithm on the opposing side (presumably
1141 * cooperating with the algorithm on the local side) is sending messages with expected frequency.
1142 * That is, one would not use `auto_ping()` but rather send their own messages in a way that makes sense for
1143 * the applications' algorithm.
1144 *
1145 * @param timeout
1146 * The idle timeout to observe.
1147 * The optional default is chosen by the impl to most reasonably work with the opposing `auto_ping()`
1148 * to detect a zombified/overloaded peer.
1149 * @return `false` if `*this` is not in PEER (connected, transmitting) state, or if already called successfully
1150 * in `PEER` state; otherwise `true`.
1151 */
1152 bool idle_timer_run(util::Fine_duration timeout = default_value);
1153
1154 /**
1155 * Returns the accumulated transport statistics as of this call.
1156 * If not in PEER state returns a zeroed-out stats object.
1157 *
1158 * @see #Native_handle_rcv_stats doc header about how to obtain a transport::stat::Blob_rcv_stats core from the
1159 * returned `struct`.
1160 *
1161 * Thread-safe: can be called concurrently with any other method including `async_receive_*()`.
1162 * The returned copy is a consistent snapshot.
1163 *
1164 * @return Stats snapshot by value.
1165 */
1167
1168 /**
1169 * Resets the transport statistics as of this call. The formal meaning of a reset is discussed in
1170 * `flow::util::stat` doc header. If not in PEER state this is a no-op.
1171 *
1172 * Thread-safe: can be called concurrently with any other method including `async_receive_*()`.
1173 */
1175}; // class Native_handle_receiver
1176
1177/**
1178 * A documentation-only *concept* defining the behavior of an object that is a key aspect of batch-receiving
1179 * functionality, namely as arg to Native_handle_receiver::async_receive_native_handle_batch()
1180 * (if t-param #S_NO_HNDLS is `false`) or Blob_receiver::async_receive_blob_batch()
1181 * (if `NO_HNDLS == true`).
1182 *
1183 * @see Start by reading the Native_handle_receiver concept class doc header "Batch-receiving" section.
1184 * Note that Native_handle_receiver::Native_handle_batch_in, accordingly, shall point to an impl of
1185 * this concept's variety `Msg_batch_in<..., false>`. Similarly Blob_receiver::Blob_batch_in shall
1186 * point to an impl of `Msg_batch_in<..., true>`.
1187 *
1188 * ### Background / How to use ###
1189 * This assumes you've read the aforementioned "Batch-receiving" section in this file. So with that:
1190 * -# Construct a `*this`, and supply the value N (at least 1) as an arg. This determines permanently the
1191 * max in-batch size supported.
1192 * -# Prep each of the N slots: call `prepare_target_payload(target_blob, std::move(msg_resource))` N times.
1193 * `target_blob` is exactly analogous to arg `target_blob` for
1194 * Blob_receiver::async_receive_blob(). That is it is the memory area into which to write
1195 * a potential in-message's blob aspect.
1196 * - `target_blob.size()` must be the same for every prepare_target_payload() on a given `*this`. Otherwise
1197 * behavor is undefined.
1198 * - This value can be obtained via accessor target_payload_size().
1199 * - We discuss `msg_resource` separately below. (Spoiler alert: typically this is, or includes, the backing
1200 * container (e.g., a `flow::util::Basic_blob`) for `target_blob` memory area.)
1201 * -# (Now initialized() shall be `true`, n_used() shall be zero.) You're ready to receive.
1202 * -# Let `auto assume_would_block = false`.
1203 * -# Invoke Native_handle_receiver::async_receive_native_handle_batch() (if `S_NO_HNDLS == false`)
1204 * or Blob_receiver::async_receive_blob_batch() (otherwise), passing in `this` as the key arg.
1205 * - Set arg `assume_would_block` according to this value in the present algorithm.
1206 * -# On success n_used() shall be between 1 and N. For each `idx` in [`0`, `n_used()`):
1207 * -# (Only if `S_NO_HNDLS == false`) Obtain the native-handle part of the in-message via
1208 * `result_payload_hndl(idx)`. Note it may be `.null()` (no handle in message).
1209 * -# Consume the blob part of the in-message by calling `auto K = this->result_payload_blob(idx, ...)`.
1210 * Its location shall be at `target_blob.data()` and shall be `K` bytes long (where `K <= target_blob.size()`).
1211 * Typically you'd also *consume* the `msg_resource` at that slot (we discuss `msg_resource` separately below).
1212 * (Spoiler alert: that's handled via the 2nd arg `...`.)
1213 * -# Re-prepare the slot for the next batch-receive:
1214 * `prepare_target_payload(target_blob, std::move(msg_resource), idx)`, just like was done in the initial prep N
1215 * times, but this time provide the `idx` arg.
1216 * -# Set `assume_would_block = !this->full()`. (If fewer than N slots were filled, then in-pipe is in would-block.
1217 * Otherwise it may or may not in would-block, and we should try another batch-read.)
1218 * -# clear_used(); so n_used() shall be zero again.
1219 * -# Go again: Go to step 5.
1220 *
1221 * ### Slots may be reordered ###
1222 * `async_receive_native_handle_batch()` and `async_receive_blob_batch()` are formally allowed to reorder the slots
1223 * in a `*this`. That is why the size of each slot's `target_blob` must be the same for a given `*this`, and why
1224 * the `msg_resource` in that slot shall include the backing container for the memory area `target_blob`.
1225 *
1226 * ### How to use #Msg_resource ###
1227 * Formally it is an efficiently movable type of your choice. It should include, or simply be, a backing container for
1228 * that slot's `target_blob`.
1229 *
1230 * A high-performance choice for #Msg_resource is `flow::util::Blob_sans_log_context`. We do not recommend wrapping
1231 * it in a `unique_ptr` or similar; moves of `Basic_blob`s are cheap, and this avoids additional use of heap.
1232 *
1233 * Thus, let `K` be your choice of universal per-in-message max in-blob size. Then for each slot you initially prep
1234 * you'd do something close to:
1235 *
1236 * ~~~
1237 * Blob_sans_log_context blob{K};
1238 * batch_in.prepare_target_payload(blob.mutable_buffer(), std::move(blob));
1239 * ~~~
1240 *
1241 * ...and when consuming the result from slot `idx`:
1242 *
1243 * ~~~
1244 * Blob_sans_log_context* blob_ptr;
1245 * const auto n_rcvd = batch_in.result_payload_blob(idx, &blob_ptr);
1246 * blob_ptr->resize(n_rcvd);
1247 * Blob_sans_log_context result_blob{std::move(*blob_ptr)};
1248 * // Re-prep the slot for the next batch-receive.
1249 * Blob_sans_log_context blob{K};
1250 * batch_in.prepare_target_payload(blob.mutable_buffer(), std::move(blob), idx);
1251 * // result_blob is the in-blob for this slot. Do with it what we will.
1252 * ~~~
1253 *
1254 * You have significant flexibility as to what #Msg_resource actually is, however; the above is a minimal example
1255 * given our suggested (but by no means required) choice, `flow::util::Blob_sans_log_context`.
1256 *
1257 * @see Generic_msg_batch_in implements this concept and is used when a particular Native_handle_receiver or
1258 * Blob_receiver impl must use *emulated* (as opposed to native) batching. More usefully (for perf)
1259 * Native_socket_stream_batch implements *native* batching for use with Native_socket_stream; and is specifically
1260 * set as Native_socket_stream::Native_handle_batch_in if Native_socket_stream_cfg::S_USE_OS_DGRAM_SUPPORT and
1261 * Native_socket_stream_cfg::S_USE_OS_DGRAM_BATCH_SUPPORT are both `true`. This shall be the case at least
1262 * with modern Linuxes. Otherwise... Generic_msg_batch_in to the rescue.
1263 *
1264 * @tparam Msg_resource_t
1265 * See the above section about using #Msg_resource.
1266 * @tparam NO_HNDLS
1267 * To be set to `false` if and only if you shall be trafficking in `Native_handle`s (and blobs) as opposed to
1268 * mere blobs. In other words, `true` means the following method shall not be instantiated:
1269 * result_payload_hndl(). In still other words, `true` means you are
1270 * working with a Blob_receiver; else a Native_handle_receiver.
1271 */
1272template<typename Msg_resource_t, bool NO_HNDLS>
1274 private boost::noncopyable
1275{
1276public:
1277 // Types.
1278
1279 /// Alias for template parameter.
1280 using Msg_resource = Msg_resource_t;
1281
1282 /**
1283 * Buffer(s) description type (conforming to boost.asio `MutableBufferSequence` concept) supported by `*this`.
1284 * In this concept that is always `util::Blob_mutable` (a single buffer). In the future this might become
1285 * a template parameter instead, if and only if Native_handle_receiver and Blob_receiver are extended to
1286 * support scatter/gather. As of this writing that is a to-do noted elsewhere.
1287 */
1289
1290 // Constants.
1291
1292 /// Constant mirroring template parameter.
1293 static constexpr bool S_NO_HNDLS = NO_HNDLS;
1294
1295 // Constructors/destructor.
1296
1297 /**
1298 * Constructs batch with `max_msg_count` receive-message slots. Use prepare_target_payload() with `idx == -1`
1299 * exactly `max_msg_count` times before any actually-useful operations (`async_receive_*_batch()` on the daddy
1300 * receiver).
1301 *
1302 * @param max_msg_count
1303 * The value of n_used() when full().
1304 */
1305 explicit Msg_batch_in(size_t max_msg_count);
1306
1307 /// Clears all resources -- including any #Msg_resource currently attached to any receive-slot.
1309
1310 // Methods.
1311
1312 /**
1313 * Returns `true` if and only if `*this` is ready for actual batch-receive ops -- that is
1314 * prepare_target_payload() with `idx == -1` has been called exactly `max_msg_count` (see ctor) times.
1315 *
1316 * @return See above.
1317 */
1318 bool initialized() const;
1319
1320 /**
1321 * In range [0, `max_msg_count`] (see ctor), this is the number of slots, starting with the 1st slot (`[0]`),
1322 * currently marked as storing a received message each. result_payload_blob() and similar result-accessors
1323 * take `idx` only strictly less than this value.
1324 *
1325 * It is meaningless until `initialized() == true`; incremented by the batch-receive op
1326 * (`async_receive_*_batch()`); and reset to zero by clear_used().
1327 *
1328 * @return See above.
1329 */
1330 size_t n_used() const;
1331
1332 /**
1333 * Meaningful only if n_used() is, this returns `true` if and only if `n_used() == max_msg_count`, where
1334 * `max_msg_count` is the max-slot-count value provided to ctor.
1335 *
1336 * ### Implied would-block ###
1337 * From the user's point of view, following a batch-receive operation such as
1338 * Blob_receiver::async_receive_blob_batch(), if `n_used() >= 1` *and* `full() == false`, then the
1339 * condition we call *implied would-block* is in effect. Understanding and using it may be important
1340 * for performance. To wit:
1341 *
1342 * Implied would-block means that even though, yes, we were able to get 1+ in-messages in the last batch-read --
1343 * therefore the result was *not* would-block, but --
1344 * we were not able to fill `*this` fully; therefore it is known that no more in-messages pend. If the goal
1345 * is to always drain the in-pipe (which is the case in edge-triggered-poll-based event loops typically), then
1346 * in this case you *should nevertheless not* try another read: it will result in would-block and would be wasteful.
1347 *
1348 * However in the presence of `full()`ness (*no* implied would-block), to properly move toward draining in-pipe,
1349 * you *should* try another read (and then check `full()`ness again, etc.). Peforming an async-wait for readability
1350 * instead would be wasteful. Hence in `async_receive_*_batch()`, set arg `assume_would_block = !this->full()`.
1351 *
1352 * Corollary: Consider the case where `n_used() == 1` and `full() == true`. That means the batch-size is at most
1353 * 1; therefore implied would-block is *never* possible; for `n_used() == 0` just means *actual* would-block.
1354 * And indeed: In traditional, non-batched receive algorithms, one typically simply keeps trying 1-message reads
1355 * until one returns would-block. This is a nice mental sanity-check to pass.
1356 *
1357 * @return See above.
1358 */
1359 bool full() const;
1360
1361 /**
1362 * Makes it so that n_used() would return zero. Typically used after a read-op, consuming all n_used() in-messages,
1363 * and lastly performing prepare_target_payload() for each of the latter.
1364 */
1366
1367 /**
1368 * Either prepares-for-reading-to a reserved (in ctor) but not yet initialized slot; or re-prepares an
1369 * already so-initialized (typically also recently received-to) slot.
1370 *
1371 * If initialized() is `false`, and `idx == -1` (default), then: The next uninitialized slot is initialized.
1372 *
1373 * Otherwise: The specified slot, which must already be initialized, is re-prepared. Any #Msg_resource currently
1374 * attached to that slot is destroyed.
1375 * - It is possible to re-prepare any slot at any time.
1376 * - It is more typical, however, to do so specifically after:
1377 * -# A slot is received-into via `async_receive_*_batch()`.
1378 * -# You examine that slot via `result_payload_*()` and move-away its attached #Msg_resource (hence it is
1379 * now as-if default-cted).
1380 *
1381 * @param target_blob
1382 * Non-empty (else behavior undefined) location/size of a buffer in memory into which this slot's
1383 * message shall be potentially received. `target_blob.size()` shall always be the same for a given
1384 * `*this`, or behavior is undefined. (In other words, at entry, either target_payload_size() is zero,
1385 * or `target_payload_size() == target_blob.size()` must hold.)
1386 * @param msg_resource
1387 * The resource to attach (store via move) to this slot. Typically that will include or be a backing container
1388 * (such as `flow::util::Basic_blob`) that includes at least the location described by `target_blob`.
1389 * (Other information/data may also be included.)
1390 * @param idx
1391 * `-1` to prepare an uninitialized slot (`initialized() == false` must be the case, else behavior
1392 * is undefined/assertion may trip); otherwise the slot to re-prepare.
1393 */
1394 void prepare_target_payload(const Mutable_buffer_sequence& target_blob, Msg_resource&& msg_resource,
1395 size_t idx = -1);
1396
1397 /**
1398 * Returns the total max-size of each slot's message, or zero if prepare_target_payload() has not yet been called.
1399 * @return See above.
1400 */
1401 size_t target_payload_size() const;
1402
1403 /**
1404 * Obtains information, chiefly the size of received data not-exceeding corresponding `target_blob.size()`
1405 * from prepare_target_payload() but also optionally pointer to attached #Msg_resource, about an earlier-received-to
1406 * slot.
1407 *
1408 * The method is `const` (non-mutating) unless optional `msg_resource_ptr` arg is supplied (non-null).
1409 *
1410 * @param idx
1411 * Which slot? This shall be `< n_used()`; otherwise behavior undefined.
1412 * @param msg_resource_ptr
1413 * If not null then pointee `*msg_resource_ptr` is set to point to the #Msg_resource stored into this
1414 * slot by the last prepare_target_payload(). It is typical to then move-away `move(**msg_resource_ptr)`
1415 * into a #Msg_resource object maintained by the caller.
1416 * @return Byte count of the message last received-to the `idx`th slot.
1417 */
1418 size_t result_payload_blob(size_t idx, Msg_resource** msg_resource_ptr = nullptr);
1419
1420 /**
1421 * Obtains a copy of the `Native_handle` (potentially `.null()`) in an earlier-received-to slot.
1422 *
1423 * Method exists only if #S_NO_HNDLS is `false`.
1424 *
1425 * @param idx
1426 * See result_payload_blob().
1427 * @return See above.
1428 */
1430}; // class Msg_batch_in
1431
1432} // namespace ipc::transport
1433
1434#endif // ifdef IPC_DOXYGEN_ONLY
A documentation-only concept defining the behavior of an object that is a key aspect of batch-receivi...
bool initialized() const
Returns true if and only if *this is ready for actual batch-receive ops – that is prepare_target_payl...
void prepare_target_payload(const Mutable_buffer_sequence &target_blob, Msg_resource &&msg_resource, size_t idx=-1)
Either prepares-for-reading-to a reserved (in ctor) but not yet initialized slot; or re-prepares an a...
void clear_used()
Makes it so that n_used() would return zero.
bool full() const
Meaningful only if n_used() is, this returns true if and only if n_used() == max_msg_count,...
Native_handle result_payload_hndl(size_t idx) const
Obtains a copy of the Native_handle (potentially .null()) in an earlier-received-to slot.
size_t result_payload_blob(size_t idx, Msg_resource **msg_resource_ptr=nullptr)
Obtains information, chiefly the size of received data not-exceeding corresponding target_blob....
~Msg_batch_in()
Clears all resources – including any Msg_resource currently attached to any receive-slot.
size_t target_payload_size() const
Returns the total max-size of each slot's message, or zero if prepare_target_payload() has not yet be...
Msg_batch_in(size_t max_msg_count)
Constructs batch with max_msg_count receive-message slots.
util::Blob_mutable Mutable_buffer_sequence
Buffer(s) description type (conforming to boost.asio MutableBufferSequence concept) supported by *thi...
size_t n_used() const
In range [0, max_msg_count] (see ctor), this is the number of slots, starting with the 1st slot ([0])...
Msg_resource_t Msg_resource
Alias for template parameter.
static constexpr bool S_NO_HNDLS
Constant mirroring template parameter.
A documentation-only concept defining the behavior of an object capable of reliably/in-order receivin...
Native_handle_receiver(Native_handle_receiver &&src)
Move-constructs from src; src becomes as-if default-cted (therefore in NULL state).
bool async_receive_native_handle_batch(Native_handle_batch_in< Msg_resource > *batch, bool assume_would_block, Task_err &&on_done_func)
In PEER state: Asynchronously awaits 1+ discrete message(s) – as sent by the opposing peer via Native...
bool idle_timer_run(util::Fine_duration timeout=default_value)
In PEER state: Irreversibly enables a conceptual idle timer whose potential side effect is,...
Native_handle_receiver & operator=(const Native_handle_receiver &)=delete
Disallow copying.
static constexpr bool S_META_BLOB_UNDERFLOW_ALLOWED
If false then meta_blob.size() < receive_meta_blob_max_size() in PEER-state async_receive_native_hand...
value Native_handle_rcv_stats
Return type, typically a struct, for native_handle_receive_stats().
Native_handle_receiver()
Default ctor: Creates a peer object in NULL (neither connected nor connecting) state.
Native_handle_receiver & operator=(Native_handle_receiver &&src)
Move-assigns from src; *this acts as if destructed; src becomes as-if default-cted (therefore in NULL...
static const Shared_name S_RESOURCE_TYPE_ID
Shared_name relative-folder fragment (no separators) identifying this resource type....
static constexpr size_t S_RCV_NATIVE_HANDLE_BATCH_SZ_RECOMMENDATION
A positive number: the suggested value to pass for the max_msg_count arg to Msg_batch_in ctor of obje...
Native_handle_receiver(sync_io::Native_handle_receiver &&sync_io_core_in_peer_state_moved)
sync_io-core-adopting ctor: Creates a peer object in PEER state by subsuming a sync_io core in that s...
void native_handle_receive_stats_reset()
Resets the transport statistics as of this call.
bool async_receive_native_handle(Native_handle *target_hndl, const util::Blob_mutable &target_meta_blob, Task_err_sz &&on_done_func)
In PEER state: Asynchronously awaits one discrete message – as sent by the opposing peer via Native_h...
Native_handle_rcv_stats native_handle_receive_stats() const
Returns the accumulated transport statistics as of this call.
~Native_handle_receiver()
Destroys this peer endpoint which will end the conceptual incoming-direction pipe (in PEER state,...
size_t receive_meta_blob_max_size() const
In PEER state: Returns min target-meta-blob size such that (1) async_receive_native_handle() shall no...
Native_handle_receiver(const Native_handle_receiver &)=delete
Disallow copying.
A documentation-only concept defining the behavior of an object capable of reliably/in-order sending ...
Native_handle_snd_stats native_handle_send_stats() const
Returns the accumulated transport statistics as of this call.
Native_handle_sender(Native_handle_sender &&src)
Move-constructs from src; src becomes as-if default-cted (therefore in NULL state).
void native_handle_send_stats_reset()
Resets the transport statistics as of this call.
bool async_end_sending(Task_err &&on_done_func)
Equivalent to send_native_handle() but sends a graceful-close message instead of the usual payload; t...
Native_handle_sender(const Native_handle_sender &)=delete
Disallow copying.
Native_handle_sender(sync_io::Native_handle_sender &&sync_io_core_in_peer_state_moved)
sync_io-core-adopting ctor: Creates a peer object in PEER state by subsuming a sync_io core in that s...
size_t send_meta_blob_max_size() const
In PEER state: Returns max meta_blob.size() such that send_native_handle() shall not fail due to too-...
Native_handle_sender & operator=(Native_handle_sender &&src)
Move-assigns from src; *this acts as if destructed; src becomes as-if default-cted (therefore in NULL...
static const Shared_name S_RESOURCE_TYPE_ID
Shared_name relative-folder fragment (no separators) identifying this resource type....
bool send_native_handle(Native_handle hndl_or_null, const util::Blob_const &meta_blob, Error_code *err_code=nullptr)
In PEER state: Synchronously, non-blockingly sends one discrete message, reliably/in-order,...
bool auto_ping(util::Fine_duration period=default_value)
In PEER state: Irreversibly enables periodic auto-pinging of opposing receiver with low-level message...
Native_handle_sender & operator=(const Native_handle_sender &)=delete
Disallow copying.
value Native_handle_snd_stats
Return type, typically a struct, for native_handle_send_stats().
~Native_handle_sender()
Destroys this peer endpoint which will end the conceptual outgoing-direction pipe (in PEER state,...
bool end_sending()
Equivalent to async_end_sending(F) wherein F() does nothing.
Native_handle_sender()
Default ctor: Creates a peer object in NULL (neither connected nor connecting) state.
A documentation-only concept defining the behavior of an object that is the sync_io-pattern counterpa...
A documentation-only concept defining the behavior of an object that is the sync_io-pattern counterpa...
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
Flow-IPC module providing transmission of structured messages and/or low-level blobs (and more) betwe...
boost::asio::mutable_buffer Blob_mutable
Short-hand for a mutable blob somewhere in memory, stored as exactly a void* and a size_t.
Definition: util_fwd.hpp:137
flow::Fine_duration Fine_duration
Short-hand for Flow's Fine_duration.
Definition: util_fwd.hpp:114
boost::asio::const_buffer Blob_const
Short-hand for an immutable blob somewhere in memory, stored as exactly a void const * and a size_t.
Definition: util_fwd.hpp:131
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.