文章目录
这篇文档旨在作为Chromium开发者的Mojo入门。不需要事先了解Mojo。
我应该看这个吗?
如果你计划构建一个需要IPC的Chromium特性,并且你还没有使用Mojo,是的!旧IPC已被弃用。
为什么是Mojo?
TL;DR(Too Long; Didn’t Read):长期的目标是将Chromium重构为一系列更小的服务。
我们可以更聪明地处理:
- 我们提供或不提供哪些服务
- 我们如何隔离这些服务以提高安全性和稳定性
- 我们向用户提供哪些二进制特性
更健壮的消息传递层为许多有趣的可能性打开了大门;特别是,它允许我们集成大量的组件,而没有链接时的依赖关系,并且它打破了代码库中越来越多有趣的跨语言界限。
在过去的几年里,我们从使用Chromium IPC和维护Chromium依赖关系中学到了很多东西,我们觉得现在有一个重要的机会让开发人员的生活变得更轻松,并帮助他们更快地构建更多更好的功能,同时降低用户的成本。
Mojo概述
Mojo system API提供了一个小的 低级别IPC原语(low-level IPC primitives)套件: 消息管道, 数据管道,以及共享缓冲区(message pipes,data pipes, and shared buffers)。在这个API之上,我们构建了更高级别的bindings APIs,为编写C++、Java或JavaScript代码的用户简化消息传递。
☆本文主要关注将C++ bindings 用于消息管道,这可能是Chromium开发人员遇到的最常见的用法。
消息管道(Message Pipes)
消息管道是一种轻量级原语,用于相对较小的数据包的可靠双向传输。不足为奇的是,一个管道有两个端点,任一端点都可以通过另一个消息管道传输。
因为我们在浏览器进程和每个子进程之间启动一个原始的消息管道,这反过来意味着你可以创建一个新的管道,并最终将任何一端发送到任何进程,两端仍然能够无缝地和排他地相互对话。再见,routing IDs!
虽然消息管道可以携带任意的非结构化数据包,但我们通常将它们与生成的绑定结合使用,以确保在所有端点上都有一致的、定义良好的、版本化的消息结构。
Mojom
Mojom是Mojo接口的IDL(接口定义语言 Interface Definition Language)。在给定的.mojom
文件中,绑定生成器(bindings generator)输出当前支持的所有三种语言的绑定。
例如:
// src/components/frob/public/interfaces/frobinator.mojom
module frob.mojom;
interface Frobinator {
Frobinate();
};
将生成以下输出:
out/Debug/gen/components/frob/public/interfaces/frobinator.mojom.cc
out/Debug/gen/components/frob/public/interfaces/frobinator.mojom.h
out/Debug/gen/components/frob/public/interfaces/frobinator.mojom.js
out/Debug/gen/components/frob/public/interfaces/frobinator.mojom.srcjar
...
生成的代码隐藏了管道两端序列化和反序列化消息的所有细节。
C++头文件(frobinator.mojom.h
)为每个指定的mojom接口定义了一个抽象类。命名空间派生自module
name。
注意: chromium将组件
foo
的模块名 约定为foo.mojom
。这意味着组件foo
的所有mojom-generated C++ typenames将存在于foo::mojom
命名空间,以避免与non-generated typenames冲突。
在此示例中,生成的frob::mojom::Frobinator
具有单一的纯虚函数:
namespace frob {
namespace mojom {
class Frobinator {
public:
virtual void Frobinate() = 0;
};
} // namespace mojom
} // namespace frob
要创建一个Frobinator
服务,只需实现frob::mojom::Frobinator
并提供一种将管道绑定到它的方法。
绑定到管道(Binding to Pipes)
提示: 从名称,根本看不出谁是客户端,谁是服务端。那就带着答案去学习吧 ↓
InterfacePtr<T>
:客户端
InterfaceRequest<T>
:服务端相关
让我们来看一些示例代码:
// src/components/frob/frobinator_impl.cc
#include "components/frob/public/interfaces/frobinator.mojom.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/bindings/interface_request.h"
namespace frob {
class FrobinatorImpl : public mojom::Frobinator {
public:
FrobinatorImpl(mojom::FrobinatorRequest request)
: binding_(this, std::move(request)) {}
~FrobinatorImpl() override {}
// mojom::Frobinator:
void Frobinate() override { DLOG(INFO) << "I can't stop frobinating!"; }
private:
mojo::Binding<mojom::Frobinator> binding_;
};
} // namespace frob
首先要注意的是mojo::Binding<T>
将消息管道的一端 绑定到 服务的实现(implementation of a service)。这意味着 它在管道的那一端监视传入的消息;它知道如何为 interface T
解码消息,并将它们分派给T
implementation 的methods 。
mojom::FrobinatorRequest
是mojo::InterfaceRequest<mojom::Frobinator>
的类型别名,本质上是 强类型消息管道端点(a strongly-typed message pipe endpoint) 的语法糖。创建新消息管道的一种常见方式是通过调用 定义于interface_request.h
的 GetProxy
:
mojom::FrobinatorPtr proxy; //这是客户端
mojom::FrobinatorRequest request = mojo::GetProxy(&proxy); //这是服务端相关
这创建了一个新的消息管道,一端由proxy
拥有,另一端由request
拥有。它具有一个很好的特性,即可以将通用类型信息附加到管道的两端。It has the nice property of attaching common type information to each end of the pipe.
请注意,InterfaceRequest<T>
实际上并不执行任何操作。它只是在编译时定义一个管道端点(scopes a pipe endpoint),并将其与一个接口类型关联起来。因此,其他类型化的服务绑定原语,如mojo::Binding<T>
,在需要绑定到一个端点时,会使用这些对象作为输入。As such, other typed service binding primitives such as mojo::Binding take these objects as input when they need an endpoint to bind to.
InterfaceRequest<T> 是一个用于 Mojo IPC系统的类型。它的主要作用是在编译时定义一个管道端点(pipe endpoint),并将其与一个特定的接口类型(interface type)关联起来。这个对象本身并不执行任何操作,而是作为一个标识符或者占位符,用于后续的服务绑定。
当其他类型化的服务绑定原语,如 mojo::Binding<T>,需要绑定到一个端点时,它们会使用 InterfaceRequest<T> 对象作为输入。这样,mojo::Binding<T> 就可以知道它应该将消息传递给哪个具体的接口实现。
mojom::FrobinatorPtr
是mojo::InterfacePtr<mojom::Frobinator>
的类型别名。InterfacePtr<T>
不仅限定了一个消息管道端点(scopes a message pipe endpoint),而且还在内部实现了类型 T 的每一个方法,这是通过序列化相应的消息并将其写入管道来完成的。
因此,我们可以将这个组合起来,通过管道与FrobinatorImpl进行通信:
frob:mojom::FrobinatorPtr frobinator;
frob::FrobinatorImpl impl(GetProxy(&frobinator));
// Tada!
frobinator->Frobinate();
在幕后,这个操作序列化了一个与Frobinate
请求相对应的消息,并将其写入管道的一端。最终(并且通常是很快之后),impl的内部mojo::Binding
将这条消息解码,并调用impl.Frobinate()
。
注意: 在这个例子中,服务和客户端位于同一个进程中,这样运行是没有问题的。如果它们位于不同的进程中(请参见下面的示例在Chromium中公开服务),对Frobinate()
的调用看起来会完全一样!
回应请求(Responding to Requests)
Chromium IPC 中一个常见的做法是 通过不透明标识符(如整数型请求ID)跟踪IPC请求,以便后续能通过另一方向上的 名义关联的消息 对特定请求进行响应。A common idiom in Chromium IPC is to keep track of IPC requests with some kind of opaque identifier (i.e. an integer request ID) so that you can later respond to a specific request using some nominally related message in the other direction.
// 此处插入一段mojo的语法介绍,因为接下来需要用到
interface Foo {
// A request which takes no arguments and expects no response.
MyMessage();
// A request which has some arguments and expects no response.
MyOtherMessage(string name, array<uint8> bytes);
// A request which expects a single-argument response.
MyMessageWithResponse(string command) => (bool success);
// A request which expects a response with multiple arguments.
MyMessageWithMoarResponse(string a, string b) => (int8 c, int8 d);
};
这是mojom接口定义的一部分。我们可以这样扩展我们的Frobinator
服务:
module frob.mojom;
interface Frobinator {
Frobinate();
GetFrobinationLevels() => (int min, int max);
};
并更新我们的实现:
class FrobinatorImpl : public mojom::Frobinator {
public:
// ...
// mojom::Frobinator:
void Frobinate() override { /* ... */ }
void GetFrobinationLevels(const GetFrobinationLevelsCallback& callback) {
callback.Run(1, 42);
}
};
当服务实现(service implementation)运行回调时,响应参数会被序列化并通过管道发送回去。另一端的代理知道如何读取这个响应,并且会转而将它分派到那一端的回调函数上。
void ShowLevels(int min, int max) {
DLOG(INFO) << "Frobinator min=" << min << " max=" << max;
}
// ...
mojom::FrobinatorPtr frobinator;
FrobinatorImpl impl(GetProxy(&frobinator));
frobinator->GetFrobinatorLevels(base::Bind(&ShowLevels));
这正是你所期望的。
在Chromium中公开服务(Exposing Services in Chromium)
在浏览器中暴露服务有多种方式。现在一种常见的方法是使用content::ServiceRegistry。这些服务注册通常成对出现,跨越进程边界,并提供基本的服务注册和连接接口。例如,每个RenderFrameHost都有一个ServiceRegistry,每个对应的RenderFrame也有一个。这些注册表是相互关联的。
ServiceRegistry是一个关键组件,用于在浏览器的不同界面(如渲染进程和浏览器进程)上暴露服务。具体来说:
定义:ServiceRegistry 是一组接口和机制,允许服务在进程边界上进行注册和连接。
功能:它提供基本的服务注册和连接接口,使得不同进程之间可以通过消息管道(message pipes)进行通信。
使用场景:每个 RenderFrameHost 和对应的 RenderFrame 都有一个 ServiceRegistry 实例,这些注册表是相互关联的,确保服务可以在渲染进程和浏览器进程之间无缝通信。
RenderFrameHost: 这是浏览器进程中的一个组件,负责管理和渲染网页的一个框架(frame)。每个 RenderFrameHost 都有一个 ServiceRegistry 实例。
RenderFrame: 这是渲染进程中的一个组件,对应于 RenderFrameHost,负责实际的网页渲染工作。每个 RenderFrame 也有一个 ServiceRegistry 实例。
大致意思是,你可以在注册表的本地端添加一个服务——它只是从接口名称到工厂函数的映射——或者你可以通过名称连接到远程端注册的服务。
注意: 在这个上下文中,“工厂函数”只是一个回调函数,它接收一个管道端点并对其执行某些操作。预期您要么将其绑定到某种服务实现上,要么将其关闭,从而有效地拒绝连接请求。
工厂函数的定义
类型:回调函数(callback)
参数:接收一个管道端点(pipe endpoint)
功能:对该管道端点执行某些操作
具体作用与应用场景
1.与服务实现的绑定:
工厂函数可以将接收到的管道端点绑定到一个具体的服务实现上。这意味着当有新的连接请求到达时,系统会通过这个工厂函数创建并启动相应的服务实例来处理该连接。
2.拒绝连接请求:
另一种处理方式是直接关闭这个管道端点,这样做可以有效地拒绝该连接请求,不进行任何后续操作。
在Mojo系统中的使用示例
参考文本中提到的Create函数就是一个典型的工厂函数:
static void Create(content::BrowserContext* context, mojom::FrobinatorRequest request) {
new FrobinatorImpl(context, std::move(request));
}
这个Create方法在被调用时会实例化一个新的FrobinatorImpl对象,并将传入的请求(包含管道端点信息)移动到新对象中。
这样一来,每当有一个新的连接到Frobinator服务的请求到来时,就会通过这个工厂函数创建一个新的服务实例来处理该请求。
总结
综上所述,在Mojo IPC框架及上述文档的语境下,“工厂函数”主要起到中介的作用:它接收来自其他进程的连接请求(表现为一个管道端点),然后决定是把这个请求转发给具体的服务实现进行处理,还是直接关闭连接以拒绝该请求。
这种设计模式使得服务的管理和扩展变得更加灵活和高效,符合Mojo追求简洁、可组合及模块化的整体思路。
我们可以构建一个简单的浏览器端FrobinatorImpl服务,该服务可以访问任何连接到它的 frame的BrowserContext
。
We can build a simple browser-side FrobinatorImpl service that has access to a BrowserContext for any frame which connects to it:
BrowserContext 是 Chromium 中的一个关键概念,表示浏览器上下文,通常与用户的会话、配置、缓存等信息相关联。每个 BrowserContext 可以有多个标签页(frames),并且共享一些资源和设置。在这里,FrobinatorImpl 服务能够访问与之连接的任何frame的 BrowserContext,这意味着它可以获取和使用这些帧的相关上下文信息。
#include "base/macros.h"
#include "components/frob/public/interfaces/frobinator.mojom.h"
#include "content/public/browser/browser_context.h"
#include "mojo/public/cpp/system/interface_request.h"
#include "mojo/public/cpp/system/strong_binding.h"
namespace frob {
class FrobinatorImpl : public mojom::Frobinator {
public:
FrobinatorImpl(content::BrowserContext* context,
mojom::FrobinatorRequest request)
: context_(context), binding_(this, std::move(request)) {}
~FrobinatorImpl() override {}
// A factory function to use in conjunction with ServiceRegistry.
static void Create(content::BrowserContext* context,
mojom::FrobinatorRequest request) {
// See comment below for why this doesn't leak.
new FrobinatorImpl(context, std::move(request));
}
private:
// mojom::Frobinator:
void Frobinate() override { /* ... */ }
content::BrowserContext* context_;
// A StrongBinding is just like a Binding, except that it takes ownership of
// its bound implementation and deletes itself (and the impl) if and when the
// bound pipe encounters an error or is closed on the other end.
mojo::StrongBinding<mojom::Frobinator> binding_;
DISALLOW_COPY_AND_ASSIGN(FrobinatorImpl);
};
} // namespace frob
现在,在浏览器的某个地方,我们将Frobinator服务注册到每个RenderFrameHost(这是一个流行的位置):
frame_host->GetServiceRegistry()->AddService<frob::mojom::Frobinator>(
base::Bind(
&frob::FrobinatorImpl::Create,
base::Unretained(frame_host->GetProcess()->GetBrowserContext())));
在渲染进程中,我们现在可以这样做:
mojom::FrobinatorPtr frobinator;
render_frame->GetServiceRegistry()->ConnectToRemoteService(
mojo::GetProxy(&frobinator));
// It's IPC!
frobinator->Frobinate();
现在在Chromium项目中有很多具体的Mojo使用示例。你可以浏览现有的mojom文件,看看它们的实现是如何构建和连接的。
Mojo in Blink
TODO
这是一项正在进行的工作。简而言之:我们也将很快开始使用Blink中的Mojo服务,以便平台层能够直接通过Mojo消费浏览器服务。那里的长期目标是消除content/renderer
。
This is a work in progress. TL;DR: We’ll also soon begin using Mojo services from Blink so that the platform layer can consume browser services directly via Mojo. The long-term goal there is to eliminate content/renderer
.