xref: /aosp_15_r20/external/pigweed/seed/0114.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1.. _seed-0114:
2
3==============
40114: Channels
5==============
6.. seed::
7   :number: 114
8   :name: Channels
9   :status: Accepted
10   :proposal_date: 2023-10-10
11   :cl: 175471
12   :authors: Wyatt Hepler
13   :facilitator: Carlos Chinchilla
14
15-------
16Summary
17-------
18This document proposes a new ``pw_channel`` module and
19:cpp:class:`pw::channel::Channel` class. The module is similar to
20pw_stream, with three key changes:
21
22- Supports byte stream or datagram semantics (:ref:`module-pw_stream` is a
23  subset of ``pw_channel``).
24- Provides an asynchronous API (:ref:`SEED-0112`).
25- Uses the :ref:`SEED-0109` buffers system, which enables zero-copy
26  and vectored I/O.
27
28``pw_channel`` will provide the data transmit and receive API for the upcoming
29socket abstraction.
30
31--------
32Proposal
33--------
34This SEED proposes the following:
35
36- Introduce a new ``pw_channel`` module.
37- Introduce a :cpp:class:`pw::channel::Channel` virtual interface.
38  Implementors of this interface may expose byte stream or datagram semantics.
39  All operations in this interface are async and use :ref:`seed-0109` buffers
40  to provide zero-copy operations.
41- Use ``pw_channel`` as the basis for the upcoming Pigweed sockets API.
42- Replace ``pw_stream`` with ``pw_channel`` to the extent possible.
43
44----------
45Motivation
46----------
47One of the fundamental operations of computing is sending data from one place to
48another. Examples include exchanging data with
49
50- an in-memory data structure,
51- a file in a filesystem,
52- a hardware peripheral,
53- another process, or
54- a device across a network.
55
56There are many interfaces for data exchange. Pigweed provides
57:ref:`module-pw_stream`, which is a simple synchronous API for transmitting and
58receiving data. ``pw_stream``'s simple model has made it prevalent in Pigweed.
59
60The Pigweed team is revamping its communications systems (see :ref:`seed-0107`).
61The new sockets API will be a critical piece of that story. The core job of a
62socket is to exchange data with another node in the network. ``pw_stream``'s
63purpose is to facilitate data exchange, but it is too limited for sockets.
64pw_stream is missing support for several features, including:
65
66- Datagrams – Data is a byte stream. Datagrams are not supported.
67- Unreliability – Sockets may not guarantee delivery of data. ``pw_stream``
68  assumes no data is lost if ``Write`` returns ``OK``.
69- Asynchronous operations – All functions block until the operation completes.
70- Zero copy operations – All reads and writes require data to be copied.
71- Vectored I/O – All reads and writes use a single contiguous buffer.
72- Backpressure – There is no mechanism for a stream to notify the producer that
73  it needs more time to process data.
74
75These features are fairly complex and may be exposed in a variety of ways in an
76API. This SEED proposes a new ``pw_stream``-like ``Channel`` data exchange API.
77``Channel`` provides a standard I/O interface with these advanced features.
78Like ``pw_stream``, this API will be used anywhere data needs to be read and/or
79written.
80
81---------
82Use cases
83---------
84pw_rpc
85======
86pw_rpc is a communications protocol that enables calling procedures on different
87nodes (i.e. RPCs), and sharing data between them. RPCs can be sent using
88pw_stream APIs, which are blocking.
89
90Sockets
91=======
92Sockets are a communications channel between two endpoints in a network.
93Sockets support exchanging data:
94
95- as datagrams or a stream or bytes, and
96- reliably or unreliably.
97
98pw_stream
99=========
100``Channel`` should support all use cases addressed by ``pw_stream``. These
101include:
102
103- :cpp:class:`pw::stream::NullStream` -- ``NullStream`` ignores all bytes
104  written to it and produces no bytes when read. This is used when no input or
105  output is needed.
106- :cpp:class:`pw::stream::CountingNullStream` -- Counts bytes written to it.
107  Used to to determine the size of an encoded object before it is encoded to its
108  final destination.
109- :cpp:class:`pw::stream::MemoryReader` / :cpp:class:`pw::stream::MemoryWriter`
110  -- Writes data to or reads data from a fixed, contiguous memory buffer.
111  Example uses include encoding a protobuf for transport.
112- :cpp:class:`pw::stream::SocketStream` -- Supports reading from and writing to
113  a TCP socket.
114- :cpp:class:`pw::blob_store::BlobStore::Reader` /
115  :cpp:class:`pw::blob_store::BlobStore::Writer` -- ``pw_blob_store`` uses a
116  stream interface for reading and writing. This is similar to a file object.
117
118Hardware interfaces
119===================
120It is often necessary to exchange data with hardware I/O blocks.
121The ``Channel`` API could be used to abstract communications with I/O
122interfaces.
123
124------------------
125Existing solutions
126------------------
127
128pw_stream
129=========
130pw_stream provides for a synchronous, reliable byte-oriented stream.
131
132See :ref:`module-pw_stream`.
133
134C++
135===
136C++ provides an I/O stream family of classes.
137
138Java
139====
140Java provides a hierarchy of channel classes with a variety of flavors. The
141`Channel interface
142<https://docs.oracle.com/javase/8/docs/api/java/nio/channels/Channel.html>`_
143provides just two methods: ``isOpen()`` and ``close()``. Various I/O operations
144are mixed in through different interfaces. ``Channel`` supports `byte stream
145<https://docs.oracle.com/javase/8/docs/api/java/nio/channels/ByteChannel.html>`_,
146`datagram
147<https://docs.oracle.com/javase/8/docs/api/java/nio/channels/DatagramChannel.html>`_,
148`asynchronous <https://docs.oracle.com/javase/8/docs/api/java/nio/channels/AsynchronousChannel.html>`_,
149and `scatter <https://docs.oracle.com/javase/8/docs/api/java/nio/channels/ScatteringByteChannel.html>`_/
150`gather <https://docs.oracle.com/javase/8/docs/api/java/nio/channels/GatheringByteChannel.html>`_ IO.
151
152C#
153==
154The C# programming language offers a stream class similar to pw_stream and the
155proposed pw_channel module. It supports synchronous and asynchronous operations
156on a stream of bytes.
157https://learn.microsoft.com/en-us/dotnet/api/system.io.stream?view=net-7.0
158
159C#’s Channel API has a different intent than pw_channel. Its purpose is to
160synchronize objects between endpoints, and is somewhat different from what is
161proposed here.
162https://learn.microsoft.com/en-us/dotnet/api/system.threading.channels?view=net-7.0
163
164------------
165Requirements
166------------
167* Support data transmission for the upcoming sockets API (:ref:`seed-0107`):
168
169  - reliable byte stream (``SOCK_STREAM``)
170  - unreliable datagram (``SOCK_DGRAM``)
171  - reliable datagram (``SOCK_SEQPACKET``)
172
173* Asynchronous operations.
174* Efficient, minimally copying buffer with ``MultiBuf`` (:ref:`seed-0109`).
175
176------
177Design
178------
179Conceptually, a channel is a sequence of bytes or datagrams exchanged between
180two endpoints. An endpoint can be anything that produces or consumes data, such
181as an in-memory data structure, a file in a filesystem, a hardware peripheral,
182or a network socket. Both endpoints may be ``Channel`` implementations, or the
183``Channel`` may simply forward to something that provides compatible semantics,
184e.g. a memory buffer or OS socket.
185
186In Unix, "everything is a file". File descriptors provide a common I/O interface
187used for everything from files to pipes to sockets to hardware devices. Channels
188fill a similar role as POSIX file descriptors.
189
190Channel semantics
191=================
192pw_channel will provide the data exchange API for Pigweed’s upcoming network
193sockets. To this end, ``Channel`` supports the following socket semantics:
194
195- reliable byte stream (``SOCK_STREAM``)
196- unreliable datagram (``SOCK_DGRAM``)
197- reliable datagram (``SOCK_SEQPACKET``)
198
199Reliability and data type (stream versus datagram) are essential aspects of
200channel semantics. These properties affect how code that uses the APIs is
201written. A channel with different semantics cannot be swapped for another
202without updating the assumptions in the surrounding code.
203
204Data type: datagrams & byte streams
205-----------------------------------
206Fundamentally, a channel involves sending data from one endpoint to another.
207The endpoints might both be ``Channel`` instances (e.g. two sockets). Or, one
208endpoint could be a ``Channel`` while the other is an in-memory data structure,
209file in a file system, or hardware peripheral.
210
211The data type dictates the basic unit of data transmission. Datagram channels
212send and receive datagrams: "self-contained, independent entit[ies] of data"
213(`RFC 1594 <https://www.rfc-editor.org/rfc/rfc1594.txt>`_). Datagrams contain a
214payload of zero or more bytes. pw_channel does not define a maximum payload size
215for datagrams.
216
217Byte stream channels send and receive an arbitrary sequence of bytes.
218Zero-length byte stream writes are no-ops and may not result in any bytes being
219transmitted.
220
221In terms of the channels API, ``Read``, ``Write``, and ``Seek`` functions have
222different meanings for byte and and datagram channels. For byte stream channels,
223these functions work with an arbitrary number of bytes. For datagram channels,
224``Read``, ``Write``, and ``Seek`` are in terms of datagrams.
225
226Reliable channels
227-----------------
228Reliable channels guarantee that their data is received in order and without
229loss. The API user does not have to do anything to ensure this. After a write is
230accepted, the user will never have to retry it. Reads always provide data in
231order without loss. The channel implementation is responsible for this.
232
233For some channels, reliability is trivial; for others it requires significant
234work:
235
236- A memory channel that writes to a buffer is trivially reliable.
237- A socket communicating across a network will require a complex protocol such
238  as TCP to guarantee that the data is delivered.
239
240Initially, only reliable byte-oriented channels will be supported. Unreliable
241byte streams are not commonly supported, and would be difficult to apply in many
242use cases. There are circumstances where unreliable byte streams do makes sense,
243such as reading time-sensitive sensor data, where the consumer only wants the
244very latest data regardless of drops. Unreliable byte streams may be added in
245the future.
246
247Data loss
248^^^^^^^^^
249Data is never silently lost in a reliable channel. Unrecoverable data loss
250always results in the eventual closure of the channel, since a fundamental
251invariant of the channel cannot be maintained.
252
253A few examples:
254
255- A write to a TCP channel fails because of a transient hardware issue. The
256  channel and underlying TCP connection are closed.
257- A TCP channel times out on a retry. The channel and underlying TCP connection
258  are closed.
259- A write to a channel that fills a ring buffer is requested. A ``MultiBuf`` for
260  the write is not provided immediately because the ring buffer is full. The
261  channel stays open, but the write is delayed until the ring buffer has
262  sufficient space.
263
264Reliability & connections
265^^^^^^^^^^^^^^^^^^^^^^^^^
266Reliable channels operate as if they have a connection, even if the underlying
267implementation does not establish a connection. This specifically means that:
268
269- It is assumed that the peer endpoint will receive data for which the write
270  call succeeded.
271- If data is lost, the error will be reported in some form and the channel will
272  be closed.
273
274For example, a TCP socket channel would maintain an explicit connection, while a
275ring buffer channel would not.
276
277Unreliable channels
278-------------------
279Unreliable datagram channels make no guarantees about whether datagrams are
280delivered and in what order they arrive. Users are responsible for tracking
281drops and ordering if required.
282
283Unreliable channels should report read and write failures whenever possible,
284but an ``OK`` write does not indicate that the data is received by the other
285endpoint.
286
287Flow control, backpressure, and ``ConservativeLimit``
288=====================================================
289A channel may provide backpressure through its async write API. The
290``PollWritable`` method should be used to ensure that the channel is ready
291to receive calls to ``Write``. Additionally, the ``MultiBufAllocator`` may wait
292to provide a ``MultiBuf`` for writing until memory becomes available.
293
294pw_stream offered a notion of flow control through the
295:cpp:func:`pw::stream::Stream::ConservativeWriteLimit` function. Code using a
296stream could check the write limit prior to writing data to determine if the
297stream is ready to receive more. This function will not be provided in
298``pw_channel``.
299
300Openness / closedness
301=====================
302pw_channel will have an explicit open/closed concept that ``pw_stream`` lacks.
303Reads and writes may succeed when the channel is open. Reads and writes never
304succeed when the channel is closed.
305
306The channel API supports closing a channel, but does not support opening a
307channel. Channels are opened by interacting with a concrete class.
308
309Reliable channels are closed if unrecoverable data loss occurs. Unreliable
310channels may be closed when reads or writes are known to fail (e.g. a
311cable was unplugged), but this is not required.
312
313Synchronous APIs
314================
315The ``pw_channel`` class may provide synchronous versions of its functions,
316implementated in terms of the asynchronous API. These will poll the asynchronous
317API until it completes, blocking on a binary semaphore or similar primitive if
318supported. This will leverage a ``pw_async`` helper for this purpose.
319
320Channel Class Capabilities
321==========================
322``Channel`` s may offer any of five capabilities:
323
324.. list-table::
325   :header-rows: 1
326
327   * - Capability
328     - Description
329   * - ``kReliable``
330     - Data is guaranteed to arrive in order, without loss.
331   * - ``kSeekable``
332     - The read/write position may be changed via the ``Seek`` method.
333   * - ``kDatagram``
334     - Data is guaranteed to be received in whole packets matching the size and
335       contents of a single ``Write`` call.
336   * - ``kReadable``
337     - Supports reading data.
338   * - ``kWritable``
339     - Supports writing data
340
341These capabilities are expressed as generic arguments to the ``Channel`` class,
342e.g. ``Channel<kReadable | kReliable>`` for a ``Channel`` that is readable and
343reliable. Aliases are provided for common combinations, such as ``ByteStream``
344for a reliable non-seekable non-datagram stream of bytes (such as a TCP stream).
345Certain nonsensical combinations, such as a channel that is ``kSeekable`` but
346not ``kReadable`` or ``kWritable`` are disallowed via ``static_assert``.
347
348Conversion
349----------
350Channels may be freely converted to channels with fewer capabilities, e.g.
351``Channel<kReadable | kWritable>`` may be used as a ``Channel<kReadable>``.
352This allows Channels with compatible semantics to be substituted for one another
353safely.
354
355Shared Base Class for Minimal Code Size
356---------------------------------------
357``Channel`` also inherits from an ``AnyChannel`` base class which provides the
358underlying ``virtual`` interface.  Sharing a single base class avoids multiple
359inheritance, minimizing vtable overhead.
360
361Prototype Demonstrating Channel Capabilities
362--------------------------------------------
363A prototype demonstrating this interface can be seen `here
364<https://godbolt.org/z/3c4M3Y17r>`_.
365
366API sketch
367==========
368An outline of the ``AnyChannel`` base class follows. ``AnyChannel`` will rarely
369be used directly, since it makes no guarantees about any channel capabilities or
370the data type. The function signatures and comments apply to all derived classes,
371however.
372
373.. code-block:: cpp
374
375   namespace pw::channel {
376
377   /// A generic data channel that may support reading or writing bytes.
378   ///
379   /// Note that this channel should be used from only one ``pw::async::Task``
380   /// at a time, as the ``Poll`` methods are only required to remember the
381   /// latest ``pw::async::Context`` that was provided.
382   class AnyChannel {
383    public:
384     // Properties
385     [[nodiscard]] bool reliable() const;
386     [[nodiscard]] DataType data_type() const;
387     [[nodiscard]] bool readable() const;
388     [[nodiscard]] bool writable() const;
389     [[nodiscard]] Seekability seekable() const;
390
391     [[nodiscard]] bool is_open() const;
392
393     // Write API
394
395     // Checks whether a writeable channel is *currently* writeable.
396     //
397     // This should be called before attempting to ``Write``, and may be called
398     // before allocating a write buffer if trying to reduce memory pressure.
399     //
400     // If ``Ready`` is returned, a *single* caller may proceed to ``Write``.
401     //
402     // If ``Pending`` is returned, ``cx`` will be awoken when the channel
403     // becomes writeable again.
404     //
405     // Note: this method will always return ``Ready`` for non-writeable
406     // channels.
407     MaybeReady<> PollWritable(pw::async::Context& cx);
408
409     // Gives access to an allocator for write buffers. The MultiBufAllocator
410     // provides an asynchronous API for obtaining a buffer.
411     //
412     // This allocator must *only* be used to allocate the next argument to
413     // ``Write``. The allocator must be used at most once per call to
414     // ``Write``, and the returned ``MultiBuf`` must not be combined with
415     // any other ``MultiBuf`` s or ``Chunk`` s.
416     //
417     // Write allocation attempts will always return ``std::nullopt`` for
418     // channels that do not support writing.
419     MultiBufAllocator& GetWriteAllocator();
420
421     // Writes using a previously allocated MultiBuf. Returns a token that
422     // refers to this write. These tokens are monotonically increasing, and
423     // FlushPoll() returns the value of the latest token it has flushed.
424     //
425     // The ``MultiBuf`` argument to ``Write`` may consist of either:
426     //   (1) A single ``MultiBuf`` allocated by ``GetWriteAllocator()``
427     //       that has not been combined with any other ``MultiBuf`` s
428     //       or ``Chunk``s OR
429     //   (2) A ``MultiBuf`` containing any combination of buffers from sources
430     //       other than ``GetWriteAllocator``.
431     //
432     // This requirement allows for more efficient use of memory in case (1).
433     // For example, a ring-buffer implementation of a ``Channel`` may
434     // specialize ``GetWriteAllocator`` to return the next section of the
435     // buffer available for writing.
436     //
437     // May fail with the following error codes:
438     //
439     // * OK - Data was accepted by the channel
440     // * UNIMPLEMENTED - The channel does not support writing.
441     // * UNAVAILABLE - The write failed due to a transient error (only applies
442     //   to unreliable channels).
443     // * FAILED_PRECONDITION - The channel is closed.
444     Result<WriteToken> Write(MultiBuf&&);
445
446     // Flushes pending writes.
447     //
448     // Returns a ``MaybeReady`` indicating whether or not flushing has
449     // completed.
450     //
451     // After this call, ``LastFlushed`` may be used to discover which
452     // ``Write`` calls have successfully finished flushing.
453     //
454     // * Ready(OK) - All data has been successfully flushed.
455     // * Ready(UNIMPLEMENTED) - The channel does not support writing.
456     // * Ready(FAILED_PRECONDITION) - The channel is closed.
457     // * Pending - Data remains to be flushed.
458     [[nodiscard]] MaybeReady<pw::Status> PollFlush(async::Context& cx);
459
460     // Returns the latest ```WriteToken``` that was successfully flushed.
461     //
462     // Note that a ``Write`` being flushed does not necessarily mean that the
463     // data was received by the remote. For unreliable channels, flushing may
464     // simply mean that data was written out, not that it was received.
465     [[nodiscard]] WriteToken LastFlushed() const;
466
467     // Read API
468
469     // Returns a MultiBuf read data, if available. If data is not available,
470     // invokes cx.waker() when it becomes available.
471     //
472     // For datagram channels, each successful read yields one complete
473     // datagram. For byte stream channels, each successful read yields some
474     // number of bytes.
475     //
476     // Channels only support one read operation / waker at a time.
477     //
478     // * OK - Data was read into a MultiBuf.
479     // * UNIMPLEMENTED - The channel does not support reading.
480     // * FAILED_PRECONDITION - The channel is closed.
481     // * OUT_OF_RANGE - The end of the stream was reached. This may be though
482     //   of as reaching the end of a file. Future reads may succeed after
483     //   ``Seek`` ing backwards, but no more new data will be produced. The
484     //   channel is still open; writes and seeks may succeed.
485     MaybeReady<Result<MultiBuf>> PollRead(async::Context& cx);
486
487     // On byte stream channels, reads up to max_bytes from the channel.
488     // This function is hidden on datagram-oriented channels.
489     MaybeReady<Result<MultiBuf>> PollRead(async::Context& cx, size_t max_bytes);
490
491     // Changes the position in the stream.
492     //
493     // Any ``PollRead`` or ``Write`` calls following a call to ``Seek`` will be
494     // relative to the new position. Already-written data still being flushed
495     // will be output relative to the old position.
496     //
497     // * OK - The current position was successfully changed.
498     // * UNIMPLEMENTED - The channel does not support seeking.
499     // * FAILED_PRECONDITION - The channel is closed.
500     // * NOT_FOUND - The seek was to a valid position, but the channel is no
501     //   longer capable of seeking to this position (partially seekable
502     //   channels only).
503     // * OUT_OF_RANGE - The seek went beyond the end of the stream.
504     Status Seek(ptrdiff_t position, Whence whence);
505
506     // Returns the current position in the stream, or kUnknownPosition if
507     // unsupported.
508     size_t Position() const;
509
510     // Closes the channel, flushing any data.
511     //
512     // * OK - The channel was closed and all data was sent successfully.
513     // * DATA_LOSS - The channel was closed, but not all previously written
514     //   data was delivered.
515     // * FAILED_PRECONDITION - Channel was already closed, which can happen
516     //   out-of-band due to errors.
517     MaybeReady<pw::Status> PollClose(async::Context& cx);
518
519    private:
520     virtual bool do_reliable() const;
521     virtual DataType do_data_type() const;
522     virtual bool do_readable() const;
523     virtual bool do_writable() const;
524     virtual Seekability do_seekable() const;
525     virtual bool do_is_open() const;
526
527     // Virtual interface.
528     virtual MultiBufAllocator& DoGetWriteBufferAllocator() = 0;
529
530     virtual MaybeReady<> PollWritable(async::Context& cx) = 0;
531
532     virtual Result<WriteToken> DoWrite(MultiBuf&& buffer) = 0;
533
534     virtual WriteToken DoPollFlush(async::Context& cx) = 0;
535
536     [[nodiscard]] WriteToken LastFlushed() const = 0;
537
538     // The max_bytes argument is ignored for datagram-oriented channels.
539     virtual MaybeReady<Result<MultiBuf>> DoReadPoll(
540         async::Context& cx, size_t max_bytes) = 0;
541
542     virtual DoSeek(ptrdiff_t position, Whence whence) = 0;
543
544     virtual size_t DoPosition() const { return kUnknownPosition; }
545
546     virtual async::MaybeReady<Status> DoClosePoll(async::Context& cx);
547   };
548   }  // namespace pw::channel
549
550pw_channel and pw_stream
551========================
552As described, ``pw_channel`` is closely based on ``pw_stream``. It adds async,
553``MultiBuf``, and new socket-inspired semantics.
554
555``pw_channel`` is intended to supersede ``pw_stream``. There are a few options
556for how to reconcile the two modules. From most to least ideal, these are:
557
558- Fully replace ``pw_stream`` with ``pw_channel`` and remove the ``pw_stream``
559  module.
560- Rework ``pw_stream`` so it inherits from ``pw::channel::Channel``.
561- Keep ``pw_stream``, but provide adapters to convert between ``pw_stream`` and
562  ``pw_channel``.
563
564Fully replacing ``pw_stream`` with ``pw_channel`` could be complicated due to:
565
566- Potential code size increase because of ``MultiBuf`` and the async poll model.
567- The scale of migrating the all Pigweed users off of ``pw_stream``.
568- Increased API complexity imposing a burden on Pigweed users.
569