Mojo与Services入门指南

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();  // 创建新管道并传递接收端

在这个示例中,pingableRemote,而receiver是一个PendingReceiver,它是Receiver的前身,最终会变成一个ReceiverBindNewPipeAndPassReceiver是创建消息管道最常见的方式:它将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.ccPopulateFrameBinders函数中注册此方法,该方法将特定接口映射到各自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])。

相关推荐
T0uken8 小时前
【Python】UV:单脚本依赖管理
chrome·python·uv
powerfulzyh2 天前
Docker中运行的Chrome崩溃问题解决
chrome·docker·容器
代码的乐趣3 天前
支持selenium的chrome driver更新到136.0.7103.92
chrome·python·selenium
努力学习的小廉3 天前
深入了解linux系统—— 自定义shell
linux·运维·chrome
fenglllle4 天前
macOS 15.4.1 Chrome不能访问本地网络
chrome·macos
yousuotu4 天前
python如何提取Chrome中的保存的网站登录用户名密码?
java·chrome·python
颜淡慕潇4 天前
【Python】超全常用 conda 命令整理
chrome·python·conda
网硕互联的小客服5 天前
如何解决 Linux 系统文件描述符耗尽的问题
linux·运维·chrome
海尔辛5 天前
学习黑客正经版Bash 脚本入门教程
chrome·学习·bash
@PHARAOH5 天前
HOW - 在 Mac 上的 Chrome 浏览器中调试 Windows 场景下的前端页面
前端·chrome·macos