Mojo与Services入门指南
概述
本文档包含开发者高效使用 Chromium 中 Mojo 的最小知识集,涵盖 Mojo 接口使用示例、服务定义与对接,以及 Content 层核心服务的简要概览。更多文档详见 Mojo & Services。
Mojo 术语
- 消息管道 (Message Pipe) :由一对端点 (Endpoints) 组成,每个端点均有消息队列,写入一端会同步到另一端(对等端)。管道是双向的。
- mojom 文件 :定义强类型的接口 (Interfaces) ,每个接口包含多个消息 (Messages),类似 protobuf 消息。
Remote
:消息管道一端,用于发送接口定义的消息。Receiver
:另一端,用于接收并处理消息,需绑定到接口实现。
注意 :上述概括有些过于简化。请记住,消息管道仍然是双向的,mojom 消息有可能期待回复,回复从
Receiver
发回Remote
。
Receiver
必须与其mojom接口的实现进行关联(即绑定),以便处理接收到的消息。接收到的消息会作为一个调度任务分发,调用实现对象上相应的接口方法。 另一种理解方式是,Remote
会对其接口的远程实现进行调用,而该远程实现与对应的远程 Receiver
相关联。
示例:定义新Frame接口
让我们将其应用于 Chrome。假设我们想从render frame
向其对应的浏览器进程中的RenderFrameHostImpl
实例发送一条"Ping"消息。为此,我们需要定义一个合适的 mojom 接口,创建一个管道以使用该接口,然后将管道的一端连接到正确的位置,以便发送的消息可以在那里被接收和处理。本节将详细介绍该过程。
1.定义接口
创建 .mojom
文件:
cpp
// src/example/public/mojom/pingable.mojom
module example.mojom;
interface Pingable {
Ping() => (int32 random); // 接收 Ping 并返回随机数
};
构建规则,从而生成 C++ bindings:
python
# src/example/public/mojom/BUILD.gn
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [ "pingable.mojom" ]
}
2.创建管道
现在让我们创建一个消息管道来使用这个接口。
一般来说,在使用 Mojo 时,出于方便起见,接口的客户端(即
Remote
端)通常是创建新管道的一方。这很方便,因为Remote
端可以立即开始发送消息,而无需等待 InterfaceRequest 端点被传输或绑定到任何地方。
渲染器中创建管道:
cpp
// src/third_party/blink/example/public/pingable.h
mojo::Remote<example::mojom::Pingable> pingable;
mojo::PendingReceiver<example::mojom::Pingable> receiver =
pingable.BindNewPipeAndPassReceiver(); // 创建新管道并传递接收端
在这个示例中,pingable
是Remote
,而receiver
是一个PendingReceiver
,它是Receiver
的前身,最终会变成一个Receiver
。BindNewPipeAndPassReceiver
是创建消息管道最常见的方式:它将PendingReceiver
作为返回值。
注意:一个PendingReceiver实际上并不会做任何事情。它只是单个消息管道端点的惰性持有者。它的存在只是为了在编译时使它的端点类型更加明确,表示该端点期望由具有相同接口类型的Receiver进行绑定。
3.发送消息
最后,我们可以在 Remote
上调用 Ping()
方法来发送消息。
cpp
// src/third_party/blink/example/public/pingable.h
pingable->Ping(base::BindOnce(&OnPong)); // 发送 Ping 并设置回调
重要: 如果我们想要接收响应,就必须让
pingable
对象存活,直到OnPong
被调用。毕竟,pingable
拥有它的消息管道端点。如果它被销毁,那么端点也会被销毁,就没有任何东西可以接收响应消息了。 简言之,需保持pingable
存活至回调触发,否则端点销毁无法接收回复。
我们快完成了!当然,如果一切都这么容易,这个文档就不需要存在了。我们已经把从渲染器进程向浏览器进程发送消息这个难题,转化成了 只需要获取上面的receiver
对象,并以某种方式将其传递给浏览器进程,在那里它可以被转化为一个Receiver
,这个接收者会分发它接收到的消息。
4.发送PendingReceiver
至浏览器
值得注意的是,PendingReceiver
(以及一般的消息管道端点)只是另一种可以通过 mojom 消息自由发送的对象类型。在某处获取PendingReceiver
的最常见方式是将其作为方法参数传递给其他已连接的接口。 在渲染器的RenderFrameImpl
与其在浏览器中对应的RenderFrameHostImpl
之间始终连接的一个这样的接口是BrowserInterfaceBroker
。此接口是获取其他接口的工厂,它的GetInterface
方法接收一个GenericPendingReceiver
,这允许传递任意的 接口接收者(interface receivers)。
cpp
interface BrowserInterfaceBroker {
GetInterface(mojo_base.mojom.GenericPendingReceiver receiver);
}
由于GenericPendingReceiver
可以从任何特定的PendingReceiver
隐式构造,因此它可以调用此方法,并使用它之前通过BindNewPipeAndPassReceiver
创建的接收器对象。
cpp
RenderFrame* my_frame = GetMyFrame();
my_frame->GetBrowserInterfaceBroker().GetInterface(std::move(receiver)); // 移交接收端(通过`BrowserInterfaceBroker`接口传递)
这将把PendingReceiver
端点转移到浏览器进程,在那里它会被相应的BrowserInterfaceBroker
实现 接收。更多内容将在下面讨论。
5.实现接口
最后,我们需要在浏览器端实现我们的Pingable
接口。浏览器端实现类:
cpp
#include "example/public/mojom/pingable.mojom.h"
class PingableImpl : example::mojom::Pingable {
public:
explicit PingableImpl(mojo::PendingReceiver<example::mojom::Pingable> receiver)
: receiver_(this, std::move(receiver)) {} // 绑定接收端到实现
PingableImpl(const PingableImpl&) = delete;
PingableImpl& operator=(const PingableImpl&) = delete;
// example::mojom::Pingable:
void Ping(PingCallback callback) override {
// Respond with a random 4, chosen by fair dice roll.
std::move(callback).Run(4); // 返回固定值4
}
private:
mojo::Receiver<example::mojom::Pingable> receiver_;
};
RenderFrameHostImpl
拥有一个BrowserInterfaceBroker
的实现。当这个实现收到GetInterface
方法调用时,它会调用之前为该特定接口注册的处理程序。 ↓
cpp
// render_frame_host_impl.h (路径://content/browser/renderer_host/)
// RenderFrameHostImpl位于浏览器进程,不在渲染进程
class RenderFrameHostImpl
...
void GetPingable(mojo::PendingReceiver<example::mojom::Pingable> receiver);
...
private:
...
std::unique_ptr<PingableImpl> pingable_;
...
};
// render_frame_host_impl.cc
void RenderFrameHostImpl::GetPingable(
mojo::PendingReceiver<example::mojom::Pingable> receiver) {
pingable_ = std::make_unique<PingableImpl>(std::move(receiver));
}
// browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
// Register the handler for Pingable. 为Pingable接口 注册处理程序
map->Add<example::mojom::Pingable>(base::BindRepeating(
&RenderFrameHostImpl::GetPingable, base::Unretained(host))); // 关联接口与实现方法
}
我们完成了。这个设置足以在渲染器框架(renderer frame)与其浏览器端宿主对象(browser-side host object)之间建立一个新的接口连接! 假设我们让渲染器中的pingable
对象存活了足够长的时间,最终我们会看到它的OnPong
回调被调用,传入的值是完全随机的4
,正如上面的浏览器端实现所定义的那样。
服务概览和术语
上一节只是浅尝辄止地介绍了Mojo IPC在Chromium中的使用。虽然渲染器到浏览器的消息传递相对简单,并且可能是代码量上最普遍的用法,但我们正在逐步将代码库分解为一组服务,其粒度比传统的 Content browser/renderer/gpu/utility进程划分更为细致。
服务是一个自包含的代码库,它实现了一个或多个相关的功能或行为,并且与外部代码的交互完全通过Mojo接口连接进行,通常由浏览器进程作为中介。 简言之,服务是独立功能模块,通过 Mojo 接口与外部通信,通常由浏览器进程协调。
每个服务都定义并实现了一个主要的Mojo接口,浏览器可以通过该接口来管理服务的一个实例。
示例:构建简易跨进程服务(Example: Building a Simple Out-of-Process Service)
在Chromium中,通常涉及多个步骤来启动一个新服务:
- 定义主要服务接口和实现(Define the main service interface and implementation)
- 在进程外代码中连接实现(Hook up the implementation in out-of-process code)
- 编写一些浏览器逻辑以启动服务进程(Write some browser logic to launch a service process)
本节将逐步介绍这些步骤,并提供简要说明。有关此处使用的概念和API的更详细文档,请参见Mojo文档。
1.定义服务(Defining the Service)
通常服务定义放置在services
目录中,可以在树的顶层或某个子目录中。在这个例子中,我们将定义一个新的服务,专门供 Chrome 使用,因此我们将在//chrome/services
中定义它。
我们可以创建以下文件。首先是一些mojoms:
cpp
// src/chrome/services/math/public/mojom/math_service.mojom
module math.mojom;
interface MathService {
Divide(int32 dividend, int32 divisor) => (int32 quotient);
};
cpp
# src/chrome/services/math/public/mojom/BUILD.gn
import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [
"math_service.mojom",
]
}
然后是MathService
实现:
cpp
// src/chrome/services/math/math_service.h
#include "chrome/services/math/public/mojom/math_service.mojom.h"
namespace math {
class MathService : public mojom::MathService {
public:
explicit MathService(mojo::PendingReceiver<mojom::MathService> receiver);
MathService(const MathService&) = delete;
MathService& operator=(const MathService&) = delete;
~MathService() override;
private:
// mojom::MathService:
void Divide(int32_t dividend,
int32_t divisor,
DivideCallback callback) override;
mojo::Receiver<mojom::MathService> receiver_;
};
} // namespace math
cpp
// src/chrome/services/math/math_service.cc
#include "chrome/services/math/math_service.h"
namespace math {
MathService::MathService(mojo::PendingReceiver<mojom::MathService> receiver)
: receiver_(this, std::move(receiver)) {}
MathService::~MathService() = default;
void MathService::Divide(int32_t dividend,
int32_t divisor,
DivideCallback callback) {
// Respond with the quotient!
std::move(callback).Run(dividend / divisor);
}
} // namespace math
cpp
# src/chrome/services/math/BUILD.gn
source_set("math") {
sources = [
"math_service.cc",
"math_service.h",
]
deps = [
"//base",
"//chrome/services/math/public/mojom",
]
}
现在我们有了一个完全定义的MathService
实现 ↑ ,可以在进程内或进程外使用。
2.连接服务实现(Hooking Up the Service Implementation)
对于一个进程外的Chrome服务,我们只需在 //chrome/utility/services.cc
中注册一个工厂函数。
cpp
auto RunMathService(mojo::PendingReceiver<math::mojom::MathService> receiver) {
return std::make_unique<math::MathService>(std::move(receiver));
}
void RegisterMainThreadServices(mojo::ServiceFactory& services) {
// Existing services...
services.Add(RunFilePatcher);
services.Add(RunUnzipper);
// We add our own factory to this list
services.Add(RunMathService);
//...
完成此操作后,浏览器进程现在可以启动新的MathService
的进程外实例。↑
3.启动服务(Launching the Service)
如果您在进程内运行服务,实际上并没有什么有趣的事情需要做。您可以像实例化其他对象一样实例化服务实现,但是您也可以通过Mojo Remote
与其进行通信,就像它是在进程外一样。
要在上一部分完成连接操作后,启动进程外的服务实例,请使用 Content's ServiceProcessHost
API: 【浏览器进程启动:】
cpp
mojo::Remote<math::mojom::MathService> math_service =
content::ServiceProcessHost::Launch<math::mojom::MathService>(
content::ServiceProcessHost::Options()
.WithDisplayName("Math!")
.Pass());
除非发生崩溃,否则启动的进程的存活期将会与math_service
一样长。作为一个推论,你可以通过销毁(或重置)math_service
来强制终止进程。
我们现在可以进行进程外除法:(We can now perform an out-of-process division:)【执行远程调用:】
cpp
// NOTE: As a client, we do not have to wait for any acknowledgement or
// confirmation of a connection. We can start queueing messages immediately and
// they will be delivered as soon as the service is up and running.
// 注意:作为客户端,我们不必等待任何确认或连接确认。我们可以立即开始排队消息,一旦服务启动并运行,它们就会被发送。
math_service->Divide(
42, 6, base::BindOnce([](int32_t quotient) { LOG(INFO) << quotient; }));
注意:为了确保响应回调的执行,
mojo::Remote<math::mojom::MathService>
对象必须保持活动状态(请参见此部分和前一部分的提示)。
4.指定一个沙箱(Specifying a sandbox)
所有服务都必须指定一个沙箱。理想情况下,服务将在kService
进程沙箱内运行,除非它们需要访问操作系统资源。对于需要自定义沙箱的服务,必须与[email protected]协商定义新的沙箱类型。
定义接口沙箱的首选方法是在其.mojom
文件中指定一个[ServiceSandbox=type]
属性:
cpp
import "sandbox/policy/mojom/sandbox.mojom";
[ServiceSandbox=sandbox.mojom.Sandbox.kService]
interface FakeService {
...
};
有效值请参见//sandbox/policy/mojom/sandbox.mojom
。请注意,只有在使用content::ServiceProcessHost::Launch()
以进程外方式启动接口时才会应用沙箱。
作为最后的手段,可以实现基于动态或特征的映射到底层平台沙箱中,但这需要打通ContentBrowserClient(例如ShouldSandboxNetworkService()
)。 As a last resort, dynamic or feature based mapping to an underlying platform sandbox can be achieved but requires plumbing through ContentBrowserClient (e.g. ShouldSandboxNetworkService()).
Content层服务(Content-Layer Services Overview)
接口代理 (Interface Brokers)
我们定义了一个明确的mojom接口,该接口在 renderer's frame对象(RenderFrameImpl
) 和浏览器进程中的相应RenderFrameHostImpl
之间建立了持久连接。这个接口叫做BrowserInterfaceBroker
,使用起来相当简单:你只需要在RenderFrameHostImpl
上添加一个新的方法:
cpp
void RenderFrameHostImpl::GetGoatTeleporter(
mojo::PendingReceiver<magic::mojom::GoatTeleporter> receiver) {
goat_teleporter_receiver_.Bind(std::move(receiver)); // 绑定接收端
}
并在browser_interface_binders.cc
的PopulateFrameBinders
函数中注册此方法,该方法将特定接口映射到各自host中的处理程序:
cpp
// //content/browser/browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
map->Add<magic::mojom::GoatTeleporter>(base::BindRepeating(
&RenderFrameHostImpl::GetGoatTeleporter, base::Unretained(host))); // 关联接口
}
通过指定task runner,也可以将接口绑定到不同的序列上:
cpp
// //content/browser/browser_interface_binders.cc
void PopulateFrameBinders(RenderFrameHostImpl* host,
mojo::BinderMap* map) {
...
map->Add<magic::mojom::GoatTeleporter>(base::BindRepeating(
&RenderFrameHostImpl::GetGoatTeleporter, base::Unretained(host)),
GetIOThreadTaskRunner({}));
}
- Web Workers:JavaScript 多线程机制(Dedicated/Shared/Service Workers)
- BrowserInterfaceBroker:Chromium 中管理跨进程接口(Mojo)的核心组件,负责连接渲染进程和浏览器进程的接口实现
Workers在渲染器和浏览器进程中相应的远程实现之间也有BrowserInterfaceBroker连接。添加新的worker-specific接口与上述为frames添加接口的步骤类似,但有以下不同之处:
- 对于Dedicated Workers,添加一个新方法到DedicatedWorkerHost并在PopulateDedicatedWorkerBinders中注册它
- 对于Shared Workers,添加一个新方法到SharedWorkerHost并在PopulateSharedWorkerBinders中注册它
- 对于Service Workers,添加一个新方法到ServiceWorkerHost并在PopulateServiceWorkerBinders中注册它
通过在渲染器中的Blink Platform对象和浏览器进程中的相应RenderProcessHost
对象之间使用BrowserInterfaceBroker
连接,也可以添加进程级别的接口。这允许渲染器中的任何线程(包括frame和worker线程)访问该接口,但会带来额外的开销,因为使用的BrowserInterfaceBroker
实现必须是线程安全的。要添加新的进程级接口,请向RenderProcessHostImpl
添加新方法,并调用RenderProcessHostImpl::RegisterMojoInterfaces
中的AddUIThreadInterface
进行注册。在渲染器端,使用Platform::GetBrowserInterfaceBroker
来检索相应的BrowserInterfaceBroker
对象以调用GetInterface
。
要绑定 特定于嵌入器 的文档范围接口,请覆盖ContentBrowserClient::RegisterBrowserInterfaceBindersForFrame()
并将绑定器添加到提供的映射中。 For binding an embedder-specific document-scoped interface, override ContentBrowserClient::RegisterBrowserInterfaceBindersForFrame() and add the binders to the provided map.
注意:如果
BrowserInterfaceBroker
无法找到所请求接口的绑定器,它将在相关的context host中调用ReportNoBinderForInterface()
,这将导致在host的接收器上调用ReportBadMessage()
(后果之一是渲染器的终止)。为了避免在测试中出现这种崩溃(当 content_shell 不绑定某些 Chrome 特定接口,但渲染器仍然请求它们时),请使用browser_interface_binders.cc
中的EmptyBinderForFrame
helper。然而,如果可能的话,建议保持渲染器和浏览器端的一致性。
导航相关接口(Navigation-Associated Interfaces)
对于来自不同frame的消息排序很重要的情况,以及消息需要与实现导航的消息正确排序时,可以使用导航相关接口。导航相关接口利用每个框架到对应的RenderFrameHostImpl
对象的连接,并通过用于导航相关消息的相同FIFO管道发送来自每个连接的消息。因此,在导航之后发送的消息保证在导航相关消息之后到达浏览器进程,并且同一文档的不同框架发送的消息的顺序也得以保持。
要添加一个新的导航关联接口,请为 RenderFrameHostImpl
创建一个新方法,并在 RenderFrameHostImpl::SetUpMojoConnection
中通过调用 associated_registry_->AddInterface
注册它。从渲染器中,使用 LocalFrame::GetRemoteNavigationAssociatedInterfaces
获取一个对象以调用 GetInterface
(此调用类似于 BrowserInterfaceBroker::GetInterface
,不同之处在于它接受一个待处理的关联接收器[pending associated receiver],而不是一个待处理的接收器[pending receiver])。