Mojo interfaces are excessively complicated by default

118 views
Skip to first unread message

James Robinson

unread,
Dec 16, 2014, 4:49:18 PM12/16/14
to mojo...@chromium.org
In explaining mojo interfaces to folks and trying to figure out new language bindings I keep stumbling up against the same set of hurdles.  Mojo interfaces tend to get really complicated quickly and I think the set of defaults we provide in the language encourage people to create excess complexity.  The complexity manifests in two closely related ways:

*) Given a typical mojom interface definition it is in general impossible to know what the next incoming message on a particular end of a MessagePipe will be
*) The implementation of an endpoint of a MessagePipe has to handle existing in more states than are really interesting

Both of this derive from the fact that we define the set of messages that we expect to be sent in a direction on a MessagePipe in two ways.  Consider:

[Client=FooClient]
interface Foo {
  MakeACall() => (string reply);
};

interface FooClient {
  UnrelatedCall();
};

With this definition a MessagePipe will receive calls made on interface Foo on one endpoint and receive the combination of messages defined on FooClient and replies to messages defined on Foo on the other endpoint.  The combination of the two makes the implementation of the endpoint bound to FooClient complicated.  Consider the following C++ code that tries to make a synchronous call:

  void DoSomething(Foo* foo_proxy) {
    std::string reply;
    foo_proxy->MakeACall(CaptureString(&buf));  // CaptureString is a callback that copies the result into passed-in string
    foo_proxy->WaitForIncomingMethodCall();  // Intended to block until the reply is available.
    DoStuff(reply);
  }

This code has a subtle race condition.  If the next incoming message on the pipe is an invocation of UnrelatedCall() instead of the reply this code is expecting, reply will not be populated before the call to DoStuff().  It's possible to avoid this by providing additional constraints on when UnrelatedCall() can be invoked in proper operation of the interface but this has to be done via comments and careful coding and can't be expressed in our IDL language.  We do this in practice in several places by promising that a certain call will be invoked before anything else, for instance mojo.Application's Initialize() call, but this is tricky.  This sort of race is also hard to expose in testing since it will only happen when the endpoints bound to Foo and FooClient are running concurrently and both trying to do interesting things.

In the C++ bindings, we implicitly discourage writing blocking code like this by making it really hard to do so correctly.  We assume that threads are expensive and hard to use and so code will be written in a fully asynchronous way with a message loop dispatching all incoming messages.  In other language bindings threads of execution may be very cheap (a fresh Go routine costs 2KB, for instance) and easier to use, so these assumptions will not hold across all languages.  Developers using these languages may be used to writing straight-line blocking code when coding against other concurrent APIs.  Even when writing asynchronous code, the set of possible state transitions an object bound to a message pipe endpoint is a function of the possible order of incoming messages.  When the order of incoming messages is unpredictable the code has to add many difficult-to-test codepaths to handle different orders.

In practice in most of the interfaces I've written or read the client calls and replies are not all that closely correlated.  We promise a specific ordering of the two calls since they are made on the same message pipe but code rarely depends on handling the replies and client calls in a specific relative order.  It's more common for an interface and its client interface to provide sets of entry points for the two sides to talk to each other possibly concurrently about logically separate concerns.  However, since replies and client calls arrive on the same message pipe handle it's difficult to disentangle the two.  They have to be waited for as one handle and (in the C++ bindings) have to be handled on the same thread (or do something more complicated to preserve order in a hypothetical threadsafe binding in some language).

(aside: in Chrome IPC handlers, this complexity was unavoidable since a single IPC channel had to represent the full interface between two processes.  We defined filters to handle things on different threads but didn't really have a mechanism to define narrower sets of related messages.  We then had to add tags to messages to define handling in synchronous cases and impose special rules for reentrancy and blocking hierarchies for different message types.  In mojo, message pipes are cheap and provide a way to group related messages together in potentially independent FIFOs.  We just aren't taking full advantage of this flexibility).

Proposal: Given all this, I propose that we abolish the interface Client notion altogether and say that a single mojom interface declaration defines the full set of messages that can go over a single message pipe.  This means that all messages in one direction across a message pipe will be method calls on the interface and all messages the other direction will be replies to those messages.  Thus code that binds to a message pipe handle is doing one of two things: either implementing an interface or calling it..  Implementations of interfaces read incoming messages which will all be method calls and handle them, which may include sending replies on the message pipe.  Callers of interfaces format method calls as method and write them into the message pipe.  The only readable messages on the message pipe will be replies to methods the caller previously called.  The caller always knows exactly how many incoming messages are expected at any point in time and if it only makes calls one-at-a-time, or if the interface provides promises about the order of replies, it can even know exactly what order the incoming replies will arrive in.  This makes it easy to write a synchronous version of either the implementation or caller to an interface and makes the asynchronous versions much simpler since they will have a smaller set of state transitions to deal with.

If we do remove this capability then we need to make sure folks can still do everything they are currently doing or wish to do with the client notion.  When a client is used to expose a set of functionality that can be called at any point, I think the right thing to do is to provide a separate message pipe in the other direction that exposes that functionality.  Then the endpoint can decide to either bind the new message pipe to the same thread/object or not as appropriate.  For example, here's the current mojo.Shell/mojo.Application interface:

// application.mojom
module mojo;

// Applications vend Services through the ServiceProvider interface. Services
// implement Interfaces.
interface Application {
  // Initialize is guaranteed to be called before any AcceptConnection calls.
  Initialize(array<string>? args);

  AcceptConnection(string? requestor_url, ServiceProvider? provider);
};

// shell.mojom
module mojo;

// The Shell is the finder and launcher of Applications. An Application uses
// it's Shell interface to connect to other Applications.
[Client=Application]
interface Shell {
  // Loads url. mojo:{service} will result in the user of the value of the
  // --origin flag to the shell being used.
  ConnectToApplication(string? application_url, ServiceProvider&? provider);
};

(note that mojo.Application's Client is Shell, although we don't tell you that when reading only application.mojom)

I propose we change this to be:

module mojo;

interface Application {
  Initialize(Shell shell, array<string>? args);
 
  AcceptConnection(string? requestor_url, ServiceProvider? provider);
};

// The Shell is the finder and launcher of Applications. An Application uses
// it's Shell interface to connect to other Applications.
interface Shell {
  // Loads url. mojo:{service} will result in the user of the value of the
  // --origin flag to the shell being used.
  ConnectToApplication(string? application_url, ServiceProvider&? provider);
};

The implementation of Application can then pass its pointer to the Shell message pipe to whichever object needs to make fresh outgoing connections.

Another use case of clients is to provide a response to an incoming call in a way that is more complicated than can fit into a single reply.  For example a single incoming call may result in a stream of data.  For this we should (and I think in the most case do) allocate new message pipes for this.  Message pipes are cheap.

Another use case for clients is to succinctly describe the interface implemented by both sides of a particular message pipe when passing it around.  In mojom passing a parameter of type mojo.Application means passing a handle to a message pipe that implements mojo.Application on one end and mojo.Shell on the other end.  Whatever receives this parameter can both route incoming calls to mojo.Application and send outgoing calls to mojo.Shell.  The alternative is to pass the handle to the other interface around explicitly.  This means more parameters in some situations but is more explicit and IMHO clearer.  With the client mechanism there is no way to pass a handle to an interface that does *not* imply a client on the other end, even though sometimes the client doesn't make a lot of sense in some cases.

This comes up in particular with interface requests - the SomeInterface& type in mojom.  A parameter of type SomeInterface in mojom means a handle to a message pipe the other end of which implements SomeInterface, which implicitly means that the other handle on the message pipe implements SomeInterface's (possibly empty) Client.  A parameter of type SomeInterface& means the *other* handle to the message pipe AKA the client interface.  In other words, as far as the mojom type system is concerned a parameter of type mojo.Shell and a parameter of type mojo.Application& are exactly the same thing.  They both refer to the handle to a message pipe between a mojo.Shell implementation and a mojo.Application implementation that can write methods of type mojo.Shell and thus read messages of type mojo.Application.  We (ab)use this notion in the second parameter to mojo.Shell.ConnectToApplication which is of type ServiceProvider&.  ServiceProvider's client is of type ServiceProvider, so a ServiceProvider& and a ServiceProvider have exactly the same type in the mojom type system.  The intended semantics are arguably slightly different but not really.  A particular application may want to request interfaces from an application but not provide any to it or vice versa, but it's forced to do both because the type is symmetric.  I think having separate handles for the two directions will make the common case of code that only wants to do one or the other much simpler.

The last use of clients that is truly difficult to handle is an interface that wants to define a total ordering of client calls and replies to incoming methods.  Mojo defines that all messages send on a message pipe preserve FIFO order and so calls on the FooClient example from earlier and replies to incoming Foo calls must be received in the order that they are sent from the implementation of Foo.  If we split the interface up so that Foo method calls and their replies are on one message pipe and FooClient calls are on another, mojo will not promise any specific ordering of these messages.  In practice I'm not aware of any interface where this matters but if it does there are a few ways we could address it.  If we implement associated message pipes then we can just arrange to associate the Foo and FooClient message pipes and the problem will be solved.  The implementations of Foo and FooClient could also collaborate with each other to preserve order, probably by making some calls synchronous.  Lastly the interface could be redesigned so that replies to calls made on Foo are made through method calls on FooClient rather than the built-in reply mechanism, preserving order at the cost of requiring the implementations to associate the response with the request by hand.  I'm not aware of any cases where this is a problem in practice so it's hard to evaluate what needs doing here, if anything.

We could also leave the client annotation around but redefine it to refer to a different MessagePipe and have the bindings bind you to both handles by default but provide a way to extract one handle or the other if you want to deal with the two separately.  This would make the generated code a bit more complicated, at least for the existing bindings.  I'm not sure how workable this is.  Independently of this, we can decide to stop using client interfaces so much in our own code to simplify interfaces.

I believe this was discussed at least a bit back when replies were added to mojom and we decided that client interfaces handle the use case where the two sides need to spontaneously message each other.  That use case is valid, but I think it's much better to handle that with two separate message pipes.  The marginal cost of a message pipe is really low and the benefit of separating the two directions is larger than it first appears.

- James

Darin Fisher

unread,
Dec 16, 2014, 5:16:28 PM12/16/14
to James Robinson, mojo...@chromium.org
It might be nice to put this in a document linked to from the mojo docs site (go/mojo-docs). That way we can comment on it directly.

As you note, we talked a lot about making this change back when we introduced replies, and I even mostly hacked up a CL to implement it. It started to feel heavy weight, and yeah, the FIFO issue was an impediment. Maybe my approach felt heavyweight because I forced people to add their own setClient methods to their mojom interfaces. Maybe if we automated that part of it it wouldn't feel so bad.

I think the ordering problem you describe also exists to an extent in a world with only replies. Imagine if you call two methods sequentially and each generates a reply. If you want to wait synchronously just for the second reply, you cannot do so without first handling the reply to the first method. This means WaitForIncomingMethodCall is still potentially confusing in what side effect it produces.

Another thing: The model of Foo and FooClient is really meant to mimic what people do in idiomatic C++. They often have a class Foo with a client interface that they set on Foo to receive asynchronous notifications. Foo methods can also take base::Callback<>, etc. When you just think about straight C++ usage like this in Chromium, you'll see that we don't struggle with the ordering problems that much, and so maybe the struggles we experience with Mojom are not as bad as they may sometimes seem?

-Darin

James Robinson

unread,
Dec 16, 2014, 5:52:11 PM12/16/14
to Darin Fisher, mojo...@chromium.org
On Tue, Dec 16, 2014 at 2:16 PM, Darin Fisher <da...@chromium.org> wrote:
It might be nice to put this in a document linked to from the mojo docs site (go/mojo-docs). That way we can comment on it directly.

That's a really good idea!  https://groups.google.com/a/chromium.org/d/msg/mojo-dev/eqsG0aeALLo/Klovi1HJAtsJ is world-readable and mojo...@chromium.org has comment access.

As you note, we talked a lot about making this change back when we introduced replies, and I even mostly hacked up a CL to implement it. It started to feel heavy weight, and yeah, the FIFO issue was an impediment. Maybe my approach felt heavyweight because I forced people to add their own setClient methods to their mojom interfaces. Maybe if we automated that part of it it wouldn't feel so bad.

I think the ordering problem you describe also exists to an extent in a world with only replies. Imagine if you call two methods sequentially and each generates a reply. If you want to wait synchronously just for the second reply, you cannot do so without first handling the reply to the first method. This means WaitForIncomingMethodCall is still potentially confusing in what side effect it produces.

Actually we don't define the order in which replies need to be invoked at the mojom level.  I.e. if I have

interface Foo {
  One() => (int reply);
  Two() => (int reply);
};

and I invoke One() then Two(), there's no requirement at the mojom level that the implementation of Foo responds in that order.  It may hold on to the callback from method One() until it receives Two() or later.  However, if I make those calls then I do know that the incoming methods are the replies to those two methods.  I can then either make the calls one at a time to enforce fully serialized order or rely on higher-level promises in the contract with Foo to know a method order.  The thing that my proposal will ensure is that the replies will be the only thing that the class dealing with the endpoint has to deal with.  That's a much simpler problem.
 

Another thing: The model of Foo and FooClient is really meant to mimic what people do in idiomatic C++. They often have a class Foo with a client interface that they set on Foo to receive asynchronous notifications. Foo methods can also take base::Callback<>, etc. When you just think about straight C++ usage like this in Chromium, you'll see that we don't struggle with the ordering problems that much, and so maybe the struggles we experience with Mojom are not as bad as they may sometimes seem?

I don't agree that this pattern is really idiomatic C++, or that having explicit state machines in Chromium is easy to deal with.  On the first point, in C++ there's a growing trend to use fibers / {cooperative,lightweight,green} threads to deal with concurrent programming since the model is much simpler than fully asynchronous state machine advancement.  We write code in the complicated way in Chromium because the IPC system doesn't really give us much choice - we are going to get a large set of incoming messages and need to do something with them.  I'd also argue that we have piles and piles of complexity and bugs related to message ordering especially whenever anything synchronous is added to the mix.  See any of the NPAPI deadlocks or difficult pepper ordering issues we've had or the continuing struggles to rationally schedule different types of messages.

Mojom gives us an ability that chrome IPC does not have to separate concerns into separate interfaces, which sidesteps a lot of these problems.  Thus idiomatic code in mojo will look different from idiomatic Chromium code as it is operating under a different set of constraints.  Idiomatic C++ code using a coroutine library (or Go code or C# code with async+await or whatever) looks very different from Chromium IPC handling logic.  I'd argue it's much simpler to write and reason about as well.

- James

James Robinson

unread,
Dec 16, 2014, 6:28:34 PM12/16/14
to Darin Fisher, mojo...@chromium.org
On Tue, Dec 16, 2014 at 2:52 PM, James Robinson <jam...@chromium.org> wrote:
On Tue, Dec 16, 2014 at 2:16 PM, Darin Fisher <da...@chromium.org> wrote:
It might be nice to put this in a document linked to from the mojo docs site (go/mojo-docs). That way we can comment on it directly.

That's a really good idea!  https://groups.google.com/a/chromium.org/d/msg/mojo-dev/eqsG0aeALLo/Klovi1HJAtsJ is world-readable and mojo...@chromium.org has comment access.

Elliott Sprehn

unread,
Dec 16, 2014, 7:22:12 PM12/16/14
to James Robinson, mojo...@chromium.org

On Tue, Dec 16, 2014 at 1:49 PM, James Robinson <jam...@chromium.org> wrote:
[...]

Proposal: Given all this, I propose that we abolish the interface Client notion altogether and say that a single mojom interface declaration defines the full set of messages that can go over a single message pipe. [...]


+1, this seems like a good change.

- E 

Alex Nicolaou

unread,
Dec 16, 2014, 11:55:39 PM12/16/14
to Elliott Sprehn, James Robinson, mojo...@chromium.org
Why not go the next step and make the calls synchronous? If asynchrony is needed, the client can spawn multiple threads and have each one open a new message pipe to the server. This dramatically simplifies the programming model for the common case, and is only inefficient if you imagine a world where hundreds of threads in the caller want to call the same service.

alex

To unsubscribe from this group and stop receiving emails from it, send an email to mojo-dev+u...@chromium.org.

James Robinson

unread,
Dec 17, 2014, 12:36:03 AM12/17/14
to Alex Nicolaou, Elliott Sprehn, mojo...@chromium.org
On Tue, Dec 16, 2014 at 8:55 PM, Alex Nicolaou <anic...@google.com> wrote:
Why not go the next step and make the calls synchronous? If asynchrony is needed, the client can spawn multiple threads and have each one open a new message pipe to the server. This dramatically simplifies the programming model for the common case, and is only inefficient if you imagine a world where hundreds of threads in the caller want to call the same service.

That's not possible in many of the languages we support mojo bindings for because threading is either difficult or impossible.  Even in languages with strong enough threading support to handle this model, there are environments and situations where in order to interoperate with other libraries or meet specific performance or resource use criteria a developer might need to handle responses asynchronously.  Or it might just make more sense to handle the interface in that way.  My goal is not to impose a synchronous model but to simplify the handling of incoming messages.  This allows handling calls in a blocking way and simplifying the implementation of the receiver by reducing the the number of states it has to deal with.

I can definitely imaging having a pair of mojo programs written in different languages communicating with each other one handling all calls asynchronously on one thread and the other using a large number of coroutines to perform blocking calls.  The authors of each program might think the author of the other is crazy, but they should be able to communicate just fine :)

- James

Darin Fisher

unread,
Dec 17, 2014, 12:41:34 AM12/17/14
to James Robinson, mojo...@chromium.org
On Tue, Dec 16, 2014 at 2:52 PM, James Robinson <jam...@chromium.org> wrote:
On Tue, Dec 16, 2014 at 2:16 PM, Darin Fisher <da...@chromium.org> wrote:
It might be nice to put this in a document linked to from the mojo docs site (go/mojo-docs). That way we can comment on it directly.

That's a really good idea!  https://groups.google.com/a/chromium.org/d/msg/mojo-dev/eqsG0aeALLo/Klovi1HJAtsJ is world-readable and mojo...@chromium.org has comment access.

As you note, we talked a lot about making this change back when we introduced replies, and I even mostly hacked up a CL to implement it. It started to feel heavy weight, and yeah, the FIFO issue was an impediment. Maybe my approach felt heavyweight because I forced people to add their own setClient methods to their mojom interfaces. Maybe if we automated that part of it it wouldn't feel so bad.

I think the ordering problem you describe also exists to an extent in a world with only replies. Imagine if you call two methods sequentially and each generates a reply. If you want to wait synchronously just for the second reply, you cannot do so without first handling the reply to the first method. This means WaitForIncomingMethodCall is still potentially confusing in what side effect it produces.

Actually we don't define the order in which replies need to be invoked at the mojom level.  I.e. if I have

interface Foo {
  One() => (int reply);
  Two() => (int reply);
};

and I invoke One() then Two(), there's no requirement at the mojom level that the implementation of Foo responds in that order.  It may hold on to the callback from method One() until it receives Two() or later.

Right, in such cases it is just all the more likely that the client could be confused by what next incoming method call to expect.

It seems like there is still a documentation requirement unless you truly invoke one method at a time, waiting for its callback to run before proceeding to the next method.

 
However, if I make those calls then I do know that the incoming methods are the replies to those two methods.  I can then either make the calls one at a time to enforce fully serialized order or rely on higher-level promises in the contract with Foo to know a method order.  The thing that my proposal will ensure is that the replies will be the only thing that the class dealing with the endpoint has to deal with.  That's a much simpler problem.

Hmm...

I agree it makes synchronous RPCs simpler since it better mimics function calls.

 
 

Another thing: The model of Foo and FooClient is really meant to mimic what people do in idiomatic C++. They often have a class Foo with a client interface that they set on Foo to receive asynchronous notifications. Foo methods can also take base::Callback<>, etc. When you just think about straight C++ usage like this in Chromium, you'll see that we don't struggle with the ordering problems that much, and so maybe the struggles we experience with Mojom are not as bad as they may sometimes seem?

I don't agree that this pattern is really idiomatic C++, or that having explicit state machines in Chromium is easy to deal with.  On the first point, in C++ there's a growing trend to use fibers / {cooperative,lightweight,green} threads to deal with concurrent programming since the model is much simpler than fully asynchronous state machine advancement.  We write code in the complicated way in Chromium because the IPC system doesn't really give us much choice - we are going to get a large set of incoming messages and need to do something with them.  I'd also argue that we have piles and piles of complexity and bugs related to message ordering especially whenever anything synchronous is added to the mix.  See any of the NPAPI deadlocks or difficult pepper ordering issues we've had or the continuing struggles to rationally schedule different types of messages.

We're just talking about the observer pattern. I agree it has its complications. True, it is commonly motivated in Chromium by how the IPC system works, but it also motivated by how UI toolkits work. The concept of event driven programming and the designation of a main thread leads you down this path.

 

Mojom gives us an ability that chrome IPC does not have to separate concerns into separate interfaces, which sidesteps a lot of these problems.  Thus idiomatic code in mojo will look different from idiomatic Chromium code as it is operating under a different set of constraints.  Idiomatic C++ code using a coroutine library (or Go code or C# code with async+await or whatever) looks very different from Chromium IPC handling logic.  I'd argue it's much simpler to write and reason about as well.

- James


Is anyone coding client software this way? I worry about the overhead of threads (even fibers / co-routines) and of the complexity of scheduling work across so many threads. With client software there's of course a strong need to thoughtfully schedule work to hit rendering budgets, etc. It seems like adding more threads may complicate scheduling.

Please also note that Mojo message pipes are not super cheap. They still involve allocating a buffer and doing some setup. Without support for associated message pipes, I'm worried we'll have trouble adapting current APIs to work without Client interfaces as we'll introduce ordering issues. We should probably review the interfaces we have to understand what it would look like to not have Client interfaces.

Note: I converted URLLoader to just work with callbacks some time ago. You might review it to see how its events work and whether you really like it. You could compare it to blink::WebURLLoader, which of course uses the Client interface pattern instead.

-Darin

Darin Fisher

unread,
Dec 17, 2014, 12:57:16 AM12/17/14
to James Robinson, mojo...@chromium.org
By the way, I'm not clear why the Client interface causes problems for synchronous programming. Maybe we just need a variant of WaitForIncomingMethodCall that waits for the next reply instead?

Imagine you have a C++ class like this:

class Foo {
 public:
  Foo(FooClient* client);
  void DoStuff();
};

During the call to DoStuff(), it could be reasonable for a method on FooClient to be invoke before DoStuff() returns.

If you had a variant of WaitForIncomingMethodCall that was instead WaitForIncomingReply, then you could model the above just fine using Mojom.

Maybe the above is the simpler primitive, and then the decision about when to use a client and when not to use a client is more of a question of style just as it is in normal code?

-Darin

Adam Barth

unread,
Dec 17, 2014, 1:33:30 AM12/17/14
to Darin Fisher, James Robinson, mojo...@chromium.org
Would you spin a nested message loop inside WaitForIncomingReply to handle other incoming messages that arrive before the reply?

Adam

James Robinson

unread,
Dec 17, 2014, 1:51:07 AM12/17/14
to Darin Fisher, mojo...@chromium.org
On Tue, Dec 16, 2014 at 9:57 PM, Darin Fisher <da...@chromium.org> wrote:
By the way, I'm not clear why the Client interface causes problems for synchronous programming.

Having a Client and having callbacks makes it difficult to reason about what messages might come in on a given handle.  This makes both asynchronous and synchronous programming harder.  Two things exacerbate this issue in our mojom syntax.  First, the client interface doesn't have to be anywhere near the interface definition.  There doesn't even have to be a Client= annotation within the file that defines a mojom interface (for example application.mojom vs shell.mojom).  Second, the syntax and placement of reply messages and client calls is different which obscures the fact that both are incoming messages on the handle.  Since we map both types of message to the same message pipe handle the class bound to the handle has to be at some level aware of the set of messages it needs to handle.

 
Maybe we just need a variant of WaitForIncomingMethodCall that waits for the next reply instead?

Imagine you have a C++ class like this:

class Foo {
 public:
  Foo(FooClient* client);
  void DoStuff();
};

During the call to DoStuff(), it could be reasonable for a method on FooClient to be invoke before DoStuff() returns.

In C++ if I make a call to Foo::DoStuff() then I know that the thing it's working on underneath that callstack is all related to that call, so it's probably not too difficult for me to arrange for FooClient to be ready to handle calls related to ::DoStuff().  With mojom it's different.  The receiver of the call could have generated calls on the client before it even received our DoStuff call and thus could be making completely unrelated calls on us.  It's much less clear that this is safe to do.

Looking through the mojoms we have the client calls that aren't direct message responses are often not really related to the unit of work that the other side of the message pipe is trying to work on.  Instead, we often have ways for both endpoints to make unrelated requests to each other.

If you had a variant of WaitForIncomingMethodCall that was instead WaitForIncomingReply, then you could model the above just fine using Mojom.

Maybe the above is the simpler primitive, and then the decision about when to use a client and when not to use a client is more of a question of style just as it is in normal code?

In my proposal nothing would preclude using a single object to handle incoming messages from two different message pipe handles, just like you can bind multiple message pipes to an object today.  In the case where it makes most sense to handle responses from one message pipe and incoming calls from another pipe together you could bind both handles to the same object.

Ordering is still hard.  I've been reading through our existing interfaces and haven't yet found a case where the ordering of client vs response messages is obviously significant, but I haven't read them all and certainly don't understand the ins and outs of every interface.  I feel fairly confident if we do find ordering issues we can resolve them, however, either by migrating the associated messages all to use one pipe (i.e. all be explicit client calls or returning a fresh pipe as a response) or by restructuring the protocol.

- James

Viet-Trung Luu

unread,
Dec 17, 2014, 2:33:43 AM12/17/14
to James Robinson, Darin Fisher, mojo...@chromium.org
I'm in favor of this proposal.

As already mentioned, client interfaces hamper synchronous programming. This is somewhat masked by pipelining (and interface requests), but the situation is rather bad: You can only write your program synchronously if you ignore incoming messages (and potentially let the queue grow unboundedly).

But what this really indicates is that client interfaces lend themselves to poor interface design.

E.g., an application cannot signal to the shell (by simply closing a message pipe) that it won't service any (or any more) requests (i.e., accept AcceptConnection messages), while still needing to connect to other applications (i.e., send ConnectToApplication() messages).

Instead, due to the Shell/Application coupling, either one implies the other. So if you need to connect to other applications, you're supposedly agreeing to accept connections as well (even if you're really just ignoring them).

Similarly for the ServiceProvider/ServiceProvider coupling.

This is very unnatural, even from a C++ perspective. The analogy is to say that whenever someone has a pointer to a given object, the object automatically has a pointer back at them. Having lots of people having pointers to your object is entirely natural, and they need not even agree on what your object is (they might think you're a FooObserver, a BarObserver, or a BazDelegate, or a Quux).

I don't think this has been mentioned, but eliminating client interfaces makes sharing of "outbound" message pipes across multiple threads natural. (It requires more sophisticated bindings code, but it's natural since there's never any question of where to send incoming requests -- since there are none.) I think this is already a problem, e.g., with the Shell interface.

To unsubscribe from this group and stop receiving emails from it, send an email to mojo-dev+u...@chromium.org.

Darin Fisher

unread,
Dec 17, 2014, 1:13:28 PM12/17/14
to Adam Barth, mojo...@chromium.org, James Robinson

Not a nested loop exactly, but it would dispatch Client method calls.

Darin Fisher

unread,
Dec 17, 2014, 1:17:34 PM12/17/14
to James Robinson, mojo...@chromium.org

Ok, I'm convinced. I agree that "wait for next reply" would result in seemingly random Client calls running while blocked, and that means undesirable complexity.

We don't actually have that many client interfaces. We will need to be careful about how we eliminate these. Blindly converting them to use a separate pipe would be dangerous. We need to make sure that doesn't create ordering problems.

-Darin

James Robinson

unread,
Dec 17, 2014, 1:21:30 PM12/17/14
to Darin Fisher, mojo...@chromium.org
Yes - any transformation here has to be done very careful.  What I've been doing so far is sampling random mojom files and trying to understand exactly what they're trying to do and how to transform them.  Most don't have client interfaces at all, or the client interface is not really that related to its peer.

I think the next steps here are to link the doc from the mojo docs list, identify the set of interfaces that would need to change, then go through the exercise of modifying them and getting a really careful review of a few samples to make sure it's working out.  Then if things still look good we can mark the client attribute as deprecated, work through eliminating the rest, then remove it from the generator, then we can simplify the bindings code.  We can even slim the message header down a bit since whether a message is a response or not will be known from the direction the message is going on the pipe so we don't need a bit for it.

- James

Alok Priyadarshi

unread,
Dec 17, 2014, 2:07:02 PM12/17/14
to James Robinson, Darin Fisher, mojo...@chromium.org
May be this has already been answered in the thread.

How should a pure listener be implemented in the new scheme?

In the current scheme the listener is generally implemented as a Client of the Service hosting the state. The listener connects to the service and registers itself as client.

In the new scheme, since the service cannot call the client directly, we need to switch the role of Client and Service. Now the service hosting the state needs to find and connect to all interested listeners. Is this is correct? How can the service be expected to find all the listeners?

If the proposal is just about adding an extra message pipe for the client interface, then it should be OK. Although in pure listener cases where you do not call any function on the service, just listen, you would end up with two pipes where one pipe is unused.

Elliott Sprehn

unread,
Dec 17, 2014, 2:21:03 PM12/17/14
to Alok Priyadarshi, James Robinson, Darin Fisher, mojo...@chromium.org
On Wed, Dec 17, 2014 at 11:07 AM, Alok Priyadarshi <al...@chromium.org> wrote:
May be this has already been answered in the thread.

How should a pure listener be implemented in the new scheme?

In the current scheme the listener is generally implemented as a Client of the Service hosting the state. The listener connects to the service and registers itself as client.

In the new scheme, since the service cannot call the client directly, we need to switch the role of Client and Service. Now the service hosting the state needs to find and connect to all interested listeners. Is this is correct? How can the service be expected to find all the listeners?


Why wouldn't this work like a non-IPC based API where the Service exposes an addListener() API, and the client calls it passing a handle to a Listener (Client) that it implements?

interface Listener {
 void focusDidChange(Event);
}

interface ViewManager {
  Subscription addFocusListener(Listener);
}

interface Subscription {
  void stopListening();
}

and if the Subscription pipe is disconnected because the listener process dies the ViewManager can observe the pipe closure and remove the listener from it's observer set.

Viet-Trung Luu

unread,
Dec 17, 2014, 2:24:20 PM12/17/14
to Alok Priyadarshi, James Robinson, Darin Fisher, mojo...@chromium.org
On Wed, Dec 17, 2014 at 11:07 AM, Alok Priyadarshi <al...@chromium.org> wrote:
May be this has already been answered in the thread.

How should a pure listener be implemented in the new scheme?

In the current scheme the listener is generally implemented as a Client of the Service hosting the state. The listener connects to the service and registers itself as client.

I'd expect that the typical implementation would be something like:

interface FooService {
  StartObserving(FooObserver observer);
};

interface FooObserver {
  DidBar();
  DidBaz();
  ...
};

Now, the object implementing FooService may not be the same one as the one making FooObserver calls, which is generally a good thing, I think.

In the new scheme, since the service cannot call the client directly, we need to switch the role of Client and Service. Now the service hosting the state needs to find and connect to all interested listeners. Is this is correct? How can the service be expected to find all the listeners?

If the proposal is just about adding an extra message pipe for the client interface, then it should be OK. Although in pure listener cases where you do not call any function on the service, just listen, you would end up with two pipes where one pipe is unused.

Well, the first message pipe could be closed, without the second one being torn down. It also allows for the possibility of the client setting up multiple independent observers, which may be desirable under some circumstances.

That said, the way ServiceProvider is currently specified, there's nothing stopping a "service" from being something that sends messages only in the opposite direction.
 
To unsubscribe from this group and stop receiving emails from it, send an email to mojo-dev+u...@chromium.org.

Hans Muller

unread,
Dec 17, 2014, 2:25:12 PM12/17/14
to Elliott Sprehn, Alok Priyadarshi, James Robinson, Darin Fisher, mojo...@chromium.org
An existing example of a pure listener is NativeViewport.setEventDispatcher().


To unsubscribe from this group and stop receiving emails from it, send an email to mojo-dev+u...@chromium.org.

Alok Priyadarshi

unread,
Dec 17, 2014, 3:27:18 PM12/17/14
to Hans Muller, Elliott Sprehn, James Robinson, Darin Fisher, mojo...@chromium.org
Thanks for the pointers. I was abusing the client interface out of misunderstanding as well.

So NativeViewportClient should really be implemented as a NativeViewportListener?

Hans Muller

unread,
Dec 17, 2014, 4:15:13 PM12/17/14
to Alok Priyadarshi, Elliott Sprehn, James Robinson, Darin Fisher, mojo...@chromium.org
There's an interesting problem here. In a world without clients, if the NativeViewport is destroyed before a listener is added, does it have to wait for a listener to be added? If the viewport had already been destroyed, then maybe the caller would see an error upon adding the listener because the viewport's message pipe had already been closed. 

Note quite as nice as being guaranteed that the NativeViewportClient's OnDestroyed() method runs before the viewport closes its message pipe down. Assuming that's true now.


Viet-Trung Luu

unread,
Dec 17, 2014, 4:23:58 PM12/17/14
to Hans Muller, Alok Priyadarshi, Elliott Sprehn, James Robinson, Darin Fisher, mojo...@chromium.org
On Wed, Dec 17, 2014 at 1:15 PM, 'Hans Muller' via mojo-dev <mojo...@chromium.org> wrote:
There's an interesting problem here. In a world without clients, if the NativeViewport is destroyed before a listener is added, does it have to wait for a listener to be added? If the viewport had already been destroyed, then maybe the caller would see an error upon adding the listener because the viewport's message pipe had already been closed.

Does (the server side of) the message pipe being closed mean that the NV was destroyed? (In which case watching for it being destroyed is equivalent to waiting for the peer being closed.)

Note that even if the NV had already been destroyed, the caller may or may not see an error on trying to add a listener, unless adding a listener results in a response being sent. (Regardless, the caller would eventually notice the listener pipe being closed.)

Hans Muller

unread,
Dec 17, 2014, 4:44:43 PM12/17/14
to Viet-Trung Luu, Alok Priyadarshi, Elliott Sprehn, James Robinson, Darin Fisher, mojo...@chromium.org
Note that even if the NV had already been destroyed, the caller may or may not see an error on trying to add a listener, unless adding a listener results in a response being sent. (Regardless, the caller would eventually notice the listener pipe being closed.)

Right.  In fact I guess that this situation already exists. Clients need to assume that the viewport has been destroyed if the service side of the message pipe is closed out from under them.  So not having a client hasn't made the situation worse, it's just brought the difficulties of tracking a NativeViewport's lifetime into focus.

Darin Fisher

unread,
Dec 17, 2014, 4:51:58 PM12/17/14
to Hans Muller, Alok Priyadarshi, Elliott Sprehn, James Robinson, mojo...@chromium.org
On Wed, Dec 17, 2014 at 1:15 PM, Hans Muller <hansm...@google.com> wrote:
There's an interesting problem here. In a world without clients, if the NativeViewport is destroyed before a listener is added, does it have to wait for a listener to be added? If the viewport had already been destroyed, then maybe the caller would see an error upon adding the listener because the viewport's message pipe had already been closed. 

Note quite as nice as being guaranteed that the NativeViewportClient's OnDestroyed() method runs before the viewport closes its message pipe down. Assuming that's true now.

We should probably move away from methods like OnDestroyed. Instead, clients should just monitor the lifetime of a message pipe. If an error is observed on the pipe, then you can assume the other side was destroyed (or will be).

-Darin

Satoru Takabayashi

unread,
Dec 19, 2014, 3:34:40 AM12/19/14
to Darin Fisher, James Robinson, mojo...@chromium.org
On Wed, Dec 17, 2014 at 2:41 PM, Darin Fisher <da...@chromium.org> wrote:


On Tue, Dec 16, 2014 at 2:52 PM, James Robinson <jam...@chromium.org> wrote:
On Tue, Dec 16, 2014 at 2:16 PM, Darin Fisher <da...@chromium.org> wrote:
It might be nice to put this in a document linked to from the mojo docs site (go/mojo-docs). That way we can comment on it directly.

That's a really good idea!  https://groups.google.com/a/chromium.org/d/msg/mojo-dev/eqsG0aeALLo/Klovi1HJAtsJ is world-readable and mojo...@chromium.org has comment access.

As you note, we talked a lot about making this change back when we introduced replies, and I even mostly hacked up a CL to implement it. It started to feel heavy weight, and yeah, the FIFO issue was an impediment. Maybe my approach felt heavyweight because I forced people to add their own setClient methods to their mojom interfaces. Maybe if we automated that part of it it wouldn't feel so bad.

I think the ordering problem you describe also exists to an extent in a world with only replies. Imagine if you call two methods sequentially and each generates a reply. If you want to wait synchronously just for the second reply, you cannot do so without first handling the reply to the first method. This means WaitForIncomingMethodCall is still potentially confusing in what side effect it produces.

Actually we don't define the order in which replies need to be invoked at the mojom level.  I.e. if I have

interface Foo {
  One() => (int reply);
  Two() => (int reply);
};

and I invoke One() then Two(), there's no requirement at the mojom level that the implementation of Foo responds in that order.  It may hold on to the callback from method One() until it receives Two() or later.

Right, in such cases it is just all the more likely that the client could be confused by what next incoming method call to expect.

It seems like there is still a documentation requirement unless you truly invoke one method at a time, waiting for its callback to run before proceeding to the next method.

 
However, if I make those calls then I do know that the incoming methods are the replies to those two methods.  I can then either make the calls one at a time to enforce fully serialized order or rely on higher-level promises in the contract with Foo to know a method order.  The thing that my proposal will ensure is that the replies will be the only thing that the class dealing with the endpoint has to deal with.  That's a much simpler problem.

Hmm...

I agree it makes synchronous RPCs simpler since it better mimics function calls.

What if you want to invoke both synchronous and asynchronous calls? Looks still difficult.

foo_proxy->One(CaptureResult(&result));  // Try to get the result asynchronously
...
foo_proxy->Two(CaptureResult(&result));  // Try to get the result synchronously
foo_proxy->WaitForIncomingMethodCall();  // Oops

Darin Fisher

unread,
Dec 19, 2014, 3:41:08 AM12/19/14
to Satoru Takabayashi, mojo...@chromium.org, James Robinson

Yes, that's a valid issue. One option would be to put the async and sync methods on different interfaces (different pipes). This means changing the API, which may of course not be ideal. If you wanted to mimic Chrome IPC style sync calls, then you would need a way to defer other incoming messages while waiting for a particular incoming message. That is something we could invent if desired.

-Darin

Viet-Trung Luu

unread,
Dec 19, 2014, 1:26:47 PM12/19/14
to Darin Fisher, Satoru Takabayashi, mojo...@chromium.org, James Robinson
From a given thread, mixing sync and async calls is dicey.

In particular, making a sync call when an async call is pending (with the callback bound to the same thread) should probably be prohibited.

But in the new universe, we'll probably have to track the number of outstanding calls (i.e., responses still pending) anyway, so this bad condition should be easy to detect.

In a thread-safe version of the new universe, this should be done on a per-thread basis; it should be fine for one thread to make async calls on a given message pipe while another thread makes sync calls on the same message pipe.

WaitForIncomingMethodCall() should at least be renamed, and made to die if there's more than one response pending (for the calling thread). Better yet would be to generate synchronous wrappers, so that you could just do something like:

foo_proxy->SyncTwo(&result);

(probably "connection" errors should be reported synchronously also). "SyncTwo" would die if there's already a response pending (for the calling thread).

("SyncTwo" is probably a  poor way to do it, but hopefully you get the gist. Maybe you'd wrap the proxy instead, and do something like "Sync(foo_proxy)->Two(&result);".)

To unsubscribe from this group and stop receiving emails from it, send an email to mojo-dev+u...@chromium.org.

Satoru Takabayashi

unread,
Dec 19, 2014, 6:59:58 PM12/19/14
to Viet-Trung Luu, Darin Fisher, mojo...@chromium.org, James Robinson
On Sat, Dec 20, 2014 at 3:26 AM, Viet-Trung Luu <viettr...@chromium.org> wrote:
From a given thread, mixing sync and async calls is dicey.

In particular, making a sync call when an async call is pending (with the callback bound to the same thread) should probably be prohibited.

I agree it's dicey, but if I remember correctly, PPAPI allows you to mix them on the same thread, hence this might become a problem when you port PPAPI apps to Mojo.


But in the new universe, we'll probably have to track the number of outstanding calls (i.e., responses still pending) anyway, so this bad condition should be easy to detect.

In a thread-safe version of the new universe, this should be done on a per-thread basis; it should be fine for one thread to make async calls on a given message pipe while another thread makes sync calls on the same message pipe.

WaitForIncomingMethodCall() should at least be renamed, and made to die if there's more than one response pending (for the calling thread). Better yet would be to generate synchronous wrappers, so that you could just do something like:

foo_proxy->SyncTwo(&result);

(probably "connection" errors should be reported synchronously also). "SyncTwo" would die if there's already a response pending (for the calling thread).

("SyncTwo" is probably a  poor way to do it, but hopefully you get the gist. Maybe you'd wrap the proxy instead, and do something like "Sync(foo_proxy)->Two(&result);".)

Sync wrappers sound great to me. I think it'd be cool if synchronous calls are treated as first class citizens. 

Aaron Boodman

unread,
Jan 5, 2015, 5:59:58 PM1/5/15
to Satoru Takabayashi, Viet-Trung Luu, Darin Fisher, mojo...@chromium.org, James Robinson
Can I get a confirmation as to whether we're going forward with this plan? It relates to some work I'm doing.

Thanks,

- a

James Robinson

unread,
Jan 5, 2015, 6:07:00 PM1/5/15
to Aaron Boodman, Satoru Takabayashi, Viet-Trung Luu, Darin Fisher, mojo...@chromium.org
Yes, this is the plan.  The next step is to remove the Client= annotation from mojom files and rewrite the code using these interfaces to match.  Darin and I have tackled a few of these.  I expect this process to take a solid chunk of time as we have lots of interfaces and the changes aren't trivial.  When the set of mojom interfaces using Clients gets close to zero, we can start removing support for the annotation from the bindings for different languages as usage hits 0 in that language.  When all Client= annotations are removed from mojom interfaces we can remove support from the bindings generator.

I'll be working on getting a more concrete plan with timeline estimates and targets this week.

- James

Darin Fisher

unread,
Jan 5, 2015, 6:12:56 PM1/5/15
to James Robinson, Aaron Boodman, Satoru Takabayashi, Viet-Trung Luu, mojo...@chromium.org
I think there are still question marks (at least in my head) around the idea of thread-safe bindings, but removing Client= support is a go.

I've written a patch for BatteryMonitor, but I haven't committed it yet. James has made much more progress :)

-Darin

Aaron Boodman

unread,
Jan 5, 2015, 6:21:16 PM1/5/15
to Darin Fisher, James Robinson, Satoru Takabayashi, Viet-Trung Luu, mojo...@chromium.org
Cool, thanks.

James Robinson

unread,
Jan 5, 2015, 6:23:23 PM1/5/15
to Darin Fisher, Aaron Boodman, Satoru Takabayashi, Viet-Trung Luu, mojo...@chromium.org
Yeah i'm not sure exactly what we'll do to the bindings after removing support for Client - there are probably multiple opportunities there - but that's further down the line.
Reply all
Reply to author
Forward
0 new messages