Skip to content

Latest commit

 

History

History
671 lines (539 loc) · 26.1 KB

File metadata and controls

671 lines (539 loc) · 26.1 KB

Mojo Java Bindings API

This document is a subset of the Mojo documentation.

[TOC]

Overview

The Mojo Java Bindings API leverages the Java System API to provide a more natural set of primitives for communicating over Mojo message pipes. Combined with generated code from the Mojom IDL and bindings generator, users can easily connect interface clients and implementations across arbitrary intra- and inter-process boundaries.

This document provides a brief guide to Java Bindings API usage with example code snippets. Some foundational knowledge of Mojo and mojom interface definitions is assumed. Familiarity with the C++ bindings may be useful for comparison but isn't strictly required. For detailed API references you can consult the class definitions in this directory.

Should I or should I not use the Java bindings?

Java bindings typically only make sense on the browser side and for Android devices where there is some service that's unique to Android. As such, Java bindings are typically rarely used, but they are used sometimes. When implementing a cross-platform feature or service, it's probably more appropriate to use C++ Mojo bindings and (if necessary) plumb the platform-specific code implemented in Java via JNI.

C++ bindings have more features than the Java bindings. In simple cases, Java bindings provide enough functionality to do everything by themselves. However, for complex usage, it may be necessary to instead use the C++ bindings and plumb data through JNI. See the section on notable lacking features below.

However, working within Java can avoid boilerplate and complex memory management between C++ and Java.

If your service doesn't really need to be implemented in Java (because it doesn't depend on or benefit from any Java-exclusive APIs), you should probably just write the whole thing in C++.

Implementing Mojo interfaces in Java is subject to the same Chromium IPC practices and review processes. However, most reviewers are used to reviewing C++ code.

Notable lacking features

This is not an exhaustive list.

AssociatedInterfaces

These are not supported.

ReportBadMessage

There is no way for Java to report a bad message, which is the standard practice when the renderer sends a message that indicates it's misbehaving (and perhaps compromised).

The best you can do if you receive a bad message is to just ignore the request. However, try to close any Proxy objects or InterfaceRequests that might not otherwise have needed closing.

If the IPC call usually expects a response (via a callback), but the callback has not been called and the callback gets garbage collected, this will lead to the Mojo connection being disconnected. The involvement of the garbage collector makes this somewhat non-deterministic, so this behavior should not be relied upon.

Missing type conversions (StructTraits)

There are a few non-primitive but common standard library-like mojom types. The C++ bindings allow the definition of custom conversions between C++ and mojom types via StructTraits. The auto-generated Java interfaces don't implement these custom conversions as they don't support StructTraits, so you don't get the obvious conversions between these mojom and Java types. This isn't necessarily a deal-breaker though, as you can still implement the conversions yourself (with care).

An example is String16, representing a UTF-16 string. For String16, C++ can convert transparently to/from std::u16string. However, in Java, this merely produces something which instead resembles the underlying mojom struct - which just contains a short[] array.

Synchronous calls

You cannot perform synchronous method calls from Java. However, Java can still receive sync calls (as [Sync] annotations only affect the behavior on the caller side). In general, you shouldn't need to make synchronous calls as they have many disadvantages.

Key differences compared to C++ bindings

The Java bindings' APIs don't particularly use the terms "receiver" and "remote". Instead, the terms "Interface" and "Proxy" might be used. However, this document will often use a mixture of these terms, as "pending", "remotes", and "receivers" are terms used in mojom.

In C++, you'll typically have a pending_receiver<InterfaceType> which you then bind to an implementation of InterfaceType. In Java, this is an InterfaceRequest<InterfaceType>, which can be bound to your implementation of InterfaceType.

In C++, you typically have to hold on to the receiver for as long as you want it alive, or deliberately use a "self-owned" receiver that stays alive as long as the other side of the IPC connection needs it. In Java, Mojo will keep the underlying IPC connection and your interface implementation (receiver) alive as long as the other end stays alive. In practice, this behaves somewhat like self-owned receivers.

In C++, there is generally a greater distinction between remotes and pending_remotes. In Java, you will typically just deal with an InterfaceType.Proxy object, which can be used as either a remote or pending_remote interchangeably.

Object and connection lifetimes

When implementing an IPC interface, you'll also need to implement a close() method and an onConnectionError() method. The close() method will automatically be called whenever the IPC disconnects (either gracefully or due to an error). The onConnectionError() method will only be called (in addition to close()) when the connection ends due to an error.

A Mojo connection that's bound to an implementation object will keep that object alive. The connection is maintained by Mojo so long as the other (remote) end is alive (bound or unbound). Note that calling the close() method on your interface implementation yourself won't stop calls from coming in! In java, the receiver generally isn't able to unilaterally end a connection cleanly.

A Proxy object will keep any connection it's bound to alive until you call close() on it.

No longer needed Proxy objects and InterfaceRequests which are to be discarded without binding should be closed. Failing to do so can lead to resource leaks, produce warnings, or lead to non-deterministic crashes. Failing to close Proxy objects may produce warnings in log output such as java.lang.IllegalStateException: Warning: Router objects should be explicitly closed when no longer required otherwise you may leak handles.. Failing to close InterfaceRequests may produce Handle was not closed. warnings.

Types

Primitive types

mojom type non-nullable type nullable type Comment
bool boolean Boolean
int8, uint8 byte Byte Signed and unsigned1
int16, uint16 short Short Signed and unsigned1
int32, uint32 int Integer Signed and unsigned1
int64, uint64 long Long Signed and unsigned1
string String String Character encoding2
array<T> T[] T[]
array<T, N> T[] T[]
map<S, T> Map<S, T> Map<S, T>
handle Handle Handle
handle<message_pipe> MessagePipeHandle MessagePipeHandle
handle<shared_buffer> SharedBufferHandle SharedBufferHandle
handle<data_pipe_producer> DataPipe.ProducerHandle DataPipe.ProducerHandle
handle<data_pipe_consumer> DataPipe.ConsumerHandle DataPipe.ConsumerHandle
pending_remote<T> T or T.Proxy T or T.Proxy Automatic conversions3
pending_receiver<T> InterfaceRequest<T> InterfaceRequest<T>
pending_associated_remote<T> - - Unsupported4
pending_associated_receiver<T> - - Unsupported4

Enums

enum CoffeeType {
  LATTE,
  ESPRESSO,
  CAPPUCCINO,
};
// Non-nullable enum
@CoffeeType.EnumType long coffeeType = CoffeeType.LATTE;
// Nullable enum
@CoffeeType.EnumType Long coffeeType = null;

Enums are just integers with an annotation.

The constants for the different variants of an enum are available directly under the enum's generated Java class and use SCREAMING_SNAKE_CASE. (Note that this is unlike the tags for unions.)

Structs

struct BrewCoffeeRequest {
  CoffeeType coffee_type;
  uint64 beans;
  double litres_of_water;
  double litres_of_milk;
  double kilos_of_sugar;
  string? customer_name;
  // Set to null to automatically allocate some cups
  uint64? cups;
};
// Create an instance. You should not make any assumptions about the default
// state as it may be invalid.
BrewCoffeeRequest request = new BrewCoffeeRequest();
// Make sure to set all the fields. Each data member is public without setters
// or getters.
request.coffeeType = CoffeeType.LATTE;
request.beans = 30;
request.litresOfWater = 0.448;
request.litresOfMilk = 0.05;
request.kilosOfSugar = 0.002;
request.customerName = null;
request.cups = new Long(2);

As there are no getters or setters, there are no nullness checks when reading or writing to the data members of a struct. The members of a newly constructed struct object which are specified as non-nullable in the mojom may in fact be null-initialized and need to be filled out before the struct is sent across IPC. Serialization and deserialization when being sent or received across a Mojo channel will both perform nullness checks. If you receive a struct over IPC, it is guaranteed to comply with the nullability specified in the mojom file.

Unions

union BrewCoffeeResponse {
  uint64 cups_of_coffee;
  string error_message;
};
// Create an instance. You should not make any assumptions about the default
// value as it may be invalid.
BrewCoffeeResponse response = new BrewCoffeeResponse();
// Set it to the cups_of_coffee variant with a well-defined value.
response.setCupsOfCoffee(2);
// Alternatively, set it to the error_message variant.
response.setErrorMessage("I am a teapot");

// Reading a union
switch (response.which()) {
    case BrewCoffeeResponse.Tag.CupsOfCoffee:
        Log.i(TAG, "We got %d cups of coffee.\n", response.getCupsOfCoffee());
        break;
    case BrewCoffeeResponse.Tag.ErrorMessage:
        Log.e(TAG, "Could not brew coffee: %s\n.", response.getErrorMessage());
        break;
    default:
        // Your implementation is probably out of sync with the mojom.
        throw new IllegalArgumentException("Unknown tag");
}

The content of a newly constructed union object may be null-initialized, even if the variant is specified as non-nullable in the mojom. Serialization and deserialization when being sent or received across a Mojo channel will generally both perform nullness checks. (crbug.com/337849882: Invalid union data received over IPC may deserialize into a union with a default-constructed state. This may result in null content for a non-nullable variant.)

When using a getter, the autogenerated mojo code will assert that the union has the correct variant/tag. (This is roughly equivalent to a DCHECK.)

Java Unions internally hold the data for separate variants under separate member variables. These members are not cleared/nullified when other variants are set and therefore may continue to hold references to any nested data under them.

The constants for the different tags/variants of a union are available under a static Tag class under the union's generated Java class and use PascalCase. (Note that this is unlike the variants for enums.)

Interfaces

Consider the following interface used as an example in the following sections:

interface CoffeeMachine {
  const double kUsCupInLitres = 0.236588;
  const double kUsLegalCupInLitres = 0.24;
  const double kImperialCupInLitres = 0.284131;

  EnterLowPowerMode();
  PerformCleaningCycle() => ();
  BrewCoffee(BrewCoffeeRequest request) => (BrewCoffeeResponse response);
};

Making IPC calls

// Constant names are converted to Java style
Log.i(TAG, "There are %f US cups in 1 litre",
        1.0 / CoffeeMachine.US_CUP_IN_LITRES);

// coffeeMachine is of type CoffeeMachine.Proxy (which is itself an
// implementation of CoffeeMachine).

// Calling a method with no input, output, or even a completion callback.
coffeeMachine.enterLowPowerMode();

// Calling a method with no input or output, but that has a response callback
// for completion.
CoffeeMachine.PerformCleaningCycle_Response callback =
        new CoffeeMachine.PerformCleaningCycle_Response() {
    @Override
    public void call() {
        Log.i(TAG, "The cleaning cycle has finished");
    }
};
coffeeMachine.performCleaningCycle(callback);

// Calling a method with an input and an output.
CoffeeMachine.BrewCoffee_Response callback =
        new CoffeeMachine.BrewCoffee_Response() {
    @Override
    public void call(BrewCoffeeResponse response) {
        // Handle the result of the brew coffee request here.
    }
};
coffeeMachine.brewCoffee(brewCoffeeRequest, callback);

Calling these methods is asynchronous and will not block the caller.

Note that calling a method with legal inputs will not itself ever fail with an exception if there is an IPC problem. Instead, you should set a ConnectionErrorHandler on your Proxy objects if you wish to detect errors.

// coffeeMachine is of type CoffeeMachine.Proxy
ConnectionErrorHandler connectionErrorHandler = new ConnectionErrorHandler() {
    @Override
    public void onConnectionError(MojoException e) {
        // Handle the error.
    }
};
coffeeMachine.getProxyHandler().setErrorHandler(connectionErrorHandler);

Once you are done with your Proxy object, assuming you haven't transferred it across IPC, make sure to close it.

coffeeMachine.close();

Holding onto an unclosed Proxy will generally keep an IPC connection alive (unless the other side unilaterally closes it). If you forget to close it, you may leak resources (on both sides of the IPC!), get warnings, or trigger non-deterministic crashes.

Receiving IPC calls

class CoffeeMachineImpl implements CoffeeMachine {
    @Override
    public void onConnectionError(MojoException e) {
        // Handle an error here. Note that close() will also be called.
    }

    @Override
    public void close() {
        // Closed - either gracefully or through an error.
    }

    @Override
    public void enterLowPowerMode() {
        Log.i(TAG, "Zzz...");
    }

    @Override
    public void performCleaningCycle(
            CoffeeMachine.PerformCleaningCycle_Response callback) {
        Log.i(TAG, "Cleaned!");
        callback.call();
    }

    @Override
    public void brewCoffee(
            BrewCoffeeRequest request,
            CoffeeMachine.BrewCoffee_Response callback) {
        CoffeeMachine.BrewCoffeeResponse response =
                new CoffeeMachine.BrewCoffeeResponse();
        response.setErrorMessage("I am a teapot");
        callback.call(response);
    }
}

When implementing a receiver for your interface, you will need to override the onConnectionError(MojoException) and close() methods in addition to your mojom-defined methods.

Whilst it's technically possible for a single implementation object to be bound as the handler for multiple channels or receivers, you should avoid this as the onConnectionError(MojoException) and close() methods do not provide any explicit information about which channel their calls relate to. It's also conceptually confusing if you continue to use a Closeable that may have been closed.

Note that a Mojo connection that's bound to an implementation object will keep that implementation object alive. However, your implementation object has no inherent influence over the lifetime of the connection (after all, you're just implementing an interface).

Registering, mapping, binding, and passing interfaces

Working with interface brokers

Interface brokers are how the renderer process usually obtains access to an IPC interface. In Java, this will typically bind the receiver implementations for you.

See a few examples from code search. There are a few examples for general registrars for Android WebView and Chrome on Android. You will likely be able to re-use one of these for your purposes.

Adding your implementation to the binder mapping happens in the normal C++ *_bindings.cc or *_binders.cc files, which are subject to IPC security reviews. The mapping code will route over to the Java implementation. An InterfaceRegistrar, InterfaceRegistry, and a factory for your implementation class is typically used to perform the binding of an implementation to an interface that's obtained via the browser interface broker.

Much as how different layers or components use different *_bindings.cc and *_binders.cc files to expose your IPC implementation via interface brokers, there are also different interface registrars. Sometimes, their approaches/implementations can be a little inconsistent. Choosing which interface registrar to use, or creating an entirely new one, is dependent on the use case and is outside of the scope of this document. We will only briefly look at adding a hypothetical blink-defined "CoffeeMachine" interface to Android WebView as an example.

//android_webview/browser/aw_content_browser_client_receiver_bindings.cc:

template <typename Interface>
void ForwardToJavaFrame(content::RenderFrameHost* render_frame_host,
                        mojo::PendingReceiver<Interface> receiver) {
  render_frame_host->GetJavaInterfaces()->GetInterface(std::move(receiver));
}

void BindCoffeeMachineReceiver(
    content::RenderFrameHost* render_frame_host,
    mojo::PendingReceiver<blink::mojom::CoffeeMachine> receiver) {
  // Optional: if you wanted to perform any pre-binding checks, such as to
  // make sure that the origin is secure, etc, now is the time to do it:
  const url::Origin& origin = render_frame_host->GetLastCommittedOrigin();
  if (!network::IsOriginPotentiallyTrustworthy(origin)) {
    mojo::ReportBadMessage(
        "Attempted to access CoffeeMachine from non-trustworthy origin.");
    return;
  };

  ForwardToJavaFrame<blink::mojom::CoffeeMachine>(
      render_frame_host, std::move(receiver));
}

void AwContentBrowserClient::RegisterBrowserInterfaceBindersForFrame(
    content::RenderFrameHost* render_frame_host,
    mojo::BinderMapWithContext<content::RenderFrameHost*>* map) {
  map->Add<blink::mojom::CoffeeMachine>(
      base::BindRepeating(&BindCoffeeMachineReceiver));
  // ... Mappings for other interfaces
}

//android_webview/java/src/org/chromium/android_webview/AwInterfaceRegistrar.java:

class AwInterfaceRegistrar {
    @CalledByNative
    private static void registerMojoInterfaces() {
        InterfaceRegistrar.Registry.addRenderFrameHostRegistrar(
                new AndroidWebviewRenderFrameHostInterfaceRegistrar());
    }

    private static class AndroidWebviewRenderFrameHostInterfaceRegistrar
            implements InterfaceRegistrar<RenderFrameHost> {
        @Override
        public void registerInterfaces(
                InterfaceRegistry registry,
                RenderFrameHost renderFrameHost) {
            // A made up example:
            registry.addInterface(
                    CoffeeMachineImpl.MANAGER,
                    new CoffeeMachineImplFactory(renderFrameHost));
            // ... Calls for other interfaces.
        }
    }
}

CoffeeMachineImplFactory.java:

public class CoffeeMachineImplFactory implements InterfaceFactory {
    private final RenderFrameHost mRenderFrameHost;

    public CoffeeMachineFactory(RenderFrameHost renderFrameHost) {
        mRenderFrameHost = renderFrameHost;
    }

    @Override
    public CoffeeMachineImpl createImpl() {
        return new CoffeeMachineImpl(mRenderFrameHost);
    }
}

However, if you're passing pending_receivers around as part of the IPC calls of your interfaces, you will need to do the binding yourself, as described in the next sections.

Passing interfaces through IPC calls (pending_receivers and pending_remotes)

Consider the following interfaces used as an example in the following sections:

interface CoffeeMachine {
  BrewCoffee(BrewCoffeeRequest request) => (BrewCoffeeResponse response);
  // ...
};

interface Employee {
  ProvideCoffeeAccess(pending_remote<CoffeeMachine> coffee_machine);
};

interface Kitchen {
  RequestCoffeeAccess(pending_receiver<CoffeeMachine> coffee_machine);
};

Receiving a pending_receiver and binding to it

There is a bit of a terminology mismatch in Java compared to C++/mojom where Java uses some older phrasing. Notably, pending_receivers are called "InterfaceRequests" in Java (this actually used to be the old term used in other bindings like C++). Once received on the Java side, these can then be bound to an implementation.

class KitchenImpl implements Kitchen {
    // ...
    @Override
    public void requestCoffeeAccess(
            InterfaceRequest<CoffeeMachine> interfaceRequest) {
        CoffeeMachine coffeeMachine = new CoffeeMachineImpl();
        CoffeeMachine.MANAGER.bind(coffeeMachine, interfaceRequest);
    }
}

If you do not intend to bind an implementation to an InterfaceRequest and instead wish to just discard it, you should close() it.

Receiving a pending_remote and using it

Pending remotes are automatically bound and converted to Proxy objects when received.

class EmployeeImpl implements Employee {
    // ...
    @Override
    public void provideCoffeeAccess(CoffeeMachine coffeeMachine) {
        // We can immediately use the remote/proxy
        coffeeMachine.brewCoffee(mMyUsualPlease, mDrinkCoffeeCallback);
        coffeeMachine.close();

        // Or, we could pass the proxy onto somewhere else, and it will be
        // unbound from the proxy and converted from a remote back into a
        // pending remote.
        mColleague.provideCoffeeAccess(coffeeMachine);
        // Do not close() if passed elsewhere.
    }
}

If you are finished with the remote/proxy (and have not passed it on elsewhere), you should close() it.

Creating and sending pending_receivers and pending_remotes

You can create a connected pair of an InterfaceType.Proxy and an InterfaceRequest<InterfaceType>. You can then either use these yourself or send them over IPC.

// import org.chromium.mojo.system.Pair;
// import org.chromium.mojo.system.impl.CoreImpl;

Pair<CoffeeMachine.Proxy, InterfaceRequest<CoffeeMachine>> pair =
        CoffeeMachine.MANAGER.getInterfaceRequest(CoreImpl.getInstance());

// Both the proxy and the interface request can used by us or be sent over IPC.
// Note that calls can be queued via the proxy/remote even before the
// pending_receiver/interface request is bound to an implementation.
employee.provideCoffeeAccess(pair.first);
kitchen.requestCoffeeAccess(pair.second);

There is a shortcut if you want to provide the other side of the IPC with a remote to an implementation you own. Instead of creating a pair of a Proxy and an InterfaceRequest, you can directly supply your implementation object instead of a proxy and Mojo will create, bind, and send a pending remote for you:

CoffeeMachine coffeeMachine = new CoffeeMachineImpl()
employee.provideCoffeeAccess(coffeeMachine);

Note that whether you send a pending_remote one way or send a pending_receiver the other way can have an effect on ownership, lifetimes, and performance. It may be better to supply a pending_receiver as an argument to a method call than to wait for a pending_remote from the response callback - especially if you want to queue up messages before all binding has taken place.

Threading

When receiving IPC calls, Mojo will invoke your interface implementation's methods on the thread/sequence on which the implementation was bound. This also extends to the close and onConnectionError methods. (This is similar to how things work in C++.)

You should generally only use the Mojo APIs from within a valid sequence, though purely manipulating mojom-generated Java data types (structs, enums, unions) is OK. Various Mojo method calls will crash if invoked from outside of a valid sequence.

Note that ordinary Proxy objects are not thread safe and should generally only be used from the thread on which they are created. However, you can create a thread safe wrapper around your proxy object using InterfaceType.MANAGER.buildThreadSafeProxy(originalProxy), which will forward calls to the thread on which the thread-safe proxy wrapper was created. You must therefore build the thread-safe proxy from the thread which owns the original proxy.

Footnotes

  1. Both signed and unsigned integers use the same Java types. Java has well-defined overflow and underflow behavior. If you need to represent the larger half of an unsigned number space, use the negative number space of the Java integer types. For example, a uint32 with value 0xFFFFFFFF, is represented by a -1 int in Java; 0xFFFFFFFE is -2; etc. 2 3 4

  2. Mojom strings are UTF-8 encoded, whilst Java strings are UTF-16 encoded. The Java Mojo bindings will automatically translate between the two encodings for you. However, you should bear in mind that certain invalid Unicode strings (for example, unpaired surrogates) can result in lossy conversions. (If you're dealing with binary data, you should not be using strings anyway.)

  3. Passing a pending_remote<InterfaceType> over IPC to Java will cause it to be automatically converted to an InterfaceType.Proxy before it is passed to any of your interface implementations. Proxy objects can also be passed over IPC as pending_remotes. Java's Mojo bindings do not really make a distinction between pending and non-pending remotes as C++ does and will just automatically bind, unbind, and rebind them as needed when crossing IPC boundaries. See the section on passing interfaces through IPC for details.

  4. Associated interfaces are not supported in Java. Attempting to use them will cause an AssociatedInterfaceNotSupportedException or AssociatedInterfaceRequestNotSupportedException. 2