Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
asio_local_stream_socket.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#pragma once
20
24#include "ipc/util/util_fwd.hpp"
25#include <flow/error/error.hpp>
26#include <flow/log/log.hpp>
27#include <flow/common.hpp>
28#include <boost/array.hpp>
29#include <boost/io/ios_state.hpp>
30#include <boost/noncopyable.hpp>
31#include <type_traits>
32#include <algorithm>
33#include <stdexcept>
34#include <cstring>
35#include <cassert>
36#include <cerrno>
37#include <cstdlib>
38#include <ostream>
39#include <utility>
40#include <vector>
41#include <sys/types.h>
42#include <sys/socket.h>
43
44#ifndef FLOW_OS_LINUX // Sanity-re-check. We'll be sending sockets through sockets, etc., which requires Linux.
45static_assert(false, "Should not have gotten to this line; should have required Linux; this header assumes it. "
46 "Might work in other POSIX OS (e.g., macOS) but must be checked/tested.");
47#endif
48
49// See asio_local_stream_socket_fwd.hpp for doc header (intro) to this namespace.
51{
52
53// Types.
54
55#ifndef FLOW_OS_LINUX
56static_assert(false, "asio_local_stream_socket::Msg_batch_in is based on Linux-specific recvmmsg() API.");
57#endif
58
59/**
60 * Similarly to the transport::Msg_batch_in concept (as taken by API
61 * `{Blob|Native_handle}_receiver::async_receive_*_batch()` concept) implements persistent-across-reads
62 * batched-receipt of messages over an `asio_local_stream_socket::Peer_socket<Protocol_pkt_stream>`.
63 * There is a heavy focus on performance, including across multiple batched receives (nb_read()) on the same
64 * `*this` without linear-time prep work ahead of each such receive op.
65 *
66 * @see Native_handle_receiver concept doc header for an explanation of receive-batching (including what
67 * native batching is; how `Msg_resource_t` template-param relates to things); and how the general design
68 * enables high perf.
69 *
70 * ### Rationale ###
71 * Formally speaking this is a stand-alone class template, as opposed to an implementation of any particular
72 * concept. (See Native_socket_stream_msg_batch_in for the `Msg_batch_in` concept impl for
73 * Native_socket_stream. As noted below it in fact uses a `*this` most centrally in its impl.) Put simply,
74 * it (1) prepares the data structures needed for -2-; (2) wrangles `recvmmsg()` or equivalent; and (3) maintains
75 * the aforementioned data structures, so that repetitions of -2- can be done with almost zero additional
76 * work (and certainly not linear in batch-size). In order to do something fancier, such as actually being
77 * suitable as main arg `batch` to sync_io::Native_socket_stream::async_receive_blob_batch(), it would need to
78 * speak a more advanced protocol rather than what it does speak... which is simply "receive whatever arrives
79 * and save it for the user to access." So why is it a public API? Answer: For the same reason, e.g.,
80 * nb_read_some_with_native_handle() (and other public members of the ipc::transport::asio_local_stream_socket
81 * namespace) are public: It may well prove useful for low-level work directly on raw local sockets.
82 * (That said, informally, we expect most users to use the somewhat higher-level Native_socket_stream
83 * instead -- or something even higher-level such as struc::Channel.)
84 *
85 * Please note that we have intentionally made the receiver-engine-facing API, namely nb_read() and
86 * reuse_result_payloads(), publicly accessible, again due to the above reasoning. Again the idea
87 * is that conceivably the Flow-IPC user could make use of it,
88 * as it is fairly general. As of this writing, however, only other Flow-IPC internal code (namely
89 * sync_io::Native_socket_stream) uses this sub-API; but that need not be the case.
90 *
91 * ### Details ###
92 * As of this writing this is internally done, specifically, via Linux-specific `recvmmsg()` API operating on a
93 * `SOCK_SEQPACKET` connection (hence the aforementioned #Protocol_pkt_stream). When/if support for non-Linux is
94 * added, this class (or hypothetical similar one) could support any equivalent from other OS.
95 *
96 * This implementation is fairly general, down to allowing an arbitrary (albeit mandatorily consistent across a given
97 * `*this` lifetime) #Mutable_buffer_sequence type. (E.g., could provide a simple util::Blob_mutable
98 * or an `array<Blob_mutable>` or `vector` thereof, etc.) The main limitation is one shared with the rest
99 * of transport::asio_local_stream_socket API -- as informed by the needs of the rest of Flow-IPC -- in that
100 * in addition to arbitrary binary data blobs it supports the transmission of `Native_handle`s, namely either zero
101 * or one such items per in-message. As usual a given in-message may contain 0+ bytes of blob data and 0 or 1 handles;
102 * but never 0 bytes and 0 handles (this is treated as a fatal error).
103 *
104 * It features the following APIs as of this writing:
105 *
106 * - For user (including internal user struc::sync_io::Channel): `size_t max_msg_count`-taking ctor,
107 * initialized(), n_used(), full(), clear_used(), prepare_target_payload(), result_payload_blob(),
108 * result_payload_hndl() (for Native_handle_receiver only).
109 * - For higher-level receiver engine (such as sync_io::Native_socket_stream + Native_socket_stream_msg_batch_in
110 * working in concert): nb_read(), reuse_result_payloads().
111 * - The expected order of ops for each batched-read (after reaching initialized() originally) is:
112 * -# (Usually) Higher-level user invokes Blob_receiver::async_receive_blob_batch() or
113 * Native_handle_receiver::async_receive_native_handle_batch() which internally invokes:
114 * -# Core batch-receive: nb_read().
115 * -# Filter-out control messages without destroying them: reuse_result_payloads()
116 * (if applicable depending on your higher-level protocol if any).
117 * - Harvest any control-message info among the filtered-out in-messages.
118 * -# User consumes results: result_payload_blob() and possibly result_payload_hndl().
119 * -# User resets for next batch-read:
120 * -# Save N = n_used().
121 * -# clear_used().
122 * -# prepare_target_payload() (for each of the first N slots only).
123 *
124 * The algorithmic centerpieces of a `*this` are as follows. Note that, as usual in this context, this info is
125 * of interest not to the eventual user doing higher-level receiving via Blob_receiver and the like; but implementers
126 * of specific `Blob_receiver`s (et al). So we have:
127 *
128 * - nb_read() which internally performs `recvmmsg()` (or equivalent op in other OS).
129 * - prepare_target_payload() and result_payload_blob() et al interact with it to maintain high perf.
130 * - reuse_result_payloads() which helps (at least Native_socket_stream_msg_batch_in but conceivably others)
131 * support higher-level protocols with both user in-messages and control messages (e.g., auto-ping, graceful-close)
132 * while maintaining high perf.
133 *
134 * ### Regarding structure of each target-blob area(s) user supplies to prepare_target_payload() ###
135 * You can skip this section if you promise to, in every prepare_target_payload() call for a given `*this`,
136 * supply the same number of scatter/gather buffers of the same respective lengths. (E.g.: always a 2-buffer and
137 * a 4096-buffer.)
138 *
139 * In general you *can* provide a different structure (e.g. 1 2-byte buffer and 3 1-byte buffers; or just 1 7-byte
140 * buffer; or...) to each prepare_target_payload() versus the others. It is conceivable to make use of this for
141 * some real purpose. However:
142 *
143 * - Informally: one typically doesn't know (at this level) what's coming, so it is probably correct to provide, at
144 * least, the same total number of target bytes for each prepare_target_payload(). It is also likely more
145 * natural to have the same # and length-order of scatter/gather buffers across which to spread these bytes;
146 * though technically doing otherwise could be fine (just... why bother?).
147 * - This is even more-so if you plan to use reuse_result_payloads() which shall potentially swap slot order
148 * which would probably make keeping-up with what happened hard-to-impossible, unless each slot is structurally
149 * identical to all others.
150 *
151 * @internal
152 *
153 * ### Impl notes ###
154 * Generally the details here are informed by the somewhat higher-level needs of Native_socket_stream and
155 * Native_socket_stream_msg_batch_in which implement the protocol used by the former. As such that protocol is
156 * close to -- but not quite the same as -- simply supporting arbitrary in-datagrams (each with an optional
157 * native-handle), which is what a `*this` supplies.
158 *
159 * The devil is really in the details, while being driven by the basic receive-batching design described in
160 * Native_handle_receiver concept doc header. The basic point is, after invoking ctor and N prepare_target_payload(),
161 * subsequent per-batched-read (nb_read()) are not linear-time, so that:
162 *
163 * - each actually received-to slot will understandably need processing;
164 * - but all others require little to no reset or post-processing.
165 *
166 * Thus we get the benefits of receiving potentially many in-messages via 1 OS-call; while requiring ~no extra
167 * compute when compared with a conventional non-batched series of N single-message-receives-until-would-block.
168 *
169 * `recvmmsg()` makes this achievable, though far from trivial. To summarize:
170 *
171 * - `recvmmsg()` takes an array of `mmsghdr`, each of which contains an `msghdr` (identical to what `recvmsg()`
172 * takes one of) and `.msg_len`; and returns the number of `mmsghdr`s that were actually received-to;
173 * while each individual `.msg_len` holds the # of bytes received for that in-message (identical to what
174 * `recvmsg()` would have returned).
175 * - The `msghdr` has a pointer/length w/r/t a scatter/gather array of `iovec` structures in heap, each storing
176 * a location/size, being conceptually identical to our util::Blob_mutable.
177 * - It also has a pointer/length w/r/t a complex *ancillary data* structure in heap (we use it for 0-1 FDs).
178 * - If a given slot was not read-to by a `recvmmsg()`, then its `mmsghdr` array entry is untouched and can be
179 * reused as-is.
180 * - If a given slot *was* read-to, but via reuse_result_payloads() it is determined that it contained no
181 * user in-message but merely what the receiver-engine considers a control/metadata in-message -- not to be
182 * passed to the user as such -- then that `mmsghdr` (and related) array entry can also be reused as-is... almost.
183 * A couple of items do need to be set again; you'll see. In any case these should be very cheap and relatively
184 * rare anyway.
185 * - If it was read-to, and it was indeed a user in-message, then naturally prepare_target_payload() has to be
186 * re-invoked. However you'll see that even this requires only a certain subset of the work compared to
187 * the one-time initializing prepare_target_payload().
188 * - For our part we make use of this as follows:
189 * - We store the array of `mmsghdr`s, which is continually received-to, re-primed, received-to, re-primed, ....
190 * This is #m_mmsg_hdrs, a `vector`.
191 * - All other info, including the aforementioned `iovec`-array and ancillary-data
192 * `mmsghdr`-member-pointer-pointees, is stored in #m_mdts, a structurally identical `vector` (same
193 * size, capacity, and index order as `m_mmsg_hdrs`).
194 * - So slot indexed `idx` exactly consists of the two `struct`s `m_mmsg_hdrs[idx]` and `m_mdts[idx]`.
195 * - User's prepare_target_payload() calls sets them up until initialized() is true.
196 * - nb_read() and reuse_result_payloads() read into the first N slots, where N = # of pending messages capped
197 * at total # of slots we store.
198 * - User can consume any received-to slot `idx`'s data and then re-prepare that slot by supplying new target memory
199 * area(s). They do this by calling `prepare_target_payload(..., idx)`.
200 * - Any slots that were untouched by nb_read() or reuse_result_payloads() simply carry-over to the next nb_read().
201 * They need not be touched at all.
202 *
203 * @endinternal
204 *
205 * @tparam Msg_resource_t
206 * Same as for Generic_msg_batch_in. However, caution! Be very sure that whatever is stored here, if it
207 * provides the backing bytes for the #Mutable_buffer_sequence passed to the corresponding
208 * prepare_target_payload(), does *not* change location in memory if the `Msg_resource_t` is *moved*
209 * onto another `Msg_resource_t`. For example, this *does* hold for `flow::util::Blob` or `vector` or
210 * `unique_ptr<int>`; but *not* for `array` or `int`.
211 * @tparam Mutable_buffer_sequence_t
212 * See #Mutable_buffer_sequence.
213 */
214template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
216 private boost::noncopyable
217{
218public:
219 // Types.
220
221 /// Buffer(s) description type (conforming to boost.asio `MutableBufferSequence` concept) supported by `*this`.
222 using Mutable_buffer_sequence = Mutable_buffer_sequence_t;
223
224 /// Alias for template param `Msg_resource_t`.
225 using Msg_resource = Msg_resource_t;
226
227 // Constructors/destructor.
228
229 /**
230 * Identical to ctor of Generic_msg_batch_in.
231 * @param max_msg_count
232 * See above.
233 */
234 explicit Msg_batch_in(size_t max_msg_count);
235
236#ifdef IPC_DOXYGEN_ONLY
237 /// Clears all resources -- including any #Msg_resource currently attached to any receive-slot.
239#endif
240
241 // Methods.
242
243 /**
244 * Identical to Generic_msg_batch_in.
245 * @return See above.
246 */
247 bool initialized() const;
248
249 /**
250 * In range [0, `max_msg_count`] (see ctor), this is the number of slots, starting with the 1st slot (`[0]`),
251 * currently marked as storing a received message each.
252 *
253 * @note result_payload_blob() and similar result-accessors *do* take `idx >=` this value
254 * (cf. Generic_msg_batch_in). That is because of the possibility of reuse_result_payloads(); see that
255 * guy.
256 *
257 * It is meaningless until `initialized() == true`; incremented by nb_read(); decremented by reuse_result_payloads();
258 * and reset to zero by clear_used().
259 *
260 * @return See above.
261 */
262 size_t n_used() const;
263
264 /**
265 * Identical to Generic_msg_batch_in. That doc header is worth reading; especially see notes
266 * on *implied would-block*.
267 *
268 * @return See above.
269 */
270 bool full() const;
271
272 /**
273 * Makes it so that n_used() would return zero (or, in advanced form, another value). Typically used after an
274 * nb_read() before the next one.
275 *
276 * @param new_n_used
277 * The value n_used() shall return.
278 */
279 void clear_used(size_t new_n_used = 0);
280
281 /**
282 * Identical to Generic_msg_batch_in (though the type of `target_blob` is *potentially* more complex).
283 *
284 * @param target_blob
285 * See above.
286 * @param msg_resource
287 * See above.
288 * @param idx
289 * See above.
290 */
291 void prepare_target_payload(const Mutable_buffer_sequence& target_blob, Msg_resource&& msg_resource,
292 size_t idx = -1);
293
294 /**
295 * Obtains information, chiefly the size of received data `<=` total-size of corresponding `target_blob`
296 * from prepare_target_payload() but also optionally pointer to attached #Msg_resource, about an earlier-received-to
297 * slot.
298 *
299 * The method is `const` (non-mutating) unless optional `msg_resource_ptr` arg is supplied (non-null).
300 *
301 * @param idx
302 * Which slot? Nota bene: this method *does* allow `idx >= this->n_used()`
303 * (cf. Generic_msg_batch_in). Probably: `idx < this->n_used()` when scanning messages yielded by
304 * nb_read() and *not* "un-yielded" by reuse_result_payloads() (if any); and `idx >= this->n_used()`
305 * when scanning the messages (if any!) "un-yielded" by reuse_result_payloads() (if any). In the latter
306 * case presumably `idx` will not exceed the last such "un-yielded" message's index; but this is not
307 * detected let alone enforced. Use with care.
308 * @param msg_resource_ptr
309 * If not null then pointee `*msg_resource_ptr` is set to point to the #Msg_resource stored into this
310 * slot by the last prepare_target_payload(). It is typical to then move-away `move(**msg_resource_ptr)`
311 * into a Msg_resource object maintained by the caller.
312 * @return Byte count of the message last received-to the `idx`th slot.
313 */
314 size_t result_payload_blob(size_t idx, Msg_resource** msg_resource_ptr = nullptr);
315
316 /**
317 * Obtains a copy of the `Native_handle` (potentially `.null()`) in an earlier-received-to slot.
318 *
319 * @internal
320 *
321 * @todo Only copy-access to a slot's stored `Native_handle` is provided: Msg_batch_in::result_payload_hndl(); a
322 * receiver-engine rewinding n_used() (un-emitting slots) can thus close a stored handle (via the copy) but not
323 * nullify the stored value, leaving a stale (closed) handle value recorded in a meaningless-territory slot. For
324 * cleanliness/defensiveness an engine-facing API -- e.g., close-and-nullify over an index range -- might be
325 * helpful or required; see its would-be use in Native_socket_stream_msg_batch_in::nb_read() rewind logic.
326 * Suggest also applying identical changes to `Generic_msg_batch_in` API, `async_receive_batch_emulation()`
327 * doc header documenting the `tparam Batch`, and accordingly the rewind logic inside
328 * `async_receive_batch_emulation_on_init_msg()`. Both rewind spots as of this writing are marked with
329 * associated to-dos.
330 *
331 * @endinternal
332 *
333 * @param idx
334 * See result_payload_blob().
335 * @return See above.
336 */
338
339 /**
340 * Performs a high-perf non-blocking batched-receive into the batch area with indices
341 * [n_used(), `max_msg_count`). See ctor for the latter. Usually, but not necessarily, `this->n_used() == 0`,
342 * as usually one would clear_used() first, or `*this` was just initialized(). Returns `false` if
343 * the aforementioned range is empty -- that is full() is `true` and no-ops modulo logging; otherwise returns
344 * `true` and proceeds. Further documentation assumes the latter.
345 *
346 * Let `n_payloads` be the size of the aforementioned range (thus `n_payloads >= 1`). This receives
347 * at most `n_payloads` messages. If no error but no in-messages are pending, it is would-block
348 * (see specific code below). If no error, and messages were pending, the number is not explicitly returned
349 * but rather reflected in n_used() increasing by that # (at least 1, at most `n_payloads`). If error,
350 * and no messages were pending, emits that error and leaves n_used() unchanged.
351 *
352 * It is *not* a possible outcome that messages were emitted (n_used() changed) *and* an error is emitted.
353 * See section "Deferred errors" just below however.
354 *
355 * @note Be aware of the *implied would-block* condition. See full() doc header. Spoiler alert:
356 * if we yielded data/no error, then it wasn't would-block... but if `full() == false` post-op, then
357 * the in-pipe is nevertheless in would-block state. It would be wasteful to try another nb_read().
358 * Conversely, though, if `full()` then you can and should nb_read() again (assuming the goal is to
359 * drain the in-pipe such as in edge-triggered loops).
360 *
361 * ### Special graceful-close (a/k/a EOF) semantics ###
362 * nb_read_some_with_native_handle(), a-la Boost, emits `boost::asio::error::eof` on encountering graceful-close
363 * in the message stream -- never combined with emitting an in-message as well; so in a sense that "error" is
364 * an in-message itself (never followed by any more in-messages). This nb_read() never emits that code however.
365 *
366 * Instead: If and only if there is no error emitted (including would-block), and any pending in-messages (0 or more
367 * of them) are capped with graceful-close, then `*graceful_close` is set to `true`; else to `false`. The
368 * graceful-close "in-message" thus is treated semantically as an in-message and is always emitted if and only if it
369 * is pending. To recap the possible outcomes:
370 *
371 * - Socket-hosing error or `INVALID_ARGUMENT`: emitted via standard flow error semantics.
372 * - Subtle exception: error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE and
373 * error::Code::S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL are not socket-hosing. Further I/O
374 * can be attempted. However:
375 * - In-direction: Informally, it is ill-advised to continue work in this direction. In-messages potentially
376 * following the overflowing one, in this batch, would have been dropped. In most protocols it would be
377 * difficult to continue from this point.
378 * - Out-direction: Work really can continue. Informally, though, it might be ill-advised to take advantage
379 * of this -- but less so than in the previous bullet point.
380 * - No in-message(s) and no graceful-close pending: emitted via those same semantics as
381 * `boost::asio::error::would_block`.
382 * - Otherwise, at least 1 of (in-message, graceful-close) is pending. So:
383 * 0+ in-message(s) are emitted into `*this` (n_used() increases if 1+), and `*graceful_close` is assigned
384 * to `true` if graceful-close capped those 0+ in-messages; to `false` otherwise.
385 * - So: It is not possible that there is no error, n_used() did not increase (0 in-messages), *and*
386 * `*graceful_close = false` is set.
387 *
388 * @note Graceful-close, if detected (and it will be if indeed pending), is never deferred to the next nb_read().
389 * This is potentially important, if you're running an edge-triggered-poll (e.g., `EPOLLET` with `epoll_*()`)
390 * event loop. If we were to defer reporting a pending graceful-close, you'd likely invoke `epoll_wait()` (or
391 * equivalent), and it will probably hang, as the *readability edge* has already been consumed.
392 * (We've empirically confirmed this in Linux.)
393 *
394 * ### Edge-triggered-poll friendliness ###
395 * A key guarantee, for those who run an edge-triggered-poll event loop (e.g., `EPOLLET` with `epoll_*()`), is that
396 * *all* pending in-messages (counting graceful-close as in-message) are flushed into `*this` (and `*graceful_close`),
397 * up to `full() == true`. Thus, if you nb_read() repeatedly until would-block is emitted, or `!full()`
398 * (implied would-block), or socket-hosing error, and then await (edge-triggered) readability again, all will work
399 * well.
400 *
401 * ### Deferred errors: Advanced discussion ###
402 * (Some of this references implementation details. In this advanced context we deem it appropriate.)
403 *
404 * We've noted that we will never emit a
405 * (socket-hosing / `MESSAGE_SIZE_EXCEEDS_USER_STORAGE` / `LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL` /
406 * `INVALID_ARGUMENT`, non-would-block, non-graceful-close)
407 * error, if we do emit 1+ in-messages (let's count `*graceful_close = true` as an in-message for this discussion).
408 * What happens, though, if the socket has 1+ in-messages pending *and* an error following those in-messages? In
409 * Linux, as of this writing, at least in some mainstream versions, the `man recvmmsg` page says that in this case (it
410 * is even marked under "BUGS," probably out of caution more than anything) the error is not emitted; but any
411 * following `recv*()` call will emit the pending error. In short: we simply report (in this context) what the OS
412 * does, so if that happens, we'll simply emit the same stuff (in fact we have no good way of knowing it's happening,
413 * rather than the error simply happening a bit later).
414 *
415 * Not so fast, though: This opens the possibility of a subtle problem. To wit: if the OS knows an error is pending
416 * after 1+ in-messages, and `recvmmsg()` ignores it, but the next `recv*()` will report the error, then can't this
417 * break the edge-triggered-poll-event-loop-friendly semantics, wherein *all* in-data must be flushed before
418 * invoking another await-readability OS-call (`epoll_wait()` in Linux)? That is, an `epoll_wait()` for readability
419 * would hang in such a hypothetical scenario (if edge-triggered, `EPOLLET`; level-triggered is always safe).
420 *
421 * In short: As of this writing, in the case of Linux, it is not a problem in practice. So don't worry about it...
422 * but read on if you want to understand why/what is happening.
423 *
424 * Essentially it is not a problem simply because, in our case of dgram-based local IPC over Unix-domain sockets,
425 * specifically when reading, there simply *is* no error condition that can be "queued" after in-messages.
426 * Graceful-close is possible -- but we handle that, and it's not (internally in Linux) an error. `ECONNRESET`
427 * is not a thing in this context; `EPIPE` is for writing (not reading); others catastrophic problems such as
428 * `EBADF` are not "queued." With TCP, `ECONNRESET` would indicate an RST, but it's not a thing for us. With UDP,
429 * some sort of ICMP wackiness could maybe pop up -- but we aren't networking, and it's not UDP.
430 *
431 * That said, it is important to keep an eye on this (as Linux develops and/or we add other OS support); semantics
432 * could differ there. Even in Linux, we *did* empirically confirm that in general the situation wherein a
433 * socket is in queued-error state, and `recv*()` would yield an error (namely `ECONNRESET` due to queued RST
434 * intentionally sent by opposing side), then an `epoll_wait()` for readability, with `EPOLLET`, will hang and not
435 * re-report readability despite the error-state of the socket. Again, though, we could only force this in TCP, so
436 * it's not immediately relevant -- but it is conceivable it could become relevant. E.g., this code could be
437 * extended to newer Linux that act differently and/or other OS that act differently and/or support networked
438 * sockets (perhaps UDP).
439 *
440 * @param logger_ptr
441 * Logger (or null) to use for logging inside.
442 * @param peer_socket_ptr
443 * Peer socket over which to receive.
444 * @param graceful_close
445 * If no error is emitted, and we return `true`, then `*graceful_close` is assigned `true` if the 0+
446 * messages received (as reflected via n_used()) were followed by graceful-close in the in-message stream;
447 * to `false` if the 1+ messages received were not followed by graceful-close. Untouched otherwise.
448 * @param err_code
449 * See `flow::Error_code` docs for error reporting semantics. Generated codes:
450 * error::Code::S_INVALID_ARGUMENT (`!this->initialized()` at entry);
451 * `boost::asio::error::would_block` (socket not readable: not even 1 message or graceful-close pending);
452 * those emitted by nb_read_some_with_native_handle()
453 * (including error::Code::S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE)
454 * (except would-block which has different meaning as noted; and
455 * `boost::asio::error::eof` (graceful-close) which is communicated via
456 * `graceful_close` arg as noted).
457 * @param message_flags
458 * See nb_read_some_with_native_handle().
459 * @param total_rcvd_bytes_or_null
460 * If not null, `*total_rcvd_bytes_or_null` is set to the sum of bytes received (into user buffers) across all
461 * received datagrams. Useful for stats: this is total low-level bytes including all scatter/gather segments.
462 * Meaningless on error or would-block (left unset).
463 * @return `false` if pre-condition `!this->full()` is failed; `true` otherwise. See above.
464 */
465 bool nb_read(flow::log::Logger* logger_ptr, Peer_socket<Protocol_pkt_stream>* peer_socket_ptr, bool* graceful_close,
466 Error_code* err_code = nullptr, int message_flags = 0,
467 size_t* total_rcvd_bytes_or_null = nullptr);
468
469 /**
470 * Scans every in-message slot in range [`start_idx`, `this->n_used()`); performantly rearranges this range
471 * so that all in-messages for which `is_unused_func(M) == false` (if any + without mutual order changing) are
472 * at the start of the range, followed by the other ones (if any; no ordering guarantees); and adjusts
473 * (no-op or decreases) n_used() by the # of the `is_unused_func(M) == true` slots detected.
474 *
475 * @see Msg_batch_in class doc header for rationale/background/discussion. In short, though, the idea is
476 * `is_unused_func(M) == false` indicates a "user in-message" (worthy of remaining within the
477 * 0-to-`n_used()` range), while the converse indicates a control/metadatum message (which should
478 * be moved past the "used" range).
479 *
480 * Conceptually this is similar to `std::remove_if()`, except swaps -- instead of mere moves -- are used, thus
481 * ensuring the slots potentially change relative order, but the set of slots by value is unchanged; and
482 * n_used() changes instead of a returned iterator are the mechanism of communicating how many items are
483 * "filtered out."
484 *
485 * ### What to do with the filtered-out slots? ###
486 * One option is to do nothing. You can nb_read() again; these slots will be readable-into; no problem.
487 * So then you'd just consume the ones up to `this->n_used()` as normal, and that's that... ready to nb_read()
488 * again.
489 *
490 * Otherwise -- namely if the filtered-out messages potentially carry useful information, or something --
491 * you can do the first or possibly both of the following.
492 *
493 * - Scan them (even though they're past the first `n_used()` slots) via `result_payload_*()`.
494 * - Consume and re-prepare (prepare_target_payload()) them.
495 *
496 * Bottom line is, you can (but need not) do stuff to them just as you can do to the not-unused ones. Have care.
497 *
498 * @param start_idx
499 * Index (0-based) of first message to scan (see above). Typically this would be what n_used() returned
500 * before the preceding nb_read(). (Fittingly the last message to scan is indicated by
501 * current/post-`nb_read()` value of also n_used().)
502 * @param is_unused_func
503 * See above. We suggest this is written to be as performant as humanly possible. E.g. try to find
504 * the cheapest-and-likeliest conditions first and thus early-return whenever possible.
505 */
506 template<typename Is_unused_func>
507 void reuse_result_payloads(size_t start_idx, const Is_unused_func& is_unused_func);
508
509 /**
510 * Identical to Generic_msg_batch_in.
511 * @param os
512 * See above.
513 */
514 void to_ostream(std::ostream* os) const;
515
516private:
517 // Types.
518
519 /// Per-batch-slot information beyond what is stored in the corresponding slot's native `mmsghdr m_mmsg_hdrs[idx]`.
521 {
522 // Data.
523
524 /**
525 * Efficient (via adapt-as-we-go technique) object that adapts the boost.asio portable #Mutable_buffer_sequence
526 * (sequence of boost.asio util::Blob_mutable location/size structures) into the POSIX native `iovec` array
527 * containing the exact same information.
528 *
529 * The implementation is cleverly specialized, so that depending on what concrete type #Mutable_buffer_sequence
530 * actually is, the storage and access to the resulting `iovec`s is as performant as possible. In particular
531 * there are specializations for a single `Blob_mutable`; STL and Boost `array<2>` thereof; and (worst-case)
532 * variable-length things such as `vector<Blob_mutable>`. E.g., in Flow-IPC a major use-case is
533 * Native_socket_stream_msg_batch_in which in fact uses `Mutable_buffer_sequence = array<Blob_mutable, 2>`.
534 *
535 * ### Defense of using an internal (`detail`) boost.asio class ###
536 * Well, it's useful. We could roll "our own" by copy/pasting it instead, but in practice that's just
537 * busy-work. That said there is some risk of this changing under us with a newer Boost version; we accept this
538 * small risk.
539 */
540 boost::asio::detail::buffer_sequence_adapter<util::Blob_mutable, Mutable_buffer_sequence> m_buf_seq_adapter;
541
542 /// Per-slot resource moved from user's object via `prepare_target_payload()`.
544
545 /**
546 * The target/result `Native_handle` to receive-to for this slot; null originally; null or not upon
547 * a receive op into the slot.
548 */
551 /**
552 * Stores this slot's ancillary-data `cmsg*` structure, into which ancillary data if any are received.
553 *
554 * This must be the last member to avoid gcc error "flexible array member 'cmsghdr::__cmsg_data' not at end of ...";
555 * though we feel in our case it is safe due to measures taken inside Msg_control_as_union.
556 */
558
559 // Constructors/destructor.
560
561 /**
562 * Ctor.
563 * @param buf_seq
564 * The buffers.
565 * @param msg_resource
566 * Object to move-assign to #m_resource.
567 */
568 Mdt_per_payload(const Mutable_buffer_sequence& buf_seq, Msg_resource&& msg_resource);
569
570 // Methods.
571
572 /**
573 * Implements maximally efficient swapping, as needed by reuse_result_payloads(). The algorithm is very much
574 * specific to Msg_batch_in; it would be insufficient if used in some generic sense outside that context; so
575 * e.g. it assumes certain values *in our case* never change after prepare_target_payload().
576 *
577 * @param that
578 * Other guy to swap-with; does not equal `this`.
579 */
581 }; // struct Mdt_per_payload
582
583 // Constants.
584
585 /// Value we give for `msghdr::msg_flags` as in-arg. It is also an out-arg, so sometimes we must reassign this.
586 static constexpr int S_PER_MSG_FLAGS = 0;
587
588 // Data.
589
590 /**
591 * The slots, excluding what is in #m_mmsg_hdrs. `.capacity() == max_msg_count` (see ctor); `.size()` is same
592 * once initialized().
593 *
594 * ### Rationale: perf ###
595 * Note we store the `Mdt_per_payload`s "directly" in the `vector<>` as opposed to wrapping in `unique_ptr`
596 * handles or similar. (Similarly we don't wrap any member(s) inside in `unique_ptr` either.) Why? Answer:
597 * For perf... but how so? Answer: added heap-allocation and indirection (mostly the former) are relatively costly.
598 * On the other hand any moving of elements inside the `vector` would counteract this (somewhat, depending). Do
599 * we actually do that? We use `.reserve()` and a constant `.capacity()`, so with just prepare_target_payload()
600 * init and nb_read(), no, we don't move these elements as of this writing, with two exceptions. 1, there's
601 * the initial load via prepare_target_payload(). 2, *if* they choose to reuse_result_payloads(), then we do
602 * sometimes move (swap) them (albeit relatively rarely in practice). (Also the swap compute itself is fairly
603 * painstakingly minimal (see reuse_result_payloads()).)
604 *
605 * All in all, on researching this in some detail, it is cheaper than adding heap-indirection, particularly if
606 * #Msg_resource is efficiently-movable (usually it is: usually it'll be like a `vector<uint8_t>` move or cheaper).
607 *
608 * ### Corollary ###
609 * Because we made this choice, *and* because we take `Msg_resource&&` (not a handle thereto) in
610 * prepare_target_payload(), we add the requirement that any backing data structures provided by the user
611 * as #Msg_resource objects are such that their locations do *not* change when a `Msg_resource` is moved,
612 * if reuse_result_payloads() is ever used. See class doc header's template param docs, where we announce this
613 * requirement.
614 */
615 std::vector<Mdt_per_payload> m_mdts;
616
617 /**
618 * The slots, excluding what is in #m_mdts; a sub-array of this is passed by nb_read() to `recvmmsg()`.
619 * `.capacity()`, `.size()` shall always equal those of #m_mdts; and a given index in one refers to the same slot
620 * in the other.
621 */
622 std::vector<::mmsghdr> m_mmsg_hdrs;
623
624 /**
625 * See n_used(). Restating for informational convenience: meaningless until `initialized() == true`; incremented by
626 * nb_read(); decremented by reuse_result_payloads(); and reset to zero by clear_used().
627 */
628 size_t m_n_used;
629}; // class Msg_batch_in
630
631#ifndef FLOW_OS_LINUX
632static_assert(false, "Flow-IPC must define Opt_peer_process_credentials w/ Linux SO_PEERCRED semantics. "
633 "Build in Linux only.");
634#endif
635
636/**
637 * Gettable (read-only) socket option for use with asio_local_stream_socket::Peer_socket `.get_option()` in order to
638 * get the connected opposing peer process's credentials (PID/UID/GID/etc.). Note accessors and data are
639 * in non-polymorphic base util::Process_credentials.
640 *
641 * If one calls `X.get_option(Opt_peer_process_credentials& o)` on #Peer_socket `X`, and `X` is connected to opposing
642 * peer socket in process P, then `o.*()` credential accessors (such as `o.process_id()`) shall return values that were
643 * accurate about process P, at the time P executed `Peer_socket::connect()` or `local_ns::connect_pair()` yielding
644 * the connection to "local" peer `X`.
645 *
646 * @see boost.asio docs: `GettableSocketOption` in boost.asio docs: implemented concept.
647 *
648 * @internal
649 * This is the Linux `getsockopt()` option with level `AF_LOCAL` (a/k/a `AF_UNIX`), name `SO_PEERCRED`.
650 */
653{
654public:
655 // Constructors/destructor.
656
657 /// Default ctor: each value is initialized to zero or equivalent.
659
660 /**
661 * Boring copy ctor.
662 * @param src
663 * Source object.
664 */
666
667 // Methods.
668
669 /**
670 * Boring copy assignment.
671 * @param src
672 * Source object.
673 * @return `*this`.
674 */
676
677 /**
678 * For internal boost.asio use, to enable `Peer_socket::get_option(Opt_peer_process_credentials&)` to work.
679 *
680 * @see boost.asio docs: `GettableSocketOption::level()` in boost.asio docs: implemented concept.
681 *
682 * @tparam Protocol
683 * See concept API.
684 * @param proto
685 * See concept API.
686 * @return See concept API.
687 *
688 * @internal
689 * It's `AF_LOCAL` a/k/a `AF_UNIX`.
690 */
691 template<typename Protocol>
692 int level(const Protocol& proto) const;
693
694 /**
695 * For internal boost.asio use, to enable `Peer_socket::get_option(Opt_peer_process_credentials&)` to work.
696 *
697 * @see boost.asio docs: `GettableSocketOption::name()` in boost.asio docs: implemented concept.
698 *
699 * @tparam Protocol
700 * See concept API.
701 * @param proto
702 * See concept API.
703 * @return See concept API.
704 *
705 * @internal
706 * It's `SO_PEERCRED`.
707 */
708 template<typename Protocol>
709 int name(const Protocol& proto) const;
710
711 /**
712 * For internal boost.asio use, to enable `Peer_socket::get_option(Opt_peer_process_credentials&)` to work.
713 *
714 * @see boost.asio docs: `GettableSocketOption::data()` in boost.asio docs: implemented concept.
715 *
716 * @tparam Protocol
717 * See concept API.
718 * @param proto
719 * See concept API.
720 * @return See concept API.
721 *
722 * @internal
723 * It's `SO_PEERCRED`.
724 */
725 template<typename Protocol>
726 void* data(const Protocol& proto);
727
728 /**
729 * For internal boost.asio use, to enable `Peer_socket::get_option(Opt_peer_process_credentials&)` to work.
730 *
731 * @see boost.asio docs: `GettableSocketOption::size()` in boost.asio docs: implemented concept.
732 *
733 * @tparam Protocol
734 * See concept API.
735 * @param proto
736 * See concept API.
737 * @return See concept API.
738 *
739 * @internal
740 * It's `sizeof(ucred)`.
741 */
742 template<typename Protocol>
743 size_t size(const Protocol& proto) const;
744
745 /**
746 * For internal boost.asio use, to enable `Peer_socket::get_option(Opt_peer_process_credentials&)` to work.
747 *
748 * @see boost.asio docs: `GettableSocketOption::resize()` in boost.asio docs: implemented concept.
749 *
750 * @tparam Protocol
751 * See concept API.
752 * @param proto
753 * See concept API.
754 * @param new_size_but_really_must_equal_current
755 * See concept API.
756 *
757 * @internal
758 * Resizing is not allowed for this option, so really it's either a no-op, or -- if
759 * `new_size_but_really_must_equal_current != size()` -- throws exception.
760 */
761 template<typename Protocol>
762 void resize(const Protocol& proto, size_t new_size_but_really_must_equal_current) const;
763}; // class Opt_peer_process_credentials
764
765/* data() and size() feed a util::Process_credentials to getsockopt() as if it were a `::ucred`; so the former
766 * must remain nothing but the latter, layout-wise. */
767static_assert(std::is_standard_layout_v<util::Process_credentials>
768 && (sizeof(util::Process_credentials) == sizeof(::ucred)),
769 "util::Process_credentials must remain layout-identical to a lone `::ucred`; "
770 "Opt_peer_process_credentials::data()/size() rely on it.");
771
772// Free functions: in *_fwd.hpp.
773
774// Msg_batch_in class template implementations.
775
776template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
778 (const Mutable_buffer_sequence& buf_seq, Msg_resource&& msg_resource) :
779 /* Internally, in Linux e.g., sets up iovec[], cumulative size, etc. It is also extra-quick in that a specialization
780 * is chosen at compile-time that adapts the concrete type Mutable_buffer_sequence extra-efficiently; for example
781 * by keeping an `iovec[2]` when Mutable_buffer_sequence is `array<Blob_mutable, 2>`. */
782 m_buf_seq_adapter(buf_seq),
783 m_resource(std::move(msg_resource))
784{
785 /* Caution! `man cmsg` in Linux says:
786 * "When initializing a buffer that will contain a series of cmsghdr structures (e.g., to be sent with sendmsg(2)),
787 * that buffer should first be zero-initialized to ensure the correct operation of CMSG_NXTHDR()."
788 * (We are recv*(), not send*(), but seems it would apply too... unless kernel would do it in that case.)
789 * We don't use CMSG_NXTHDR() though, so skip `m_msg_control_as_union.m_buf.fill(0)` here for perf.
790 * Careful! May need to add this later, if adding more types of ancillary data (but not if merely increasing
791 * N_PAYLOAD_FDS). */
792}
793
794template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
796 m_n_used(0)
797{
798 m_mdts.reserve(max_msg_count);
799 m_mmsg_hdrs.reserve(max_msg_count);
800 // They're empty though. Let them set up each guy via prepare_target_payload(-1) x N times.
801 assert((m_mdts.capacity() == max_msg_count) && "We use .capacity() to query max_msg_count.");
802 assert((m_mmsg_hdrs.capacity() == max_msg_count) && "We use .capacity() to query max_msg_count.");
803}
804
805template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
807 (const Mutable_buffer_sequence& target_blob, Msg_resource&& msg_resource, size_t idx)
808{
809 using Buf_seq_adapter = decltype(Mdt_per_payload::m_buf_seq_adapter);
810
811 if (idx == size_t(-1)) // Set up new slot.
812 {
813 // Ensure .emplace_back() wouldn't increase .capacity().
814 assert((!initialized())
815 && "At this time the fully-initialized batch size shall be permanently set via ctor; cannot grow.");
816
817 m_mdts.emplace_back(target_blob, std::move(msg_resource));
818 auto& mdt = m_mdts.back();
819
820 /* This part is identical to what we do in nb_read_some_with_native_handle() when making msghdr, so omitting cmnts.
821 * (@todo Code reuse here is arguably possible but would probably (1) be more lines overall and (2) might
822 * be challenging due to this one doing .emplace_back(), the other simply declaring on stack. Revisit.) */
823 m_mmsg_hdrs.emplace_back(); // @todo Is there some way to .emplace_back({ ... })?
824 auto& mmsg_hdr = m_mmsg_hdrs.back();
825 auto& msg_hdr = mmsg_hdr.msg_hdr;
826
827 // Identical to msghdr setup in nb_read_some_with_native_handle(); omitting those comments.
828 msg_hdr.msg_name = nullptr; // (Unused.)
829 msg_hdr.msg_namelen = 0; // (Unused.)
830 // (Ptr does not change, not even via prepare_target_payload(); see below.)
831 msg_hdr.msg_iov = mdt.m_buf_seq_adapter.buffers();
832 // (Does not change except via prepare_target_payload() or reuse_result_payloads(); see below.)
833 msg_hdr.msg_iovlen = mdt.m_buf_seq_adapter.count();
834 msg_hdr.msg_control = mdt.m_msg_control_as_union.m_buf.data(); // (Ptr does not change.)
835 // (Compile-time constant as in-arg *and* is modified by recv[m]msg() as out-arg; but nb_read() shall correct it.)
836 msg_hdr.msg_controllen = sizeof(Msg_control_as_union::m_buf);
837 // (Compile-time constant as in-arg *and* is modified by recv[m]msg() as out-arg; but nb_read() shall correct it.)
838 msg_hdr.msg_flags = S_PER_MSG_FLAGS;
839 // mmsg_hdr.msg_len: Out-arg (queried in, e.g., result_payload_blob()). Can leave uninit.
840
841 // Caution! Any changes or reasoning about the above <=> check reuse_result_payloads()'s swap logic too!
842 }
843 else if (idx < m_mdts.size()) // Re-prepare slot to describe new target memory area.
844 {
845 auto& mdt = m_mdts[idx];
846
847 /* Buf_seq_adapter{target_blob} is self-evidently correct; then move-assign (really copy-assign) the temporary
848 * into mdt.m_buf_seq_adapter. Perf: as of this writing (and likely ~forever) it copies a size_t total-size;
849 * and the iovec array by value. (As of this writing -- when used by Native_socket_stream_msg_batch_in --
850 * that is array<2>; so a couple void* and a couple size_t; but in general it could be a heavier op.) */
851 mdt.m_buf_seq_adapter = Buf_seq_adapter{target_blob};
852
853 mdt.m_resource = std::move(msg_resource); // Same as when initializing.
854
855 // The other `mdt` members are out-args; can be left alone. (Up to user to query responsibly.)
856
857 /* Now as for m_mmsg_hdrs[idx]: For unchanging/unused members it is uncontroversial to do nothing.
858 * That leaves msg_iov and msg_iovlen; let's discuss those. (It probably seems highly questionable to not
859 * simply reassign them again; the safest thing would be to just do that; but in for a penny, in for a pound
860 * perf-wise.)
861 * - msg_iov: Oh boy... to avoid excessive verbiage we point you to reuse_result_payloads() which discusses it,
862 * namely how it never changes.
863 * - msg_iovlen: For a general Mutable_buffer_sequence there's no choice but to reassign this simple integer.
864 * Again please read comment in reuse_result_payloads().
865 * @todo It is conceivable to skip it depending on Mutable_buffer_sequence; the logic could be specialized
866 * (much like buffer_sequence_adapter does). For example, if we are used by Native_socket_stream_msg_batch_in,
867 * then as of this writing Mutable_buffer_sequence is array<2>, so `mdt.m_buf_seq_adapter.count() == 2`, period.
868 * Considering this is a mere integer assignment, the to-do is hardly the highest-priority thing. */
869 auto& msg_hdr = m_mmsg_hdrs[idx].msg_hdr;
870 msg_hdr.msg_iovlen = mdt.m_buf_seq_adapter.count();
871
872 assert((msg_hdr.msg_controllen == sizeof(mdt.m_msg_control_as_union))
873 && "It is initialized to this constant value, and while recv*() shall modify it, nb_read() should have "
874 "immediately reset it again.");
875 }
876 else // if (idx >= m_mdts.size()) && not -1
877 {
878 assert(false && "prepare_target_payload(x) must either replace an existing slot x or add slot via `x = -1`.");
879 }
880} // Msg_batch_in::prepare_target_payload()
881
882template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
884 (size_t idx, Msg_resource** msg_resource_ptr)
885{
886 assert(idx < m_mmsg_hdrs.size());
887 // Note: We intentionally allow `idx >= m_n_used` due to reuse_result_payloads(); see our doc header.
888
889 if (msg_resource_ptr)
890 {
891 *msg_resource_ptr = &(m_mdts[idx].m_resource);
892 }
893
894 return m_mmsg_hdrs[idx].msg_len;
895}
896
897template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
899{
900 assert(idx < m_mdts.size());
901 // Note: We intentionally allow `idx >= m_n_used`; same reason as for result_payload_blob().
902
903 return m_mdts[idx].m_result_hndl;
904}
905
906template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
908{
909 return m_mdts.size() == m_mdts.capacity();
910}
911
912template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
914{
915 return m_n_used;
916}
917
918template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
920{
921 return m_n_used == m_mdts.size();
922}
923
924template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
926{
927 m_n_used = new_n_used;
928}
929
930template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
932{
933 using std::swap; // This is the proper ADL-friendly style (do *not* inline std::swap() instead).
934
935 /* Rationale for caring about swapping of guys like *this: (at least) Msg_batch_in::reuse_result_payloads().
936 * It might be best to look at that thing for context too when grokking this.
937 *
938 * Why the custom swap()? Answer:
939 * - At least one thing need not actually be swapped; see below.
940 * - If there's a custom, faster swap(Msg_resource_t&, Msg_resource_t&), then the below will invoke it
941 * (as opposed to the default which would do 3 move-assigns).
942 * - There *is* a custom, faster Native_handle swap, so we invoke it (similar reasoning). */
943
944 /* This ultimately swaps the `iovec` arrays (and perhaps some other stuff such as the computed total size) stored
945 * inside boost::asio::detail::buffer_sequence_adapter. (Depending on the Mutable_buffer_sequence specialization
946 * they might not be arrays but, e.g., just single `iovec`s... but memory-wise equivalent to arrays of some constant
947 * length.) So most likely it can be thought of as 3 memcpy()s. Actually as of this writing Mutable_buffer_sequence,
948 * when we are used by Native_socket_stream_msg_batch_in, is array<2> (b/c its prepare_target_payload() takes just
949 * a single Mutable_buffer, and we pre-pend m_msg_type's location/size); so in that case it's more like:
950 * 3-op swap of 2 void*, 3-op swap of 2 size_t. That is to say it's pretty quick. In general though depending
951 * on Mutable_buffer_sequence it could be slower; but that's life.
952 *
953 * Perf aside the point is: this is a by-value swap. */
955
956 /* m_msg_control_as_union is out-arg, and nb_read() saves any contents to m_result_hndl; so this is garbage.
957 * No need to swap via `swap(m_msg_control_as_union, that->m_msg_control_as_union)`. */
958
959 swap(m_resource, that->m_resource); // Either std::swap() or custom-swap the `Msg_resource_t`s (ADL).
960
961 /* This is an important out-arg, so of course we should swap it. It is just an int really.
962 * Though since we're expounding on every little thing already... std::swap()
963 * of Mdt_per_payload would cause 3 Native_handle move-assigns, and as of this writing actually that'd
964 * waste cycles on unnecessarily nullifying source ->m_result_hndl, 3 times; so it's nice to do this. */
966} // Mdt_per_payload::swap()
967
968template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
969template<typename Is_unused_func>
970void
972 const Is_unused_func& is_unused_func)
973{
974 using std::swap; // This is the proper ADL-friendly style (do *not* inline std::swap() instead).
975
976 /* This is adapted from a GNU STL remove_if() impl; but swap()s instead of merely move()ing.
977 * (To illustrate, remove_if("abc def ghi ", " ") would result in "abcdefghihi "; as the defghi chars
978 * in the back would get moved-to the front area, hence the move-from would just duplicate them; for other element
979 * types it would nullify them. Our algorithm would instead yield "abcdefghi ", the earlier-on spaces being
980 * swap-moved to the back.) Also we deal in indices and is_unused(idx) calls as opposed to iterators/F(*it); but
981 * logically same thing. */
982
983 auto& first = start_idx; // (Mere alias so as to match the GNU STL impl more closely.)
984
985 for (; (first != m_n_used) && (!is_unused_func(first)); ++first) {}
986
987 if (first == m_n_used)
988 {
989 return; // Unchanged -- all elements are used.
990 }
991 // else
992
993 auto result = first++;
994 for (; first != m_n_used; ++first)
995 {
996 if (!is_unused_func(first))
997 {
998 // Must swap slots [result] and [first]. Slot = m_mmsg_hdrs[idx] + m_mdts[idx].
999
1000 m_mdts[result].swap(&m_mdts[first]); // See inside this for key comments.
1001 // ^-- @todo swap(..., ...) would be better, but it's a private inner struct, so we haven't bothered....
1002
1003 /* There is no custom swap(mmsghdr&, mmsghdr&), so the default would happen if we simply did
1004 * swap(m_mmsg_hdrs[result], m_mmsg_hdrs[first]);
1005 * Namely the scalars therein would be swapped using the usual 3-step thing. They are all integers or pointers,
1006 * so the perf is fine... but (1) in for a penny, in for a pound: We decided to be precious about perf and swap
1007 * only what is necessary; and (2) more importantly in our case some of the values point to m_mdts[idx] -- so
1008 * the proper "swap" is to do nothing in that case! So it wouldn't just be "slow" but also wrong.
1009 *
1010 * Let's justify what we actually do do. To wit:
1011 *
1012 * - Firstly we can ignore (for perf) the various unused and constant-valued fields:
1013 * msg_hdr.{msg_name, msg_namelen, msg_flags, msg_controllen}.
1014 * - msg_hdr.msg_controllen is a subtle beast. As an in-arg, it is always set to a constant value in our case
1015 * (sizeof(a thing)). Then, however, recv*() modifies it! It is unclear to *what* exactly it is modified;
1016 * presumably something like the size of received ancillary data; but docs do not say how to use it and
1017 * moreover say CMSG*() macros are to be used to traverse ancillary data (and we do just that in nb_read(),
1018 * where we grab the native handles if any and save in m_mdts[...].m_result_hndl). That does not, however,
1019 * mean msg_controllen (as out-arg) is not used; maybe CMSG*() grabs it/uses it somehow. That said: by the
1020 * time we are executed, nb_read() has already set m_result_hndl (and the m_mdts[].swap() dealt with it
1021 * above), and reset msg_controllen to the constant value it must equal as in-arg in the next nb_read().
1022 * Therefore: bottom line: It is always set to a constant value, except for a short time inside nb_read()
1023 * just after recv*(). Therefore, here, we can ignore it as a constant-valued field.
1024 * - The out-arg mmsghdr::msg_len (# of bytes received) is clearly correct to swap in normal fashion; it
1025 * is accessed via public accessor(s).
1026 * - msg_control points to where ::recvmmsg() is to place received FDs.
1027 * Its value is: mdt[...].m_msg_control_as_union.m_buf.data(): ptr to element 0 of array, namely the
1028 * ancillary-data (FD) storage area of a certain constant (no matter the slot) size. Even if the
1029 * values inside said array mattered (they don't as of this writing; nb_read() copies FD(s) into m_mdts[]),
1030 * pointing to the same-index-th mdts[].m_msg_control_as_union would have been correct; meaning
1031 * the contents would've been swapped above, so the pointers should remain unchanged. (This is a bit tricky
1032 * IMO (ygoldfel) to realize and might require some pencil-and-paper to prove to oneself.)
1033 * - msg_iov and msg_iovlen: Okay, so IMHO (ygoldfel) this is shockingly difficult to grok, if one is not
1034 * very careful in one's reasoning. It also very much relies on how
1035 * boost::asio::detail::buffer_sequence_adapter is implemented; it's in detail:: in the first place and
1036 * in any case internally could change; but we've already decided to take that risk elsewhere. So let's
1037 * just go with the facts of its current state (Boost-1.87). buffer_sequence_adapter, for the purposes of
1038 * this discussion, should be thought of as being isomorphic to array<iovec, N>, where N is a constant
1039 * (yes, it could even just be simply equal to `iovec`, depending on Mutable_buffer_sequence, but that still
1040 * fits). Then, msg_iov is a pointer &...[0] of that array; and `msg_iovlen <= N` is how much of that
1041 * array to use. (This all would be different if .buffers() returned some malloc()ed heap address instead
1042 * of a pointer right into m_mdts[] memory area itself.) So given that info:
1043 * - Up above, we did in fact swap the *contents* of the two array<iovec, N>s. Therefore
1044 * the *pointers* msg_iov should *not* be swapped: they are pointing at the proper iovec-arrays already.
1045 * - However the active *lengths* do need to be swapped! E.g., picture [1 2 x]<=>[3 4 5]; this becomes
1046 * [3 4 5]...[1 2 x]. msg_iov (unchanged) were pointing at 1 and 3; now to 3 and 1; that's right.
1047 * But if the lengths were 2<=>3, their becoming 3...2 is correct; whereas remaining 2...3 would be
1048 * blatantly wrong; the msg_iovlen=2 would ignore the element `5`, and msg_iovlen=3 would refer to
1049 * the element `x` among others.
1050 * So yes, as odd it may seem, the correct thing is to not touch msg_iov; but to swap msg_iovlen.
1051 * @todo See also to-do in prepare_target_payload() regarding conceivable specialized logic that could
1052 * at times skip this swap as well.
1053 * - Just to confuse you even more: if we're being real, any user of reuse_result_payloads() is highly
1054 * likely to simply keep msg_iovlen always at the same value, and same for the individual buffers'
1055 * lengths (inside each iovec), when comparing between any two slots. If it's possible their algorithm
1056 * allows for swapping slots like this, then they pretty much have to have identical structure. Granted,
1057 * perhaps one could have, like, 3 1-buffers and 1 3-buffer (totaling the same capacity, 3) in 2 respective
1058 * slots; but come on, why do such a thing? So the swapping is likely a no-op anyway... but hey, if
1059 * we want to be correct, we should be correct. */
1060
1061 auto& mmsg_hdr1 = m_mmsg_hdrs[result];
1062 auto& mmsg_hdr2 = m_mmsg_hdrs[first];
1063 auto& msg_hdr1 = mmsg_hdr1.msg_hdr;
1064 auto& msg_hdr2 = mmsg_hdr2.msg_hdr;
1065
1066 swap(mmsg_hdr1.msg_len, mmsg_hdr2.msg_len);
1067 swap(msg_hdr1.msg_iovlen, msg_hdr2.msg_iovlen);
1068
1069#if 0 // This is pretty well tested by now -- cut it out for perf when assert()s enabled.
1070 // Paranoia strikes deep... opportunistically check this invariant... perhaps again.
1071 assert((msg_hdr1.msg_controllen == sizeof(Msg_control_as_union::m_buf))
1072 &&
1073 (msg_hdr2.msg_controllen == sizeof(Msg_control_as_union::m_buf))
1074 && "It is initialized to this constant value, and while recv*() shall modify it, nb_read() should have "
1075 "immediately reset it again.");
1076#endif
1077 // End of swap code.
1078
1079 ++result;
1080 } // if (!is_unused_func(first))
1081 } // for (; first != m_n_used; ++first)
1082
1083 m_n_used = result;
1084
1085 /* @todo Conceptually, our approach to swapping the m_mdts[] and m_mmsg_hdrs[] pairs is arranged as follows:
1086 * Skip anything constant/unused; swap m_mdts[] first; swap non-m_mdts[]-referencing parts of m_mmsg_hdrs[]
1087 * (i.e., msg_len); and lastly update/swap the relevant m_mdts[]-referencing parts (i.e., msg_hdr.msg_iovlen).
1088 * The part of that that's at all tricky is this subset: swap m_mdts[] by value, so no pointer into m_mdts[]
1089 * needs updating. This *is* correct (hence all the verbiage above), but perf-wise: there's the
1090 * m_buf_seq_adapter swap (linear in sizeof(Mutable_buffer_sequence) -- could be quite quick, could be less so);
1091 * and m_resource swap. The potential to-do would be to switch the paradigm: m_mdts becomes a mere store
1092 * (wouldn't even need to be a vector<>; could be anything like set<>; but vector<> is faster for our purposes;
1093 * point is, the index into it no longer has meaning); and the swapping would only swap various pointers
1094 * in m_mmsg_hdrs[]. So m_mdts[] are *not* swapped by value, only various referents into it are.
1095 * This would avoid those *sort of* expensive m_buf_seq_adapter and m_resource swaps. Some of the reasoning
1096 * above wouldn't need to be so annoying (arguable). Probably would need to use the trick where msg_name would store
1097 * a pointer into m_mdts, for our own purposes, at least to be able to find the proper m_mdts[].m_result_hndl.
1098 *
1099 * Should we do it? In terms of simplifying stuff, it might be a wash. In terms of perf, it depends on how
1100 * we're used. As of this writing the swap-stuff/reuse_result_payloads() algorithm exists because of
1101 * how Native_socket_stream_msg_batch_in uses us, so let's consider that use case; see
1102 * Native_socket_stream_msg_batch_in::nb_read(), namely how it uses reuse_result_payloads() as of this writing.
1103 * To wit: illegal in-dgrams are non-existent; and there's at most one graceful-close per connection; so the
1104 * only reason to actually swap would be auto-ping in-dgrams. Those might seem like they could be quite prevalent;
1105 * but consider that if the system is so loaded to where *this* level of optimization at all matters, then
1106 * such a connection would hardly ever even need to send auto-pings (which occur in an idle channel, not otherwise).
1107 * So... I (ygoldfel) would at this point say, keep an eye on profiling results, and if reuse_result_payloads() looks
1108 * like a hot-spot, then look into it. Otherwise, leave it alone. */
1109} // Msg_batch_in::reuse_result_payloads()
1110
1111template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
1112bool
1114 Peer_socket<Protocol_pkt_stream>* peer_socket_ptr,
1115 bool* graceful_close, Error_code* err_code,
1116 int message_flags,
1117 size_t* total_rcvd_bytes_or_null)
1118{
1119 using flow::log::Sev;
1120 using boost::system::system_category;
1121 using boost::io::ios_all_saver;
1122 namespace sys_err_codes = boost::system::errc;
1123 using std::swap;
1124 using std::memcpy;
1125 using ::recvmmsg;
1126 using ::recvmsg;
1127 using ::mmsghdr;
1128 using ::cmsghdr;
1129 // using ::SOL_SOCKET; // It's a macro apparently.
1130 using ::SCM_RIGHTS;
1131 using ::MSG_DONTWAIT;
1132 using ::MSG_TRUNC;
1133 using ::MSG_CTRUNC;
1134 // using ::errno; // It's a macro apparently.
1135
1136 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(bool, nb_read, logger_ptr, peer_socket_ptr, graceful_close, _1, message_flags,
1137 total_rcvd_bytes_or_null);
1138 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
1139
1140 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_TRANSPORT);
1141
1142 assert((m_mmsg_hdrs.size() == m_mdts.size()) && "Gotta be a bug somewhere in Msg_batch_in.");
1143 if (!initialized()) // As advertised:
1144 {
1145 FLOW_LOG_WARNING("Local_stream_batch [" << *this << "]: "
1146 "Attempt to batch-read without adding all [" << m_mdts.capacity() << "] slots; so far "
1147 "only [" << m_mdts.size() << "] slots have been added via prepare_target_payload(). "
1148 "This is the API user's error, though since this API is used internally in the library "
1149 "too, it might be a bug in the library. We can't tell from here. Emitting invalid-argument "
1150 "error.");
1152 return true;
1153 }
1154 // else
1155
1156 const size_t n_payloads = m_mdts.size() - m_n_used;
1157
1158 if (n_payloads == 0)
1159 {
1160 return false;
1161 }
1162 // else if (n_payloads >= 1):
1163
1164 /* To reader/maintainer: The rest is similar to nb_read_some_with_native_handle() in various places ("just"
1165 * applied to 2+ potential in-dgrams), so we will skip some comments.
1166 *
1167 * The difference is basically that for each of the following, there are now roughly speaking n_payloads
1168 * of that thing:
1169 * - msghdr to the OS-call:
1170 * - Instead there's an array of them, one per potential in-dgram, arranged into mmsghdr [sic] structs.
1171 * - And therefore buf_seq_adapter (iovec array with target in-bufs in msghdr):
1172 * - Instead there's (essentially) a sequence of them (yes, seq of seqs), m_mdts[*].m_buf_seq_adapter.
1173 * - Also therefore msg_control_as_union (which sits ready to receive an FD, or none, upon recvmsg()):
1174 * - Instead there's one of these per msghdr, m_mdts[*].m_msg_control_as_union.
1175 * - n_rcvd_or_error (bytes received into buf_seq, returned by recvmsg(), or -1 for error):
1176 * - Instead recvmmsg() returns -1 for error, else the # of leading `msghdr`s that did receive data, and
1177 * for each of those the `n_rcvd` part, so to speak, is populated in that mmsghdr, m_mmsg_hdrs[*].msg_len.
1178 *
1179 * Due to the design of *this class, we've been painstakingly maintaining all of those things, so we can
1180 * pretty much execute ::recvmmsg() right away with no linear-time prep. Reminder: We don't target the
1181 * *entire* m_mmsg_hdrs+m_mdts vectors; but rather the trailing n_payloads of them, starting with
1182 * m_...[m_n_used]. */
1183
1184 assert(peer_socket_ptr);
1185 auto& peer_socket = *peer_socket_ptr;
1186 const Native_handle peer_hndl{peer_socket.native_handle()};
1187
1188 /* Since this method and class are to be heavily optimized: Use this optimization that caches
1189 * whether TRACE messages should be logged instead of computing it per-message. The negatives are:
1190 * - The code is a bit more involved (not too bad, when perf is truly at stake).
1191 * - If the verbosity is changed under us *during* the method's execution, this will not respond to it
1192 * immediately. It's fine: the lag is only a split-second; and a slight unevenness in taking verbosity
1193 * config changes is an expected/accepted phenomenon generally. */
1194 const bool do_log_trace = logger_ptr && logger_ptr->should_log(Sev::S_TRACE, get_log_component());
1195
1196 auto recvmmsg_hdr_ptr = &(m_mmsg_hdrs[m_n_used]);
1197 int n_rcvd_msgs_or_error;
1198
1199 /* Massage message_flags for our required purposes.
1200 * Same comments as when calling ::recvmsg() in nb_read_some_with_native_handle(). */
1201 message_flags = (message_flags | (MSG_DONTWAIT | MSG_TRUNC));
1202
1203 if (n_payloads == 1)
1204 {
1205 // There is only one target dgram; try to optimize somewhat by reverting to single-msg native API.
1206 const auto n_rcvd
1207 = recvmsg(peer_hndl.m_native_handle, &recvmmsg_hdr_ptr->msg_hdr, message_flags);
1208 if (do_log_trace)
1209 {
1210 FLOW_LOG_TRACE_WITHOUT_CHECKING
1211 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1212 "tried to batch-read up-to 1 in-message, "
1213 "potentially into a scattered-blob plus possibly a native handle; "
1214 "for efficiency used recvmsg() instead of recvmmsg(); it yielded return value "
1215 "[" << n_rcvd << "]; we shall now continue as-if this was done using recvmmsg().");
1216 }
1217
1218 if (n_rcvd == -1)
1219 {
1220 n_rcvd_msgs_or_error = -1;
1221 }
1222 else
1223 {
1224 n_rcvd_msgs_or_error = 1;
1225 recvmmsg_hdr_ptr->msg_len = static_cast<unsigned int>(n_rcvd);
1226 // Note: n_rcvd might be 0, indicating EOF (graceful-close). recvmmsg() *would* also result in `->msg_len == 0`.
1227 }
1228 }
1229 else
1230 {
1231 n_rcvd_msgs_or_error
1232 = recvmmsg(peer_hndl.m_native_handle, recvmmsg_hdr_ptr, n_payloads, message_flags,
1233 nullptr); // Don't mess with timeouts.
1234 }
1235
1236 /* Carefully check all the outputs. Return value first.
1237 * (Code reuse with nb_read_some_with_native_handle() is possible, but there are enough little differences, such
1238 * as logging, to where it feels more unwieldy than it is worth. @todo Maybe revisit.) */
1239
1240 if (n_rcvd_msgs_or_error == -1)
1241 {
1242 /* Not even 1 byte of a blob was read; and hence nor was any FD. (Omitting various comments from
1243 * nb_read_some_with_native_handle() that apply equally here.) */
1244
1245 const Error_code sys_err_code{errno, system_category()};
1246 if ((sys_err_code == sys_err_codes::operation_would_block) || // EWOULDBLOCK
1247 (sys_err_code == sys_err_codes::resource_unavailable_try_again)) // EAGAIN (same meaning)
1248 {
1249 if (do_log_trace)
1250 {
1251 FLOW_LOG_TRACE_WITHOUT_CHECKING
1252 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1253 "tried to batch-read up-to [" << n_payloads << "] in-messages, each "
1254 "potentially into a scattered-blob plus possibly a native handle per each; but read attempt "
1255 "indicated would-block; not an error condition. No in-messages received.");
1256 }
1257 *err_code = boost::asio::error::would_block;
1258 return true; // m_n_used unchanged.
1259 }
1260 // else
1261
1262 assert(sys_err_code);
1263
1264 FLOW_ERROR_SYS_ERROR_LOG_WARNING(); // Log based on sys_err_code.
1265 FLOW_LOG_WARNING("Local_stream_batch [" << *this << "]: "
1266 "Connected local peer socket [" << peer_hndl << "] "
1267 "tried to batch-read up-to [" << n_payloads << "] in-messages, each "
1268 "potentially into a scattered-blob plus possibly a native handle per each; "
1269 "but an unrecoverable error occurred. No in-messages received.");
1270 *err_code = sys_err_code;
1271 return true; // m_n_used unchanged.
1272 } // if (n_rcvd_msgs_or_error == -1)
1273 // else if (n_rcvd_msgs_or_error != -1)
1274
1275 if (n_rcvd_msgs_or_error == 0)
1276 {
1277 assert((n_payloads > 1) && "We should have returned false on 0; and used recvmsg() on 1, resulting in "
1278 "n_rcvd_msgs_or_error being -1 or 1, not zero.");
1279 // The assert() states that recvmmsg() was used. So given that:
1280
1281 FLOW_LOG_FATAL
1282 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1283 "tried to batch-read up-to [" << n_payloads << "] in-messages, each "
1284 "potentially into a scattered-blob plus possibly a native handle per each; "
1285 "but recvmmsg() returned 0; this is an unexpected semantic; EOF (graceful-close) should result in "
1286 "an in-message with msg_len=0 instead. Aborting; please look into this situation; it could be a bug.");
1287 assert(false
1288 && "Tried to batch-read up-to 2+ in-messages, each "
1289 "potentially into a scattered-blob plus possibly a native handle per each; "
1290 "but recvmmsg() returned 0; this is an unexpected semantic; EOF (graceful-close) should result in "
1291 "an in-message with msg_len=0 instead. Aborting; please look into this situation; it could be a bug.");
1292 std::abort();
1293 }
1294 // else if (n_rcvd_msgs_or_error > 0):
1295
1296 /* In-message(s) received. Here in-message means either an actual in-message or a graceful-close "in-message."
1297 * (A fairly-important, albeit undocumented and obscure fact: graceful-close is emitted as an in-message record
1298 * with msg_len=0 (no data)... followed by identical such records up to n_payloads total records.)
1299 *
1300 * Check each one for corner case errors and finalize each one's out-args. Again:
1301 * Omitting various comments that apply equally in nb_read_some_with_native_handle(). */
1302
1303 const auto recvmmsg_hdr_end = recvmmsg_hdr_ptr + n_rcvd_msgs_or_error;
1304 auto mdt_ptr = &(m_mdts[m_n_used]);
1305
1306 /* Used inside the loop on any error; essentially -- seeing as how we are emitting failure <=> no emitted msgs --
1307 * it (1) cleans-up *this (m_mmsg_hdrs, m_mdts) that may have been set to non-unused-state before the
1308 * current (recvmmsg_hdr_ptr, mdt_ptr) one that triggered the error; and (2) ensures no native-handles
1309 * that may have been received in the batch get leaked (since we emit no msgs) -- it returns these to OS.
1310 * (Subtlety: Un-emitted slots (i.e., ones starting with [m_n_used]) aren't required to have .m_result_hndl
1311 * in any particular state, .null() or otherwise. So this helper doesn't "formally" promise anything about that;
1312 * only that any in-batch-contained raw handles get closed.)
1313 *
1314 * (1) might be unnecessary paranoia -- generally user should not and possibly cannot use the socket
1315 * after an error anyway -- but we've spent significant effort on ensuring msg_{controllen|flags} is in its in-arg
1316 * form as much as possible; this will make quite sure in the (relatively) rare error case.
1317 *
1318 * (2) accounts for an edge (given a sane opposing guy) scenario; but in general why not?
1319 *
1320 * Note this is only necessary for the slots recv*() actually touched. So in particular no need to do this
1321 * at all unless `n_rcvd_msgs_or_error > 0`. */
1322 const auto clean_remaining_func = [&]() -> bool
1323 {
1324 assert((recvmmsg_hdr_ptr != recvmmsg_hdr_end) && "Internal pre-condition violated.");
1325
1326 /* (1), in short, is just resetting msg_{controllen|flags} to their init-values. For pre-error slots it's
1327 * already done; so we do it for the post-error (inclusive) slots. This loop is over those.
1328 *
1329 * (2) needs to un-leak any handle-copies in the entire in-batch: the pre-error slots if any; and the
1330 * post-error slots (inclusive; so at least 1). In this loop therefore we do the latter half. */
1331 do
1332 {
1333 auto& recvmsg_hdr = recvmmsg_hdr_ptr->msg_hdr;
1334
1335 const auto recvmsg_hdr_cmsg_ptr = CMSG_FIRSTHDR(&recvmsg_hdr); // Half of (2).
1336 if (recvmsg_hdr_cmsg_ptr
1337 && (recvmsg_hdr_cmsg_ptr->cmsg_level == SOL_SOCKET)
1338 && (recvmsg_hdr_cmsg_ptr->cmsg_type == SCM_RIGHTS))
1339 {
1340 Native_handle hndl;
1341 memcpy(&hndl.m_native_handle, CMSG_DATA(recvmsg_hdr_cmsg_ptr), sizeof(hndl.m_native_handle));
1342 hndl.close();
1343 /* Closed handle, as promised. / Did not touch *this (m_mdts[].m_result_hndl); allowed by our contract.
1344 * (In practice, as of this writing, this is equivalent behavior to never having received the
1345 * post-error slots; clearly sensible. As of this writing that applies to the error-slot in particular too,
1346 * as calling code below only touches .m_result_hndl last, after eliminating all error possibilities.) */
1347 }
1348
1349 // (1). Has to be done after the above (msg_controllen affects CMSG_FIRSTHDR() behavior).
1350 recvmsg_hdr.msg_controllen = sizeof(Msg_control_as_union::m_buf);
1351 recvmsg_hdr.msg_flags = S_PER_MSG_FLAGS;
1352 }
1353 while ((++recvmmsg_hdr_ptr) != recvmmsg_hdr_end);
1354
1355 /* That leaves the other half of (2): Un-leaking the handle-copies in pre-error slots if any. mdt_ptr points
1356 * at the error-slot still, so we backtrack to the start of the target area of *this batch, closing any
1357 * `Native_handle`s we'd recorded. (We could *not* instead just loop through all of m_mmsg_hdrs as above,
1358 * as msg_controllen would have been reset by the looping code below. Even if we could:
1359 * This is just nice, as we can also re-nullify pre-error `.m_result_hndl`s -- not required but cleaner
1360 * than leaving bogus values in there.) */
1361 const auto mdt_ptr_first = &(m_mdts[m_n_used]);
1362 while (mdt_ptr != mdt_ptr_first)
1363 {
1364 (--mdt_ptr)->m_result_hndl.close(); // Close handle, as promised. Nullify for bonus cleanliness.
1365 }
1366
1367 return true;
1368 }; // clean_remaining_func =
1369
1370 size_t n_rcvd_msgs_actual = n_rcvd_msgs_or_error; // For EOF-marker handling; you'll see.
1371 size_t total_rcvd_bytes = 0; // Running tally of msg_len across actual (non-EOF) dgrams; EOF markers contribute 0.
1372 do
1373 {
1374 auto& recvmsg_hdr = recvmmsg_hdr_ptr->msg_hdr;
1375 auto& mdt = *mdt_ptr;
1376
1377 /* msg_hdr.msg_iov array-pointed area is already written-to... and .msg_len already holds how many bytes of that
1378 * area was used. Though, as in nb_read_some_with_native_handle(), it may also indicate overflow
1379 * (N-byte in-dgram but <N bytes in target buffer). Refer to that function; keeping comments light except ones
1380 * relevant here only. */
1381
1382 const size_t target_payload_blob_sz = mdt.m_buf_seq_adapter.total_size();
1383 const auto msg_len = recvmmsg_hdr_ptr->msg_len;
1384 if (msg_len > target_payload_blob_sz)
1385 {
1386 FLOW_LOG_WARNING("Local_stream_batch [" << *this << "]: "
1387 "Connected local peer socket [" << peer_hndl << "] "
1388 "tried to batch-read up-to [" << n_payloads << "] in-messages, "
1389 "each potentially into a scattered-blob plus possibly a native handle per each; "
1390 "and it returned it read [" << n_rcvd_msgs_or_error << "] in-messages; and for message "
1391 "index [" << (n_rcvd_msgs_or_error - (recvmmsg_hdr_end - recvmmsg_hdr_ptr)) << "] (0-based) "
1392 "got [" << msg_len << "] bytes successfully, but this overflows "
1393 "the blob's capacity [" << target_payload_blob_sz << "]. "
1394 "Acting as if nothing received + error.");
1395 assert((recvmsg_hdr.msg_flags & MSG_TRUNC)
1396 && "In-dgram size overflows user-supplied buffer <=> MSG_TRUNC out-flag supposed to be set.");
1397
1398 /* Note/caution: Suppose this is iteration 2+. We emit the error anyway and don't report any received
1399 * data -- those in-dgrams are "eaten." This doesn't violate the contract (wherein we promise to not
1400 * emit data *and* an error). However it doesn't do the conceivable thing (that recvmmsg() itself does), where
1401 * we report the in-dgrams now and defer the error to the next receive-op later. Why not? Answer: Firstly
1402 * it would be hard-ish to implement; we'd have to actively store the error in a new m_, and so on. Secondly
1403 * we feel it is not worth it, as this error is truly exceptional -- the other side is misbehaving.
1404 * @todo Perhaps revisit. */
1405
1407 return clean_remaining_func(); // m_n_used unchanged.
1408 }
1409 // else: No overflow; and msg_len may be 0 (EOF, handled specially below) or 1+.
1410
1411 // Next, the out-flags.
1412
1413 /* Subtlety: It is also an in-arg; so immediately reset it, in case no fatal error results from a non-zero out-flag.
1414 * clean_remaining_func() will do the same on error for all remaining .msg_flags (and this one again... no
1415 * biggie). */
1416 decltype(recvmsg_hdr.msg_flags) out_flags = S_PER_MSG_FLAGS;
1417 swap(out_flags, recvmsg_hdr.msg_flags);
1418
1419 if (out_flags != 0)
1420 {
1421 if (logger_ptr && logger_ptr->should_log(Sev::S_INFO, get_log_component()))
1422 {
1423 ios_all_saver saver{*(logger_ptr->this_thread_ostream())}; // Revert std::hex/etc. soon.
1424 FLOW_LOG_INFO_WITHOUT_CHECKING
1425 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1426 "tried to batch-read up-to [" << n_payloads << "] in-messages, each "
1427 "potentially into a scattered-blob plus possibly a native handle per each; "
1428 "and it returned it read [" << n_rcvd_msgs_or_error << "] in-messages; and for message "
1429 "index [" << (n_rcvd_msgs_or_error - (recvmmsg_hdr_end - recvmmsg_hdr_ptr)) << "] (0-based) "
1430 "got [" << msg_len << "] bytes successfully but also returned raw "
1431 "out-flags value [0x" << std::hex << out_flags << "]. "
1432 "Will check for relevant flags but otherwise ignoring if nothing bad. "
1433 "Logging at elevated level because it's interesting; please investigate.");
1434 }
1435
1436 if ((out_flags & MSG_CTRUNC) != 0)
1437 {
1438 if (logger_ptr && logger_ptr->should_log(Sev::S_WARNING, get_log_component()))
1439 {
1440 ios_all_saver saver{*(logger_ptr->this_thread_ostream())}; // Revert std::hex/etc. soon.
1441 FLOW_LOG_WARNING_WITHOUT_CHECKING
1442 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1443 "tried to batch-read up-to [" << n_payloads << "] in-messages, "
1444 "each potentially into a scattered-blob plus possibly a native handle per each; "
1445 "and it returned it read [" << n_rcvd_msgs_or_error << "] in-messages; and for message "
1446 "index [" << (n_rcvd_msgs_or_error - (recvmmsg_hdr_end - recvmmsg_hdr_ptr)) << "] (0-based) "
1447 "got [" << msg_len << "] bytes successfully but also returned raw "
1448 "out-flags value [0x" << std::hex << out_flags << "] which includes MSG_CTRUNC. "
1449 "That flag indicates more stuff was sent as ancillary data; but we expect at most 1 native "
1450 "handle. Other side sent something strange. Acting as if nothing received + error.");
1451 }
1452
1453 // Note/caution: Same as in the S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE case above.
1454
1456 return clean_remaining_func(); // m_n_used unchanged.
1457 }
1458 // else: Fall-through.
1459 } // if (out_flags != 0)
1460
1461 /* Next, the ancillary data (with the FDs if any). Again: cmnts light; see nb_read_some_with_native_handle().
1462 * This is a new point though: We don't necessarily have to save the FD anywhere; the accessor
1463 * result_payload_hndl() could just do the below itself and return the result from there directly. However then:
1464 * the error handling has to move there which is annoying API-wise; and it's slower, as we have to do the
1465 * CMSG_FIRSTHDR() and the checking and... let's just take care of it and save the FD in a nice Native_handle for
1466 * that accessor to return. It's just an int; let's be real. */
1467
1468 cmsghdr* const recvmsg_hdr_cmsg_ptr = CMSG_FIRSTHDR(&recvmsg_hdr);
1469 if (recvmsg_hdr_cmsg_ptr)
1470 {
1471 if ((recvmsg_hdr_cmsg_ptr->cmsg_level == SOL_SOCKET) &&
1472 (recvmsg_hdr_cmsg_ptr->cmsg_type == SCM_RIGHTS))
1473 {
1474 static_assert(N_PAYLOAD_FDS == 1, "Should be only dealing with one native handle with recvmsg() "
1475 "as of this writing.");
1476 memcpy(&mdt.m_result_hndl.m_native_handle, CMSG_DATA(recvmsg_hdr_cmsg_ptr),
1477 sizeof(mdt.m_result_hndl.m_native_handle));
1478 }
1479 else
1480 {
1481 FLOW_LOG_WARNING("Local_stream_batch [" << *this << "]: "
1482 "Connected local peer socket [" << peer_hndl << "] "
1483 "tried to batch-read up-to [" << n_payloads << "] in-messages, "
1484 "each potentially into a scattered-blob plus possibly a native handle per each; "
1485 "and it returned it read [" << n_rcvd_msgs_or_error << "] in-messages; and for message "
1486 "index [" << (n_rcvd_msgs_or_error - (recvmmsg_hdr_end - recvmmsg_hdr_ptr)) << "] (0-based) "
1487 "got [" << msg_len << "] bytes successfully but also "
1488 "unexpected ancillary data of cmsg_level|cmsg_type "
1489 "[" << recvmsg_hdr_cmsg_ptr->cmsg_level << '|' << recvmsg_hdr_cmsg_ptr->cmsg_type << "]. "
1490 "Acting as if nothing received + error.");
1491
1492 // Note/caution: Same as in the S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE case above.
1493
1495 return clean_remaining_func(); // m_n_used unchanged.
1496 }
1497 // else: Fall-through (.m_result_hndl is set just above).
1498 }
1499 else
1500 {
1501 // No ancillary data, meaning no native handle; that's quite normal; indicate it as promised:
1502 mdt.m_result_hndl = {};
1503 }
1504
1505 /* Please see nb_read_some_with_native_handle() comment near msg_controllen init; as well as
1506 * reuse_result_payloads() comment discussing it. That justifies the following (as well as clean_remaining_func())
1507 * in more detail -- but basically we have the invariant that recvmsg_hdr.msg_controllen is always this constant
1508 * value as in-arg, and recv*() happens to set it to something else as out-arg. So we always undo it either here
1509 * (up to detecting an error) or via clean_remaining_func() (upon detecting error). */
1510 recvmsg_hdr.msg_controllen = sizeof(Msg_control_as_union::m_buf);
1511
1512 /* Almost done except for the following fairly-important, albeit ~undocumented and obscure fact:
1513 * recvmmsg() will *not* simply return 0 on EOF (nor defer doing so until the next recv*(), if 1+ in-messages
1514 * preceded it in the present recvmmsg()). Instead, it will yield endless 0-length data-free "messages,"
1515 * apparently mimicking what would happen if we were to instead trigger repeated recvmsg() calls (each would
1516 * return 0, hence the corresponding mmsg_hdr.msg_len is set to 0 also). Therefore: */
1517 const bool eof_marker = (msg_len == 0) && mdt.m_result_hndl.null();
1518 if (eof_marker)
1519 {
1520 /* K (see below) <= n_rcvd_msgs_or_error - 1.
1521 * If current n_rcvd_msgs_actual = its original value, then K < n_rcvd_msgs_actual, so n_rcvd_msgs_actual
1522 * shall now become the new smaller value, marking the *first* empty "in-dgram" (really, EOF marker).
1523 * If current n_rcvd_msgs_actual has been set by a previously set EOF marker in an earlier iteration,
1524 * then K > n_rcvd_msgs_actual; hence n_rcvd_msgs_actual remains unchanged and continues to mark the first
1525 * EOF marker "in-dgram." So this is a nice compact/fast way to express setting it only once at most.
1526 *
1527 * However:
1528 * - This assumes there won't be non-data-free datagrams following an EOF-marker one. Indeed that is
1529 * impossible, unless sender sends an entirely data-free datagram; we disallow this formally and also
1530 * even if that weren't followed, then by either updating m_n_used accordingly, or emitting `eof` error (see
1531 * after loop), we'll essentially ignore anything following the first EOF-marker -- no matter what that is.
1532 * That's fine.
1533 * - This doesn't care if, after EOF-marker 1, all available slots are filled with EOF-markers
1534 * (n_rcvd_msgs_or_error == n_payloads), or if some were left untouched (n_rcvd_msgs_or_error < n_payloads).
1535 * Any touched ones shall be reset; any untouched ones don't need any reset; and n_rcvd_msgs_actual (and
1536 * therefore m_n_used or *err_code) shall be set based on EOF-marker 1. That said as of this writing
1537 * in reality Linux at least will fill all subsequent slots with EOF-marker "datagrams"
1538 * (n_rcvd_msgs_or_error == n_payloads). Our code is agnostic of this and does not verify this.
1539 * - We could also (on seeing EOF-marker 1) stop the loop, etc. etc. However that actually seemed more complex
1540 * to code; particularly given our desire to reset the in-arg/out-args .msg_flags and .msg_controllen for
1541 * potential reuse/whatever. So we go the pithier way. */
1542 n_rcvd_msgs_actual = std::min(n_rcvd_msgs_actual,
1543 // Let the following arg's value be K in the above comment.
1544 size_t(size_t(n_rcvd_msgs_or_error)
1545 - size_t(recvmmsg_hdr_end - recvmmsg_hdr_ptr))); // This is always >= 1.
1546 } // if (eof_marker)
1547
1548 if (do_log_trace)
1549 {
1550 FLOW_LOG_TRACE_WITHOUT_CHECKING
1551 ("Local_stream_batch [" << *this << "]: "
1552 "recvmmsg[" << peer_hndl << "] reports for message index "
1553 "[" << (n_rcvd_msgs_or_error - (recvmmsg_hdr_end - recvmmsg_hdr_ptr)) << "] (0-based) receipt of possible "
1554 "native handle [" << mdt.m_result_hndl << "]; as well as "
1555 "[" << msg_len << "] bytes of the blob's capacity [" << target_payload_blob_sz << "]; "
1556 "EOF-marker? = [" << eof_marker << "]. "
1557 "Total in-message records received: [" << n_rcvd_msgs_or_error << "] out of max [" << n_payloads << "]. "
1558 "Potentially corrected (due to 1st EOF-marker record) in-message count: [" << n_rcvd_msgs_actual << "].");
1559 }
1560
1561 total_rcvd_bytes += msg_len; // EOF markers contribute 0; no extra cost.
1562
1563 // All done (for this in-message)!
1564 }
1565 while ((++mdt_ptr, ++recvmmsg_hdr_ptr) != recvmmsg_hdr_end);
1566
1567 assert(graceful_close);
1568 if (n_rcvd_msgs_actual == size_t(n_rcvd_msgs_or_error))
1569 {
1570 // Mainstream case. Note that `n_rcvd_msgs_or_error > 0`; hence 'n_rcvd_msgs_actual > 0'.
1571 *graceful_close = false;
1572 }
1573 else
1574 {
1575 // Occurs at most once per connection.
1576
1577 assert((n_rcvd_msgs_or_error > 0) && "n=0 and n=-1 should have been dealt with earlier + return.");
1578 *graceful_close = true;
1579
1580 if (do_log_trace)
1581 {
1582 FLOW_LOG_TRACE_WITHOUT_CHECKING
1583 ("Local_stream_batch [" << *this << "]: Connected local peer socket [" << peer_hndl << "] "
1584 "tried to batch-read up-to [" << n_payloads << "] in-messages, each "
1585 "potentially into a scattered-blob plus possibly a native handle per each; "
1586 "and got ostensibly [" << n_rcvd_msgs_or_error << "] of those; but only [" << n_rcvd_msgs_actual << "] were "
1587 "actual in-messages (0 = allowed), while the rest were EOF-markers; therefore emitting "
1588 "graceful-close=1 flag.");
1589 }
1590 }
1591
1592 err_code->clear();
1593
1594 if (total_rcvd_bytes_or_null)
1595 {
1596 *total_rcvd_bytes_or_null = total_rcvd_bytes;
1597 }
1598
1599 m_n_used += n_rcvd_msgs_actual; // m_n_used increases here; except it might be unchanged, if *graceful_close.
1600 return true; // OMG. Amazing.
1601} // Msg_batch_in::nb_read()
1602
1603template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
1605{
1606 auto& os = *os_ptr;
1607 os << "Lcl[slots-total/rdy/rcvd [" << m_mdts.size() << '/' << m_mdts.capacity() << '/' << m_n_used << "]]@" << this;
1608}
1609
1610template<typename Mutable_buffer_sequence_t, typename Msg_resource_t>
1611std::ostream& operator<<(std::ostream& os,
1613{
1614 val.to_ostream(&os);
1615 return os;
1616}
1617
1618// Free function template implementations.
1619
1620template<typename Protocol, typename Const_buffer_sequence>
1621size_t nb_write_some_with_native_handle(flow::log::Logger* logger_ptr,
1622 Peer_socket<Protocol>* peer_socket_ptr,
1623 Native_handle payload_hndl,
1624 const Const_buffer_sequence& payload_blob,
1625 Error_code* err_code)
1626{
1627 using util::Blob_const;
1628 using boost::system::system_category;
1629 using boost::array;
1630 using boost::asio::detail::buffer_sequence_adapter;
1631 using std::is_same_v;
1632 using std::memcpy;
1633 namespace sys_err_codes = boost::system::errc;
1634 using ::sendmsg;
1635 using ::msghdr;
1636 using ::cmsghdr;
1637 // using ::SOL_SOCKET; // It's a macro apparently.
1638 using ::SCM_RIGHTS;
1639 using ::MSG_DONTWAIT;
1640 using ::MSG_NOSIGNAL;
1641 // using ::errno; // It's a macro apparently.
1642
1643 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(size_t, (nb_write_some_with_native_handle<Protocol, Const_buffer_sequence>),
1644 logger_ptr, peer_socket_ptr, payload_hndl, payload_blob, _1);
1645 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
1646
1647 /* To reader/maintainer: The below isn't that difficult, but if you need exact understanding and definitely if you
1648 * plan to make changes -- even just clarifications or changes in the doc header -- then read the entire doc header
1649 * of nb_read_some_with_native_handle() first. */
1650
1651 assert(peer_socket_ptr);
1652 auto& peer_socket = *peer_socket_ptr;
1653
1654 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_TRANSPORT);
1655
1656 /* Let us send as much as possible of payload_blob; and the native socket payload_hndl.
1657 * (For Protocol_pkt_stream as much as possible -- if not none -- shall be *all* of payload_blob.)
1658 * As explicitly documented in boost.asio docs: it does not provide an API for the latter (fairly hairy
1659 * and Linux-specific) feature, but it is doable by using sendmsg() natively via peer_socket.native_handle()
1660 * which gets the native handle (a/k/a FD). */
1661
1662 buffer_sequence_adapter<Blob_const, Const_buffer_sequence> buf_seq_adapter{payload_blob}; // See explanation below.
1663 const size_t payload_blob_sz = buf_seq_adapter.total_size();
1664
1665 FLOW_LOG_TRACE("Connected local peer socket wants to write scattered-blob "
1666 "([" << buf_seq_adapter.count() << "] sub-blobs, total size [" << payload_blob_sz << "], "
1667 "first sub-blob at @[" << buf_seq_adapter.buffers()->iov_base << "]) "
1668 "plus native handle [" << payload_hndl << "]. Will try to send.");
1669
1670 /* What's left is to set up msg_control[len] which is how we send the native handle (payload_hndl).
1671 * This is based on example snippet taken from Linux `man cmsg`. FWIW it's rather difficult to figure out
1672 * how to write it from the rest of the documentation, which is likely complete and correct but opaque; the
1673 * example is invaluable. Note we are particularly using the SOL_SOCKET/SCM_RIGHTS technique. */
1674
1675 Msg_control_as_union msg_control_as_union;
1676 /* Caution! `man cmsg` in Linux says:
1677 * "When initializing a buffer that will contain a series of cmsghdr structures (e.g., to be sent with sendmsg(2)),
1678 * that buffer should first be zero-initialized to ensure the correct operation of CMSG_NXTHDR()."
1679 * We don't use CMSG_NXTHDR() though, so skip `msg_control_as_union.m_buf.fill(0)` here for perf.
1680 * Careful! May need to add this later, if adding more types of ancillary data (but not if merely increasing
1681 * N_PAYLOAD_FDS). */
1682
1683 msghdr sendmsg_hdr =
1684 {
1685 nullptr, // msg_name - Address; not used; we are connected.
1686 0, // msg_namelen - Ditto.
1687 /* msg_iov - Scatter/gather array. Here we use boost.asio's neat buffer_sequence_adapter which in this OS
1688 * does iovec[] work for us: sets up the array and the count and computes the total size as it goes.
1689 * (See doc header of Msg_batch_in::Mdt_per_payload::m_buf_seq_adapter for relevant info/defense of its use.) */
1690 buf_seq_adapter.buffers(),
1691 buf_seq_adapter.count(), // msg_iovlen - # elements in msg_iov.
1692 // The next 2 fields msg_control[len] contain ancillary data to send; we used it for the FDs.
1693 msg_control_as_union.m_buf.data(), // Still need to load it with stuff after this (uninit for now).
1695 0 // msg_flags is unused. The language won't let me leave it as garbage though.
1696 };
1697
1698 // Load up .msg_control with the FDs (accessed via macro thingies).
1699
1700 cmsghdr* const sendmsg_hdr_cmsg_ptr = CMSG_FIRSTHDR(&sendmsg_hdr); // Or cast from &m_buf; or use &m_align....
1701 sendmsg_hdr_cmsg_ptr->cmsg_level = SOL_SOCKET;
1702 sendmsg_hdr_cmsg_ptr->cmsg_type = SCM_RIGHTS;
1703 sendmsg_hdr_cmsg_ptr->cmsg_len = CMSG_LEN(sizeof(Native_handle::handle_t) * N_PAYLOAD_FDS);
1704 // Copy the FDs. We have just the one; copy just one thing but static_assert() to help future-proof.
1705 static_assert(N_PAYLOAD_FDS == 1, "Should be only passing one native handle into sendmsg() as of this writing.");
1706 memcpy(CMSG_DATA(sendmsg_hdr_cmsg_ptr), &payload_hndl.m_native_handle, sizeof(payload_hndl.m_native_handle));
1707
1708 const auto n_sent_or_error
1709 = sendmsg(peer_socket.native_handle(), &sendmsg_hdr,
1710 /* If socket is un-writable then don't block;
1711 * EAGAIN/EWOULDBLOCK instead. Doing it this way is easier than messing with fcntl(), particularly
1712 * since we're working with a boost.asio-managed socket; that's actually fine -- there's a portable
1713 * API native_non_blocking() in boost.asio for setting this -- but feels like the less interaction between
1714 * portable boost.asio code we use and this native stuff, the better -- so just keep it local here. */
1715 MSG_DONTWAIT | MSG_NOSIGNAL);
1716 // ^-- Dealing with SIGPIPE is a pointless pain; if conn closed that way just give us an EPIPE error.
1717
1718 if (n_sent_or_error == -1)
1719 {
1720 /* Not even 1 byte of the blob was sent; and hence nor was payload_hndl. (Docs don't explicitly say that 2nd
1721 * part after the semicolon, but it's essentially impossible that it be otherwise, as then it'd be unknowable.
1722 * Update: Confirmed in kernel source and supported by this delightfully reassuring (in several ways) link:
1723 * [ https://gist.github.com/kentonv/bc7592af98c68ba2738f4436920868dc ] (Googled "SCM_RIGHTS gist").) */
1724
1725 const Error_code sys_err_code{errno, system_category()};
1726 if ((sys_err_code == sys_err_codes::operation_would_block) || // EWOULDBLOCK
1727 (sys_err_code == sys_err_codes::resource_unavailable_try_again)) // EAGAIN (same meaning)
1728 {
1729 FLOW_LOG_TRACE("Write attempt indicated would-block; not an error condition. Nothing sent.");
1730 /* Subtlety: We could just set it to sys_err_code; but we specifically promised in contract we'd set it to the
1731 * boost::asio::error::would_block representation of would-block condition. Why did we promise that? 2 reasons:
1732 * 1, that is what boost.asio's own spiritually similar Peer_socket::write_some() would do in non_blocking() mode.
1733 * 2, then we can promise a specific code instead of making them check for the above 2 errno values.
1734 *
1735 * Subtlety: net_flow::Peer_socket::sync_receive() uses somewhat different semantics; it indicates would-block by
1736 * returning 0 *but* a falsy *err_code. Why are we inconsistent with that? Answer: Because net_flow is not
1737 * trying to be a boost.asio extension; we are. In net_flow's context (as of this writing) no one is surprised
1738 * when semantics are somewhat different from boost.asio; but in our context they might be quite surprised
1739 * indeed. */
1740 *err_code = boost::asio::error::would_block;
1741 return 0;
1742 }
1743 // else
1744
1745 assert(sys_err_code);
1746
1747 // All other errors are fatal.
1748 FLOW_ERROR_SYS_ERROR_LOG_WARNING(); // Log based on sys_err_code.
1749 FLOW_LOG_WARNING("Connected local peer socket tried to write scattered-blob "
1750 "([" << buf_seq_adapter.count() << "] sub-blobs, total size [" << payload_blob_sz << "], "
1751 "first sub-blob at @[" << buf_seq_adapter.buffers()->iov_base << "]) "
1752 "plus native handle [" << payload_hndl << "]; "
1753 "but an unrecoverable error occurred. Nothing sent.");
1754 *err_code = sys_err_code;
1755 return 0;
1756 } // if (n_sent_or_error == -1)
1757 // else if (n_sent_or_error != -1)
1758
1759 if constexpr(is_same_v<Protocol, Protocol_pkt_stream>)
1760 {
1761 assert((static_cast<size_t>(n_sent_or_error) == payload_blob_sz)
1762 && "sendmsg() returned neither -1 nor full-dgram-sent length; unexpected behavior for SEQPACKET! "
1763 "Dgram too large? Misunderstood/misdocumented OS API?");
1764
1765 FLOW_LOG_TRACE("sendmsg() reports the native handle [" << payload_hndl << "] was successfully sent; as "
1766 "were all of the dgram blob's [" << payload_blob_sz << "] bytes.");
1767 }
1768 else
1769 {
1770 assert((n_sent_or_error > 0)
1771 && "sendmsg() should return either -1 or a positive number (indicates success).");
1772
1773 FLOW_LOG_TRACE("sendmsg() reports the native handle [" << payload_hndl << "] was successfully sent; as "
1774 "were [" << n_sent_or_error << "] of the blob's [" << payload_blob_sz << "] bytes.");
1775 }
1776
1777 err_code->clear();
1778 return static_cast<size_t>(n_sent_or_error);
1779} // nb_write_some_with_native_handle()
1780
1781template<typename Protocol, typename Mutable_buffer_sequence>
1782size_t nb_read_some_with_native_handle(flow::log::Logger* logger_ptr,
1783 Peer_socket<Protocol>* peer_socket_ptr,
1784 Native_handle* target_payload_hndl_ptr,
1785 const Mutable_buffer_sequence& target_payload_blob,
1786 Error_code* err_code,
1787 int message_flags)
1788{
1789 using flow::log::Sev;
1790 using boost::asio::detail::buffer_sequence_adapter;
1791 using boost::io::ios_all_saver;
1792 using boost::system::system_category;
1793 using boost::array;
1794 using std::is_same_v;
1795 using std::memcpy;
1796 namespace sys_err_codes = boost::system::errc;
1797 using ::recvmsg;
1798 using ::msghdr;
1799 using ::cmsghdr;
1800 // using ::SOL_SOCKET; // It's a macro apparently.
1801 using ::SCM_RIGHTS;
1802 using ::MSG_DONTWAIT;
1803 using ::MSG_TRUNC;
1804 using ::MSG_CTRUNC;
1805 // using ::errno; // It's a macro apparently.
1806
1807 FLOW_ERROR_EXEC_AND_THROW_ON_ERROR(size_t, (nb_read_some_with_native_handle<Protocol, Mutable_buffer_sequence>),
1808 logger_ptr, peer_socket_ptr, target_payload_hndl_ptr,
1809 target_payload_blob, _1, message_flags);
1810 // ^-- Call ourselves and return if err_code is null. If got to present line, err_code is not null.
1811
1812 /* To reader/maintainer: The below isn't that difficult, but if you need exact understanding and definitely if you
1813 * plan to make changes -- even just clarifications or changes in the doc header -- then read the entire doc header
1814 * of nb_read_some_with_native_handle() first. */
1815
1816 assert(peer_socket_ptr);
1817 assert(target_payload_hndl_ptr);
1818
1819 buffer_sequence_adapter<util::Blob_mutable, Mutable_buffer_sequence> buf_seq{target_payload_blob};
1820
1821 auto& peer_socket = *peer_socket_ptr;
1822 auto& target_payload_hndl = *target_payload_hndl_ptr;
1823
1824 FLOW_LOG_SET_CONTEXT(logger_ptr, Log_component::S_TRANSPORT);
1825 target_payload_hndl = {}; // We promised to set to this if no native-handle received (including on error).
1826
1827 /* Let us receive as much as possible into buf_seq area up to its size; and a native socket (if any) into
1828 * target_payload_hndl (if none, then it'll remain null()).
1829 * As explicitly documented in boost.asio docs: it does not provide an API for the latter (fairly hairy
1830 * and Linux-specific) feature, but it is doable by using recvmsg() natively via peer_socket.native_handle()
1831 * which gets the native handle (a/k/a FD).
1832 *
1833 * Recommending first looking at nb_write_some_with_native_handle(), as we operate symmetrically/similarly. */
1834
1835 const size_t target_payload_blob_sz = buf_seq.total_size();
1836
1837 /* Set up .msg_control*. This, and then interpreting the output after the call, is based on the out-equivalent in
1838 * nb_write_some_with_native_handle() as well as cross-referencing with some Internet sources.
1839 *
1840 * Before the call, we must reserve space for the ancillary out-data, if any; set msg_control to point to that;
1841 * and msg_controllen to the size of that thing. */
1842
1843 Msg_control_as_union msg_control_as_union;
1844 /* Caution! `man cmsg` in Linux says:
1845 * "When initializing a buffer that will contain a series of cmsghdr structures (e.g., to be sent with sendmsg(2)),
1846 * that buffer should first be zero-initialized to ensure the correct operation of CMSG_NXTHDR()."
1847 * (We are recv*(), not send*(), but seems it would apply too... unless kernel would do it in that case.)
1848 * We don't use CMSG_NXTHDR() though, so skip `msg_control_as_union.m_buf.fill(0)` here for perf.
1849 * Careful! May need to add this later, if adding more types of ancillary data (but not if merely increasing
1850 * N_PAYLOAD_FDS). */
1851
1852 msghdr recvmsg_hdr =
1853 {
1854 nullptr, // msg_name - Address; not used; we are connected.
1855 0, // msg_namelen - Ditto.
1856 /* msg_iov - Scatter/gather array. See similarly-themed notes at similar
1857 * spot in nb_write_some_with_native_handle(); they apply here too, more or less. */
1858 buf_seq.buffers(),
1859 // msg_iovlen - # elements in msg_iov. This is an *input* arg only; output is total length read: the ret value.
1860 buf_seq.count(),
1861 /* The following fields:
1862 * msg_control - In-arg; points to complex ancillary data area (which can only be properly navigated using
1863 * CMSG* macros) which is an out-arg of sorts. This value itself is an in-arg however.
1864 * msg_controllen - In-arg *and* out-arg. As in-arg, it's the length of area pointed by msg_control.
1865 * As out-arg, it describes the size of resulting ancillary data; however it is not to be itself checked
1866 * or used apparently; CMSG* macros shall be used to traverse ancillary data. Nevertheless msg_controllen
1867 * can/will be set by recv[m]msg() (not important for us here but can be important if reusing inputs in
1868 * a subsequent calls; see class Msg_batch_in).
1869 * msg_flags - This is an out-arg containing special feature flags. These may be checked below after call. */
1870 msg_control_as_union.m_buf.data(),
1872 0 // May or may not need to be initialized, but the language won't let me avoid it, and anyway it seems prudent.
1873 };
1874
1875 // Massage message_flags for our required purposes. Re. message_flags please read notes in our doc header.
1876
1877 /* If socket is un-readable then don't block; EAGAIN/EWOULDBLOCK instead. ...Further comment omitted;
1878 * see sendmsg() elsewhere in this header. Same thing here. */
1879 if constexpr(is_same_v<Protocol, Protocol_pkt_stream>)
1880 {
1881 // For dgram-based protocol: Also use MSG_TRUNC, so we can (at least) log some info on overflow.
1882 message_flags = (message_flags | (MSG_DONTWAIT | MSG_TRUNC));
1883 }
1884 else
1885 {
1886 message_flags = (message_flags | MSG_DONTWAIT);
1887 }
1888
1889 const auto n_rcvd_or_error
1890 = recvmsg(peer_socket.native_handle(), &recvmsg_hdr, message_flags);
1891
1892 // Carefully check all the outputs. Return value first.
1893
1894 if (n_rcvd_or_error == -1)
1895 {
1896 /* Not even 1 byte of a blob was read; and hence nor was any target_payload_hndl. (See comment in similar
1897 * spot in nb_write_some_with_native_handle(); applies here equally.) */
1898
1899 const Error_code sys_err_code{errno, system_category()};
1900 if ((sys_err_code == sys_err_codes::operation_would_block) || // EWOULDBLOCK
1901 (sys_err_code == sys_err_codes::resource_unavailable_try_again)) // EAGAIN (same meaning)
1902 /* Reader: "But, ygoldfel, why didn't you just check errno itself against those 2 actual Evalues?"
1903 * ygoldfel: "Because it makes me feel better about portable-ish style. Shut up, that's why. Get off my lawn." */
1904 {
1905 FLOW_LOG_TRACE("Connected local peer socket tried to read into scattered-blob "
1906 "([" << buf_seq.count() << "] sub-blobs, "
1907 "up to total size [" << target_payload_blob_sz << "], "
1908 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
1909 "plus possibly a native handle; and read attempt indicated would-block; "
1910 "not an error condition. Nothing received.");
1911 // Subtlety x 2: ...omitted. See similar spot in nb_write_some_with_native_handle(). Same here.
1912 *err_code = boost::asio::error::would_block;
1913 return 0; // target_payload_hndl already set.
1914 }
1915 // else
1916
1917 assert(sys_err_code);
1918
1919 // All other errors are fatal.
1920 FLOW_ERROR_SYS_ERROR_LOG_WARNING(); // Log based on sys_err_code.
1921 FLOW_LOG_WARNING("Connected local peer socket tried to read into scattered-blob "
1922 "([" << buf_seq.count() << "] sub-blobs, "
1923 "up to total size [" << target_payload_blob_sz << "], "
1924 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
1925 "plus possibly a native handle; but an unrecoverable error occurred. Nothing received.");
1926 *err_code = sys_err_code;
1927 return 0; // target_payload_hndl already set.
1928 } // if (n_rcvd_or_error == -1)
1929 // else if (n_rcvd_or_error != -1)
1930
1931 if (n_rcvd_or_error == 0)
1932 {
1933 /* WARNING doesn't feel right: it's a graceful connection end.
1934 * INFO could be good, but it might be too verbose depending on the application.
1935 * Use TRACE to be safe; caller can always log differently if desired. */
1936 FLOW_LOG_TRACE("Connected local peer socket tried to read into scattered-blob "
1937 "([" << buf_seq.count() << "] sub-blobs, "
1938 "up to total size [" << target_payload_blob_sz << "], "
1939 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
1940 "plus possibly a native handle; "
1941 "but it returned EOF meaning orderly connection shutdown by peer. Nothing received.");
1942 *err_code = boost::asio::error::eof;
1943 return 0; // target_payload_hndl already set.
1944 }
1945 // else if (n_rcvd_or_error > 0):
1946
1947 err_code->clear(); // Used by some checks below to mean "no error condition found yet."
1948
1949 // Check for in-dgram overflow (if applicable).
1950 if constexpr(is_same_v<Protocol, Protocol_pkt_stream>)
1951 {
1952 if (size_t(n_rcvd_or_error) > target_payload_blob_sz) // This is detectable due to *input* flag MSG_TRUNC.
1953 {
1954 /* As advertised log a WARNING and treat it as an error. Officially we said the in-dgram is eaten, and it is;
1955 * but in reality also a truncated in-dgram is now in the target buffer(s), though we didn't mention that. */
1956 FLOW_LOG_WARNING("Connected local peer socket tried to read into scattered-blob "
1957 "([" << buf_seq.count() << "] sub-blobs, "
1958 "up to total size [" << target_payload_blob_sz << "], "
1959 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
1960 "plus possibly a native handle; "
1961 "and it returned it read [" << n_rcvd_or_error << "] bytes successfully, but this overflows "
1962 "the aforementioned total-size; emitting error accordingly. Socket remains operational, but "
1963 "the overflowing in-dgram is lost.");
1964
1965 // Sanity-check that the *output* flag MSG_TRUNC is active (<=> ret value shows overflow, per docs + tests).
1966 assert((recvmsg_hdr.msg_flags & MSG_TRUNC)
1967 && "In-dgram size overflows user-supplied buffer <=> MSG_TRUNC out-flag supposed to be set.");
1968
1970 /* target_payload_hndl already set.
1971 * Would `return 0` here but want to avoid leaking received handles, so defer until below. */
1972 }
1973 // else { No overflow. }
1974 }
1975 // else { No overflow possible in stream mode. }
1976
1977 /* Assuming no error found yet:
1978 *
1979 * Next, buf_seq-pointed area... which is already written to (its first n_rcvd_or_error bytes). Nothing to do.
1980 *
1981 * Next, recvmsg_hdr.msg_flags. Basically only the following is relevant: */
1982 if ((!*err_code) && (recvmsg_hdr.msg_flags != 0))
1983 {
1984 if (logger_ptr && logger_ptr->should_log(Sev::S_INFO, get_log_component()))
1985 {
1986 ios_all_saver saver{*(logger_ptr->this_thread_ostream())}; // Revert std::hex/etc. soon.
1987 FLOW_LOG_INFO_WITHOUT_CHECKING
1988 ("Connected local peer socket tried to read into scattered-blob "
1989 "([" << buf_seq.count() << "] sub-blobs, "
1990 "up to total size [" << target_payload_blob_sz << "], "
1991 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
1992 "plus possibly a native handle; "
1993 "and it returned it read [" << n_rcvd_or_error << "] bytes successfully but also returned raw "
1994 "out-flags value [0x" << std::hex << recvmsg_hdr.msg_flags << "]. "
1995 "Will check for relevant flags but otherwise "
1996 "ignoring if nothing bad. Logging at elevated level because it's interesting; please investigate.");
1997 }
1998
1999 if ((recvmsg_hdr.msg_flags & MSG_CTRUNC) != 0)
2000 {
2001 if (logger_ptr && logger_ptr->should_log(Sev::S_WARNING, get_log_component()))
2002 {
2003 ios_all_saver saver{*(logger_ptr->this_thread_ostream())}; // Revert std::hex/etc. soon.
2004 FLOW_LOG_WARNING_WITHOUT_CHECKING
2005 ("Connected local peer socket tried to read into scattered-blob "
2006 "([" << buf_seq.count() << "] sub-blobs, "
2007 "up to total size [" << target_payload_blob_sz << "], "
2008 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
2009 "plus possibly a native handle; "
2010 "and it returned it read [" << n_rcvd_or_error << "] bytes successfully but also returned raw "
2011 "out-flags value [0x" << std::hex << recvmsg_hdr.msg_flags << "] which includes MSG_CTRUNC. "
2012 "That flag indicates more stuff was sent as ancillary data; but we expect at most 1 native "
2013 "handle. Other side sent something strange. Acting as if nothing received + error.");
2014 }
2016 /* target_payload_hndl already set.
2017 * Would `return 0` here but want to avoid leaking received handles, so defer until below. */
2018 }
2019 // else { No truncation occurred. }
2020 } // if ((!*err_code) && (recvmsg_hdr.msg_flags != 0)) (but *err_code may have become truthy inside)
2021
2022 /* Lastly examine ancillary data (which might have also been truncated: MSG_CTRUNC above).
2023 * Use, basically, the method from `man cmsg` in Linux.
2024 *
2025 * We are interested whether *err_code (un-leak any received hndl) or !*err_code (emit any received hndl). */
2026 cmsghdr* const recvmsg_hdr_cmsg_ptr = CMSG_FIRSTHDR(&recvmsg_hdr);
2027 if (recvmsg_hdr_cmsg_ptr)
2028 {
2029 // There is some ancillary data. It can only (validly according to our expected protocol) be one thing.
2030 if ((recvmsg_hdr_cmsg_ptr->cmsg_level == SOL_SOCKET) &&
2031 (recvmsg_hdr_cmsg_ptr->cmsg_type == SCM_RIGHTS))
2032 {
2033 static_assert(N_PAYLOAD_FDS == 1,
2034 "Should be only dealing with one native handle with recvmsg() as of this writing.");
2035 memcpy(&target_payload_hndl.m_native_handle, CMSG_DATA(recvmsg_hdr_cmsg_ptr),
2036 sizeof(target_payload_hndl.m_native_handle));
2037
2038 if (*err_code)
2039 {
2040 target_payload_hndl.close(); // Avoid the native-handle (which is a received copy) leak.
2041 }
2042 // else { Cool: Native-handle received and emitted. }
2043 }
2044 else if (!*err_code) // && (unknown ancillary data type)
2045 {
2046 FLOW_LOG_WARNING("Connected local peer socket tried to read into scattered-blob "
2047 "([" << buf_seq.count() << "] sub-blobs, "
2048 "up to total size [" << target_payload_blob_sz << "], "
2049 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
2050 "plus possibly a native handle; "
2051 "and it returned it read [" << n_rcvd_or_error << "] bytes successfully but also "
2052 "unexpected ancillary data of cmsg_level|cmsg_type "
2053 "[" << recvmsg_hdr_cmsg_ptr->cmsg_level << '|' << recvmsg_hdr_cmsg_ptr->cmsg_type << "]. "
2054 "Acting as if nothing received + error.");
2056 // target_payload_hndl already set. We shall return below.
2057 }
2058 // else if (*err_code && (unknown ancillary data type)) { Detected error before; stick with that one. }
2059
2060 /* Used to check here that CMSG_NXTHDR() would yield null (meaning no more ancillary data) and issued
2061 * LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL if not. However it was a perf giveaway that was likely pointless,
2062 * even with a misbehaving opposing peer; we've only provided space for 1 FD, and MSG_CTRUNC (checked above) would
2063 * have pointed out if that were insufficient. So just move on. */
2064 }
2065 // else { No ancillary data, meaning no native handle; that's quite normal. }
2066
2067 if (*err_code)
2068 {
2069 return 0; // Any potential handle-leak avoided; can get out.
2070 }
2071 // else:
2072
2073 if constexpr(is_same_v<Protocol, Protocol_pkt_stream>)
2074 {
2075 FLOW_LOG_TRACE("Connected local peer socket tried to read into scattered-blob "
2076 "([" << buf_seq.count() << "] sub-blobs, "
2077 "up to total size [" << target_payload_blob_sz << "], "
2078 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
2079 "plus possibly a native handle; recvmsg() reports receipt of possible "
2080 "native handle [" << target_payload_hndl << "]; as well as "
2081 "an in-dgram taking [" << n_rcvd_or_error << "] of the "
2082 "blob's [" << target_payload_blob_sz << "]-byte capacity.");
2083 }
2084 else
2085 {
2086 FLOW_LOG_TRACE("Connected local peer socket tried to read into scattered-blob "
2087 "([" << buf_seq.count() << "] sub-blobs, "
2088 "up to total size [" << target_payload_blob_sz << "], "
2089 "first sub-blob at @[" << buf_seq.buffers()->iov_base << "]) "
2090 "plus possibly a native handle; recvmsg() reports receipt of possible "
2091 "native handle [" << target_payload_hndl << "]; as well as "
2092 "[" << n_rcvd_or_error << "] of the blob's [" << target_payload_blob_sz << "]-byte capacity.");
2093 }
2094
2095 return static_cast<size_t>(n_rcvd_or_error);
2096} // nb_read_some_with_native_handle()
2097
2098// Opt_peer_process_credentials template implementations.
2099
2100template<typename Protocol>
2101int Opt_peer_process_credentials::level(const Protocol&) const
2102{
2103 return AF_LOCAL;
2104}
2105
2106template<typename Protocol>
2107int Opt_peer_process_credentials::name(const Protocol&) const
2108{
2109 return SO_PEERCRED;
2110}
2111
2112template<typename Protocol>
2114{
2115 return static_cast<void*>(static_cast<util::Process_credentials*>(this));
2116}
2117
2118template<typename Protocol>
2119size_t Opt_peer_process_credentials::size(const Protocol&) const
2120{
2121 return sizeof(util::Process_credentials);
2122}
2123
2124template<typename Protocol>
2125void Opt_peer_process_credentials::resize(const Protocol& proto, size_t new_size_but_really_must_equal_current) const
2126{
2127 using flow::util::ostream_op_string;
2128 using std::length_error;
2129
2130 if (new_size_but_really_must_equal_current != size(proto))
2131 {
2132 throw length_error(ostream_op_string
2133 ("Opt_peer_process_credentials does not actually support resizing; requested size [",
2134 new_size_but_really_must_equal_current, "] differs from forever-size [",
2135 size(proto), "]. boost.asio internal bug or misuse?"));
2136 }
2137}
2138
2139} // namespace ipc::transport::asio_local_stream_socket
Similarly to the transport::Msg_batch_in concept (as taken by API {Blob|Native_handle}_receiver::asyn...
std::ostream & operator<<(std::ostream &os, const Msg_batch_in< Mutable_buffer_sequence_t, Msg_resource_t > &val)
Serializes a Msg_batch_in to a standard output stream.
Native_handle result_payload_hndl(size_t idx) const
Obtains a copy of the Native_handle (potentially .null()) in an earlier-received-to slot.
bool full() const
Identical to Generic_msg_batch_in.
Msg_batch_in(size_t max_msg_count)
Identical to ctor of Generic_msg_batch_in.
void to_ostream(std::ostream *os) const
Identical to Generic_msg_batch_in.
void reuse_result_payloads(size_t start_idx, const Is_unused_func &is_unused_func)
Scans every in-message slot in range [start_idx, this->n_used()); performantly rearranges this range ...
static constexpr int S_PER_MSG_FLAGS
Value we give for msghdr::msg_flags as in-arg. It is also an out-arg, so sometimes we must reassign t...
Msg_resource_t Msg_resource
Alias for template param Msg_resource_t.
size_t result_payload_blob(size_t idx, Msg_resource **msg_resource_ptr=nullptr)
Obtains information, chiefly the size of received data <= total-size of corresponding target_blob fro...
~Msg_batch_in()
Clears all resources – including any Msg_resource currently attached to any receive-slot.
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])...
void clear_used(size_t new_n_used=0)
Makes it so that n_used() would return zero (or, in advanced form, another value).
bool initialized() const
Identical to Generic_msg_batch_in.
std::vector< Mdt_per_payload > m_mdts
The slots, excluding what is in m_mmsg_hdrs.
std::vector<::mmsghdr > m_mmsg_hdrs
The slots, excluding what is in m_mdts; a sub-array of this is passed by nb_read() to recvmmsg().
void prepare_target_payload(const Mutable_buffer_sequence &target_blob, Msg_resource &&msg_resource, size_t idx=-1)
Identical to Generic_msg_batch_in (though the type of target_blob is potentially more complex).
bool nb_read(flow::log::Logger *logger_ptr, Peer_socket< Protocol_pkt_stream > *peer_socket_ptr, bool *graceful_close, Error_code *err_code=nullptr, int message_flags=0, size_t *total_rcvd_bytes_or_null=nullptr)
Performs a high-perf non-blocking batched-receive into the batch area with indices [n_used(),...
Mutable_buffer_sequence_t Mutable_buffer_sequence
Buffer(s) description type (conforming to boost.asio MutableBufferSequence concept) supported by *thi...
Gettable (read-only) socket option for use with asio_local_stream_socket::Peer_socket ....
size_t size(const Protocol &proto) const
For internal boost.asio use, to enable Peer_socket::get_option(Opt_peer_process_credentials&) to work...
Opt_peer_process_credentials()
Default ctor: each value is initialized to zero or equivalent.
Opt_peer_process_credentials & operator=(const Opt_peer_process_credentials &src)
Boring copy assignment.
int level(const Protocol &proto) const
For internal boost.asio use, to enable Peer_socket::get_option(Opt_peer_process_credentials&) to work...
Opt_peer_process_credentials(const Opt_peer_process_credentials &src)
Boring copy ctor.
void * data(const Protocol &proto)
For internal boost.asio use, to enable Peer_socket::get_option(Opt_peer_process_credentials&) to work...
int name(const Protocol &proto) const
For internal boost.asio use, to enable Peer_socket::get_option(Opt_peer_process_credentials&) to work...
void resize(const Protocol &proto, size_t new_size_but_really_must_equal_current) const
For internal boost.asio use, to enable Peer_socket::get_option(Opt_peer_process_credentials&) to work...
A process's credentials (PID, UID, GID as of this writing).
Additional (versus boost.asio) APIs for advanced work with local (Unix domain) stream and connected-d...
typename Protocol::socket Peer_socket
Short-hand for boost.asio Unix domain peer stream-socket (usually-connected-or-empty guy).
size_t nb_read_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket< Protocol > *peer_socket_ptr, Native_handle *target_payload_hndl_ptr, const Mutable_buffer_sequence &target_payload_blob, Error_code *err_code, int message_flags)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->receive(target...
constexpr size_t N_PAYLOAD_FDS
Our transport::asio_local_stream_socket utilities support transmitting at most this many native handl...
size_t nb_write_some_with_native_handle(flow::log::Logger *logger_ptr, Peer_socket< Protocol > *peer_socket_ptr, Native_handle payload_hndl, const Const_buffer_sequence &payload_blob, Error_code *err_code)
boost.asio extension similar to peer_socket->non_blocking(true); auto n = peer_socket->send(payload_b...
@ S_LOW_LVL_UNEXPECTED_STREAM_PAYLOAD_BEYOND_HNDL
Unable to receive incoming traffic: message contains more than: 1 blob plus 0-1 native handles.
@ S_INVALID_ARGUMENT
User called an API with 1 or more arguments against the API spec.
@ S_MESSAGE_SIZE_EXCEEDS_USER_STORAGE
User protocol-code mismatch: local user-provided storage cannot fit entire message received from oppo...
void swap(Bipc_mq_handle &val1, Bipc_mq_handle &val2)
Implements Persistent_mq_handle related concept: Swaps two objects.
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
The Msg_resource type attached to each slot of our internal m_batch.
Per-batch-slot information beyond what is stored in the corresponding slot's native mmsghdr m_mmsg_hd...
Mdt_per_payload(const Mutable_buffer_sequence &buf_seq, Msg_resource &&msg_resource)
Ctor.
Native_handle m_result_hndl
The target/result Native_handle to receive-to for this slot; null originally; null or not upon a rece...
void swap(Mdt_per_payload *that)
Implements maximally efficient swapping, as needed by reuse_result_payloads().
Msg_control_as_union m_msg_control_as_union
Stores this slot's ancillary-data cmsg* structure, into which ancillary data if any are received.
boost::asio::detail::buffer_sequence_adapter< util::Blob_mutable, Mutable_buffer_sequence > m_buf_seq_adapter
Efficient (via adapt-as-we-go technique) object that adapts the boost.asio portable Mutable_buffer_se...
Msg_resource m_resource
Per-slot resource moved from user's object via prepare_target_payload().
A monolayer-thin wrapper around a native handle, a/k/a descriptor a/k/a FD.
void close() noexcept
Little utility that returns m_native_handle to the OS.
int handle_t
The native handle type. Much logic relies on this type being light-weight (fast to copy).
handle_t m_native_handle
The native handle (possibly equal to S_NULL_HANDLE), the exact payload of this Native_handle.
Ancillary data (for recv[m]msg() and send[m]msg()) buffer, used in our case for storing transmitted n...
boost::array< uint8_t, CMSG_SPACE(sizeof(Native_handle::handle_t) *N_PAYLOAD_FDS)> m_buf
The ancillary-data buffer.