blob: d44d376a6ea7fefc45f43fb3d73f8a5f4a4a8c38 [file] [log] [blame] [view]
# Message pipes
Message pipes are the primary communication mechanism between Mojo programs. A
*message pipe* is a point-to-point (with each side being called a message pipe
*endpoint*), bidirectional message passing mechanism, where messages may contain
both data and Mojo handles.
It's important to understand that a message pipe is a "transport": it provides a
way for data and handles to be sent between Mojo programs. The system is unaware
of the meaning of the data or of the handles (other than their intrinsic
properties).
That said, Mojo provides a *standard* way of communicating over message pipes,
namely via a standardized protocol together with [Mojom IDL](mojom_idl.md)
files.
## Messages
A *message* consists of two things:
* a finite sequence of bytes, and
* a finite sequence of [Mojo handles](handles.md).
Both of these are determined when the message is sent (or *written*). Messages
are *framed* in the sense that they are "atomic" units: they are sent and
received (or *read*) as entire units, not just by Mojo programs themselves but
by the system, which is aware of and enforces the message boundaries.
(Note on terminology: We'll use "send" and "write" interchangeably, and
similarly for "receive" and "read". "Write" and "read" correspond more closely
to the names usually given to the basic Mojo operations, e.g.,
`MojoWriteMessage()` and `MojoReadMessage()`.)
## Asynchronous operation and queueing
Message pipes are *asynchronous* in the sense that sent messages do not have
intrinsic response messages mediated/enforced by the system. (This is different
from saying that message write/read are asynchronous operations: these
operations are actually synchronous and complete "immediately". However, note
that reading a message is "nonblocking" in the sense that it will fail if a
message is not available to be read. Thus a message must be waited for, and the
combined wait-then-read may form an asynchronous pattern.)
To allow message writes to complete immediately, messages are queued. Indeed,
one can understand a message pipe as a pair of queues, one in each direction.
Each endpoint has opposite notions of incoming and outgoing queues (recall that
message pipes have a pair of endpoints).
Writing a message to an endpoint then simply entails enqueueing that message on
that endpoint's outgoing queue (which is the *peer* endpoint's incoming queue).
Reading a message from an endpoint is just dequeueing a message from that
endpoint's incoming queue (which is the peer endpoint's outgoing queue).
Queueing is unlimited. Why? The problem is that limiting queueing exposes Mojo
programs to complex deadlock problems:
* One way of limiting queue sizes is to block the sender if the queue is "full".
However, the receiver may not be able or willing to consume messages until the
sender does something else (and this is often the case in asynchronous
programming). For example, perhaps the putative "receiver" does not yet even
have a handle to the endpoint yet, and that handle is sent in a message (over
some other message pipe).
* Another way would be to have the write fail if the queue is full. Then the
sender would want to additionally queue on its side. The thread would continue
running and, e.g., run its message loop. However, sender-side queueing
basically makes it impossible for the sender to transfer that endpoint
(handle), at least until the sender-side queue is cleared. However, the
receiver may not be able/willing to proceed until the sender has transferred
the aforementioned endpoint.
Thus we allow unlimited queueing. (**TODO(vtl)**: Probably we'll eventually
allow "hard" queue limits to be set for the purposes of preventing
denial-of-service. However, the response to overruns will be hard failures, in
the sense that the message pipe may be destroyed, rather than soft,
"recoverable" failures -- since those expose deadlock issues.) It is then up to
Mojo programs to implement flow control in some way. (**TODO(vtl)**: Write more
about this in the context of Mojom.)
## "Synchronous" operation
Though message pipes are asynchronous as discussed above, they may be used in a
synchronous fashion: immediately after sending a *request* message, one can then
just block and wait for the *response* message (and then read it and process
it). Of course, this requires that the protocol support this:
* Message pipes must be used in a "directional" way: there must be fixed request
and response directions or, equivalently, one endpoint belongs to the *client*
and the other to the *server* (or *impl*). (Historical note: This is the case
for the current [Mojom protocol](mojom_protocol.md), but not in previous
versions.) The issue here is that without this, the sender of the request
messages may have to process incoming request messages from its peer.
* Request messages must have unique response messages. (In the Mojom protocol,
request messages have optional unique responses. For messages without
responses, one can just proceed immediately without waiting. However, without
response messages there may be flow control issues; again, see above.) The
important point is that for each request message, there is a well-defined
number of response messages for each request and not arbitrary "callback"
messages.
* The sending of a response message must not depend on a future action of the
client. (This is a higher-level semantic that is not enforced by the Mojom
protocol. E.g., one may define a Mojom interface in which the response to a
message *Foo* isn't sent until the client sends a request *Bar*.)
That said, whether one wants to, or even can, use this synchronous mode of
operation may depend on a number of things:
* For message-loop-centric programming languages, this mode is at best
undesirable (and possibly infeasible, depending on what facilities are exposed
to user code). (E.g., this is the case for JavaScript and Dart.)
* Similarly, on a message-loop programming model (in which threads -- if there
are more than one -- are mostly coordinated by "message passing"), it is
typically undesirable to block any thread (with a message loop). Indeed, if
other message pipes are serviced by a message loop, blocking the thread may
result in deadlock. (E.g., this is the usual programming model for the
standard Mojom C++ bindings.)
* Even when blocking is permissible, it may not be desirable to do so:
advancement of the program then relies on trusting the server to be
responsive and send responses in a timely fashion.
* Mixing asynchronous and synchronous operation is problematic: one cannot send
a request and synchronously wait for a response while responses to other
messages are still pending. (Theoretically, one could buffer such other
responses until the response to particular request is received, and process
those other responses later, but this would be dubious at best.)