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])。

相关推荐
vx_33076231727 小时前
VOS3000内存满了怎么删除,录音格式如何转换呢
前端·chrome
hans7748829681 天前
【Chrome插件开发】某视频网站的m4s视频/音频下载方案,及其Chrome插件实现-v250415
前端·chrome·逆向
皓月盈江2 天前
使用谷歌浏览器自带功能将网页转换为PDF文件
chrome·pdf·html·网页转pdf·谷歌浏览器打印功能
守城小轩3 天前
Chromium 134 编译指南 Ubuntu篇:环境搭建与源码获取(一)
chrome·chrome devtools·指纹浏览器·浏览器开发·超级浏览器
難釋懷4 天前
bash的特性-bash中的引号
开发语言·chrome·bash
黑蛋同志6 天前
欧拉服务器操作系统部署deekseep(Ollama+DeekSeep+open WebUI)
运维·服务器·chrome
网硕互联的小客服6 天前
服务器风扇故障导致过热问题的解决方案
运维·服务器·chrome
爱上python的猴子6 天前
chrome中的copy xpath 与copy full xpath的区别
前端·chrome