hello.js, world.js - A minimal application that connects to another.
wget.js - Uses the network service to load a URL.
cube.js - A JS version of examples/sample_app.
--- Running Mojo Applications ---
A Mojo application written in JavaScript is launched with mojo_shell like this:
mojo_shell
Where js-application-url is a URL understood by the shell. For example a file or an http URL that names a JS source file. The JS file itself must begin with a Mojo “shebang” that specifies the Mojo URL of the JS content handler. In other words, the first line of the JS source file must be:
#!mojo:js_content_handler
Following the shebang should be a single AMD module called “main” whose value is an Application subclass. The JS content handler will create an instance of the Application and make it the client of the Mojo shell. The JS content handler is itself a Mojo application and it's responsible for creating an instance of V8 and loading the “main” JS module and all of the modules the main module depends on.
This is the overall structure of a JS Mojo application:
#!mojo:js_content_handler
define(“main”, [“mojo/services/public/js/application”, ], function(appModule, ) { class MyApplication extends appModule.Application { constructor(appShell, url) { super(appShell, url); // Initializes this.shell, this.url. // MyApplication initializations here. }
initialize(args) { } acceptConnection(url, serviceProvider) { } } return MyApplication; });
The hello.js example is little more than this basic skeleton.
The JS content handler loads the “main” module and makes an instance of its value, which must be an Application subclass. The application's constructor is passed two arguments:
appShell - a pointer to the Mojo shell. Typically this will be wrapped by a Shell object, see below.
url - the URL this application was loaded from as a String.
The (inherited) Application class constructor initializes the shell and url properties. It‘s unlikely that you’ll want to use the appShell argument directly.
The initialize() and acceptConnection() methods are defined by application.mojom and they‘re needed because the JS content handler makes the JS application the Mojo shell’s client.
--- JavaScript Classes ---
The JS content handler depends on the ECMAScript6 (“Harmony”) classes feature.
--- Mojo Application Structure ---
Mojo applications can connect to services provided by other applications and they can provide services of their own. A service is an implementation of a Mojo interface that was defined as part of a Mojo module in a “.mojom” file.
To implement a service you‘ll need the JS “bindings” for the Mojo interface. The bindings are generated by the build system and end up in files whose name is the same as the ‘.mojom’ file with a ‘.js’ suffix. It’s often helpful to look at the generated ‘mojom.js’ files.
The JS Shell class simplifies connecting to applications and services. It‘s a wrapper for the Application’s appShell argument. The Application constructor creates a Shell and assigns it to |this.shell|.
The Shell's connectToService() method returns a “proxy” to a service provided by another application.
The JS bindings for a Mojo interface‘s API are delivered as a JS module whose name is based on the ‘.mojom’ file’s path. For example, to use the Mojo network service you need the JS module based on network_service.mojom:
define(“main”, [ “mojo/services/network/public/interfaces/network_service.mojom”, “mojo/services/public/js/application”, ] function(netModule, appModule) { class MyApplication extends appModule.Application { initialize(args) { var netService = this.shell.connectToService( “mojo:network_service”, netModule.NetworkService); // Use netService's NetworkService methods. } ... }
return MyApplication; });
The first connectToService() parameter is the Mojo URL for the network service application and the second is the JS “interface” object for NetworkService. The JS interface object's properties identify the (generated) JS bindings classes used to provide or connect to a service. For example (from network_service.mojom.js):
var NetworkService = { name: ‘mojo::NetworkService’, // Fully qualified Mojo interface name. proxyClass: NetworkServiceProxy, stubClass: NetworkServiceStub, // ... };
The ‘proxyClass’ is used to access another application's NetworkService and the ‘stubClass’ is used to create an implementation of NetworkService.
In the netService case above the Shell connects to the Mojo application at “mojo:network_service”, then connects to its service called ‘NetworkService.name’ with an instance of ‘NetworkService.proxyClass’. The proxy instance is returned. The netService proxy can be used immediately.
--- Mojo Responses are Promises ---
Mojo functions can return zero or more values called a “response”. For example the EchoString function below returns a string or null.
interface EchoService { EchoString(string? value) => (string? value); };
The response is delivered to the function caller asynchronously. In C++ the caller provides a Callback object whose Run() method has one argument for each response parameter. In JS, Mojo functions that specify a response return a Promise object. The Promise resolves to an object with one property per response parameter. In the EchoString case that would be something like {value: “foo”}.
Similarly, the implementation of a Mojo interface functions that specify a response, must return a Promise. The implementation of EchoString() could be written like this:
MyEchoStringImpl.prototype.EchoString = function(s) { return Promise.resolve({value: s}); };
When an application starts, its initialize() method runs and then its acceptConnection() method runs. The acceptConnection() method indicates that another application has connected to this one and it always runs at least once.
acceptConnection(initiatorURL, serviceProvider) { // provide services to the initiator here // request services from the initiator here }
The acceptConnection serviceProvider argument can be used to provide services to the initiator, and to request services from the initiator. An application can decide exactly what to do based on the initiator's URL. The serviceProvider argument is-a JS ServiceProvider, an object that wraps a Mojo ServiceProvider proxy.
The ServiceProvider requestService() method gets a proxy for a service from the initator and optionally provides a client implementation.
The ServiceProvider provideService() method registers an interface implementation factory for a Mojo interface. The factory function is provided with an proxy for the interface's client, if it has one.
An application can also connect to other applications and their services using its shell‘s connectToApplication() and connectToService() methods. The shell’s connectToApplication() returns a ServiceProvider. The shell‘s connectToService() method is just a convenience, it’s defined like this:
connectToService(url, service, client) { return this.connectToApplication(url).requestService(service, clientImpl); };
The value of service is an interface object that identifies a Mojo interface that the application at url implements.
The usage examples that follow are based on the following trivial Mojo interface:
interface EchoService { EchoString(string? value) => (string? value); };
-- Requesting a service using the Application's Shell
Given the URL of a Mojo application that implements the EchoService we can use the application‘s shell to get an EchoService proxy. Here’s a complete application:
#!mojo:js_content_handler
define(“main”, [ “console”, “mojo/services/public/js/application”, “services/js/test/echo_service.mojom” ], function(console, appModule, echoModule) {
class EchoShellRequest extends appModule.Application { initialize(args) { var url = "file:/foo/bar/echo.js"; var echoService = this.shell.connectToService(url, echoModule.EchoService); echoService.echoString("foo").then(function(result) { console.log("echoString(foo) => " + result.value); }); } } return EchoShellRequest;
});
-- Providing a service
A complete application that unconditionally provides the EchoService looks like this:
#!mojo:js_content_handler
define(“main”, [ “mojo/services/public/js/application”, “services/js/test/echo_service.mojom” ], function(appModule, echoModule) {
class EchoService extends appModule.Application { acceptConnection(initiatorURL, serviceProvider) { function EchoServiceImpl(client) { this.echoString = function(s) { return Promise.resolve({value: s}); }; } serviceProvider.provideService(echoModule.EchoService, EchoServiceImpl); } } return EchoService;
});
As you can see, EchoServiceImpl is just a function that returns an object that implements the methods in the Mojo EchoService interface. If the EchoService defined a client interface, the factory function‘s client parameter would be a proxy for the initiator’s client service. EchoService doesn't have a client so we could have omitted this parameter.
Each time another application connects to this one, the EchoServiceImpl function will be called. The caller will be able to run the echoString() method and will get its response via a Promise.
-- Final note
An initiator‘s serviceProvider object can be retained and used to request or provide services at any time, not just from within application’s acceptConnection() method.
--- Interface Parameters ---
The caller and callee use cases that follow are in terms of the following mojom:
[Client=Bar] interface Foo { }
[Client=Foo] // Redundant but always implicitly true. interface Bar { }
interface I { provideFoo(Foo foo); requestFoo(Foo& foo); // effectively: provideFoo(Bar bar) }
-- In General
From a user‘s point of view, the bindings are in terms of the (remote) proxy class and the (local) stub class’s implementation delegate (internally, that‘s the stub class’s delegate$ property). The bindings will add/use a local$ property on proxy objects which points to the stub class's implementation delegate. They also manage remote$ property on the implementation delegate whose value is the proxy.
All that implies:
fooImpl.remote$.local$ == fooImpl (the stub class's delegate) fooProxy.local$.remote$ == fooProxy
-- Callers
Assuming that we have a proxy for interface I, iProxy.
An iProxy.provideFoo() call implies that we have an implementation of Foo, and want a proxy for Bar (Foo's client).
var myFooImpl; provideFoo(myFooImpl); myFooImpl.remote$; // A Bar proxy initialized by provideFoo(), undefined if Foo has no client.
An iProxy.requestFoo() call implies that we have an implementation of Bar and want a proxy for Foo (Bar's client).
var myBarImpl; // If Foo has no client then this is just {}. requestFoo(myBarImpl); myBarImpl.remote$; // A Foo proxy initialized by requestFoo.
The wget.js example includes a request for the URLLoader service.
-- Callees
An implementation of provideFoo(Foo foo) implies that we have an implementation of Bar (Foo's client) and want a proxy to the Foo that has been passed to us.
void provideFoo(fooProxy) { fooProxy.local$ = myBarImpl; // sets myFooImpl.remote$ = fooProxy }
An implementation of requestFoo(Foo& foo) implies that we have an implementation of Foo and want a proxy for the Bar (Foo‘s client) that’s been passed to us.
void requestFoo(barProxy) { barProxy.local$ = myFooImpl; // sets myFooImpl.remote$ = barProxy }