This document is a subset of the Mojo documentation.
[TOC]
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.
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.
This is not an exhaustive list.
These are not supported.
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.
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.
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.
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.
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.
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 |
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.)
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.
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.)
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);
};
// 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.
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).
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.
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);
};
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.
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.
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.
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
-
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
-
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.) ↩
-
Passing a
pending_remote<InterfaceType>
over IPC to Java will cause it to be automatically converted to anInterfaceType.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. ↩ -
Associated interfaces are not supported in Java. Attempting to use them will cause an
AssociatedInterfaceNotSupportedException
orAssociatedInterfaceRequestNotSupportedException
. ↩ ↩2