Chromium IPC
Chromium has a multi-process architecture to become more secure and robust like modern operating systems, and it means that Chromium has a lot of processes communicating with each other. For example, renderer process, browser process, GPU process, utility process, and so on. Those processes have been communicating using IPC [1].
Why is Mojo needed?
As a long-term intent, the Chromium team wanted to refactor Chromium into a large set of smaller services. To achieve that, they had considered below questions [3]
- Which services we bring up?
- How can we isolate these services to improve security and stability?
- Which binary features can we ship?
They learned much from using the legacy Chromium IPC and maintaining Chromium dependencies over the past years. They felt a more robust messaging layer could allow them to integrate a large number of components without link-time interdependencies as well as help to build more and better features, faster, and with much less cost to users. So, that’s why Chromium team begins to make the Mojo communication framework.
From the performance perspective, Mojo is 3 times faster than IPC, and ⅓ less context switching compared to the old IPC in Chrome [3]. Also, we can remove unnecessary layers like content/renderer layer to communicate between different processes. Because combined with generated code from the Mojom IDL, we can easily connect interface clients and implementations across arbitrary inter-process boundaries. Lastly, Mojo is a collection of runtime libraries providing a platform-agnostic abstraction of common IPC primitives. So, we can build higher-level bindings APIs to simplify messaging for developers writing C++, Java, Javascript.
Status of migrating legacy IPC to Mojo
Igalia has been working on the migration since this year in earnest. But, hundreds of IPCs still remain in Chromium. The below chart shows the progress of migrating legacy IPC to Mojo [4].
Mojo Terminology
Let’s take a look at the key terminology before starting the migration briefly.
- Message Pipe: A pair of endpoints and either endpoint may be transferred over another message pipe. Because we bootstrap a primordial message pipe between the browser process and each child process, eventually this means that a new pipe we create ultimately sends either end to any process, and the two ends will still be able to talk to each other seamlessly and exclusively. We don’t need to use routing ID anymore. Each point has a queue of incoming messages.
- Mojom file: Define interfaces, which are strongly-typed collections of messages. Each interface message is roughly analogous to a single prototype message
- Remote: Used to send messages described by the interface.
- Receiver: Used to receive the interface messages sent by Remote.
- PendingRemote: Typed container to hold the other end of a Receiver’s pipe.
- PendingReceiver: Typed container to hold the other end of a Remote’s pipe.
- AssociatedRemote/Receiver: Similar to a Remote and a Receiver. But, they run on multiple interfaces over a single message pipe while preserving message order, because the AssociatedRemote/Receiver was implemented by using the IPC::Channel used by legacy IPC messages.
Example of migrating a legacy IPC to Mojo
In the following example, we migrate WebTestHostMsg_SimulateWebNotificationClose to illustrate the conversion from legacy IPC to Mojo.
The existing WebTestHostMsg_SimulateWebNotificationClose IPC
- Message definition
File: content/shell/common/web_test/web_test_messages.h
IPC_MESSAGE_ROUTED2(WebTestHostMsg_SimulateWebNotificationClose,
std::string /*title*/, bool /*by_user*/)
- Send the message in the renderer
File: content/shell/renderer/web_test/blink_test_runner.cc
void BlinkTestRunner::SimulateWebNotificationClose(
const std::string& title, bool by_user) {
Send(new WebTestHostMsg_SimulateWebNotificationClose(
routing_id(), title, by_user));
}
- Receive the message in the browser
File: content/shell/browser/web_test/web_test_message_filter.cc
bool WebTestMessageFilter::OnMessageReceived(
const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(WebTestMessageFilter, message)
IPC_MESSAGE_HANDLER(
WebTestHostMsg_SimulateWebNotificationClose,
OnSimulateWebNotificationClose)
- Call the handler in the browser
File: content/shell/browser/web_test/web_test_message_filter.cc
void WebTestMessageFilter::OnSimulateWebNotificationClose(
const std::string& title, bool by_user) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetMockPlatformNotificationService()->
SimulateClose(title, by_user);
}
Call flow after migrating the legacy IPC to Mojo
We begin to migrate WebTestHostMsg_SimulateWebNotificationClose to WebTestClient interface from here. First, let’s see an overall call flow through simple diagrams. [5]
- The WebTestClientImpl factory method is called with passing the WebTestClientImpl PendingReceiver along to the Receiver.
- The receiver takes ownership of the WebTestClientImpl PendingReceiver’s pipe endpoint and begins to watch it for incoming messages. The pipe is readable immediately, so a task is scheduled to read the pending SimulateWebNotificationClose message from the pipe as soon as possible.
- The WebTestClientImpl message is read and deserialized, then, it will make the Receiver to invoke the WebTestClientImpl::SimulateWebNotificationClose() implementation on its bound WebTestClientImpl.
Migrate the legacy IPC to Mojo
- Write a mojom file
File: content/shell/common/web_test/web_test.mojom
module content.mojom;
// Web test messages sent from the renderer process to the
// browser.
interface WebTestClient {
// Simulates closing a titled web notification depending on the user
// click.
// - |title|: the title of the notification.
// - |by_user|: whether the user clicks the notification.
SimulateWebNotificationClose(string title, bool by_user);
};
- Add the mojom file to a proper GN target.
File: content/shell/BUILD.gn
mojom("web_test_common_mojom") {
sources = [
"common/web_test/fake_bluetooth_chooser.mojom",
"common/web_test/web_test.mojom",
"common/web_test/web_test_bluetooth_fake_adapter_setter.mojom",
]
- Implement the interface files
File: content/shell/browser/web_test/web_test_client_impl.h
#include "content/shell/common/web_test.mojom.h"
class WebTestClientImpl : public mojom::WebTestClient {
public:
WebTestClientImpl() = default;
~WebTestClientImpl() override = default;
WebTestClientImpl(const WebTestClientImpl&) = delete;
WebTestClientImpl& operator=(const WebTestClientImpl&) = delete;
static void Create(
mojo::PendingReceiver<mojom::WebTestClient> receiver);
private:
// WebTestClient implementation.
void SimulateWebNotificationClose(const std::string& title,
bool by_user) override;
};
- Implement the interface files
File: content/shell/browser/web_test/web_test_client_impl.cc
void WebTestClientImpl::SimulateWebNotificationClose(
const std::string& title, bool by_user) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetMockPlatformNotificationService()->
SimulateClose(title, by_user);
}
- Creating an interface pipe
File: content/shell/renderer/web_test/blink_test_runner.h
mojo::AssociatedRemote<mojom::WebTestClient>&
GetWebTestClientRemote();
mojo::AssociatedRemote<mojom::WebTestClient>
web_test_client_remote_;
File: content/shell/renderer/web_test/blink_test_runner.cc
mojo::AssociatedRemote<mojom::WebTestClient>&
BlinkTestRunner::GetWebTestClientRemote() {
if (!web_test_client_remote_) {
RenderThread::Get()->GetChannel()->
GetRemoteAssociatedInterface(&web_test_client_remote_);
web_test_client_remote_.set_disconnect_handler(
base::BindOnce(
&BlinkTestRunner::HandleWebTestClientDisconnected,
base::Unretained(this)));
}
return web_test_client_remote_;
}
- Register the WebTest interface
File: content/shell/browser/web_test/web_test_content_browser_client.cc
void WebTestContentBrowserClient::ExposeInterfacesToRenderer {
...
associated_registry->AddInterface(base::BindRepeating(
&WebTestContentBrowserClient::BindWebTestController,
render_process_host->GetID(),
BrowserContext::GetDefaultStoragePartition(
browser_context())));
}
void WebTestContentBrowserClient::BindWebTestController(
int render_process_id,
StoragePartition* partition,
mojo::PendingAssociatedReceiver<mojom::WebTestClient>
receiver) {
WebTestClientImpl::Create(render_process_id,
partition->GetQuotaManager(),
partition->GetDatabaseTracker(),
partition->GetNetworkContext(),
std::move(receiver));
}
- Call an interface message in the renderer
File: content/shell/renderer/web_test/blink_test_runner.cc
void BlinkTestRunner::SimulateWebNotificationClose(
const std::string& title, bool by_user) {
GetWebTestClientRemote()->
SimulateWebNotificationClose(title, by_user);
}
- Receive the incoming message in the browser
File: content/shell/browser/web_test/web_test_client_impl.cc
void WebTestClientImpl::SimulateWebNotificationClose(
const std::string& title, bool by_user) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetMockPlatformNotificationService()->
SimulateClose(title, by_user);
}
Appendix: A case study of Regression
There were a lot of flaky web test failures after finishing the migration of WebTestHostMsg to Mojo. The failures were caused by using ‘Remote’ instead of ‘AssociatedRemote’ for WebTestClient interface in the BlinkTestRunner class. Because BlinkTestRunner was using the WebTestControlHost interface for ‘PrintMessage’ as an ‘AssociatedRemote’. But, ‘Remote’ used by WebTestClient didn’t guarantee the message order between ‘PrintMessage’ and ‘InitiateCaptureDump’ message implemented by different interfaces(WebTestControlHost vs. WebTestClient). Thus, tests had often finished before receiving all logs. The actual results could be different from the expected results.
Changing Remote with AssociatedRemote for the WebTestClient interface solved the flaky test issues.
[1] Inter-process Communication (IPC)
[2] Mojo in Chromium
[3] Mojo & Servicification Performance Notes
[4] Chrome IPC legacy Conversion Status
[5] Convert Legacy IPC to Mojo