了解Mojo IDL
Mojo IDL(Interface Definition Language,接口定义语言)是用于定义Mojo接口的语言。开发者可以使用.mojom
文件来定义结构体(structs)、联合体(unions)、接口(interfaces)、常量(constants)和枚举(enums),并在构建时为支持的目标语言(如C++、JavaScript、Java)生成代码。
使用说明
-
定义模块和接口:在
.mojom
文件中,可以定义一个模块(module),并在其中声明接口(interface)。例如:module business.stuff; interface MoneyGenerator { GenerateMoney(); };
-
生成绑定代码:使用bindings_generator.py可以为定义的接口生成对应的绑定代码。
-
基本数据类型:Mojo支持多种基本数据类型,如bool、整型、浮点数、字符串、数组、映射(map)、句柄(handle)等。
-
非空(Non-Nullable)对象:在Mojo定义中,字段或参数值可以标记为可空(nullable)。如果一个字段或参数未标记为可空,但接收到的消息中其值为null,则该消息会失败验证。
-
枚举(Enums)和常量(Constants):可以定义枚举和常量,用于表示一组有限的值或固定的值。
-
属性(Attributes):可使用属性来修改定义的含义,如
[Sync]
、[MinVersion=N]
、[Stable]
、[Uuid=<UUID>]
等。 -
关联接口(Associated Interfaces):可以定义关联接口,以保证与其他接口的严格FIFO顺序。
设计细节
-
模块(Modules):通过在Mojom文件中指定模块,可以将所有定义的符号聚合在一个共同的Mojom命名空间中。
-
导入(Imports):如果Mojom引用了其他Mojom文件中的定义,则必须导入这些文件。
-
结构体(Structs)和联合体(Unions):结构体用于将相关字段组合在一起,而联合体则是一组字段,一次只能取其中一个字段的值。
-
接口(Interfaces):接口是一组参数化的请求消息的逻辑捆绑,每个请求消息都可以选择定义一个参数化的响应消息。
-
属性(Attributes):Mojom定义可以通过属性来修改其含义,例如
[Sync]
属性可以使接口方法能够同步等待响应。 -
关联接口(Associated Interfaces):关联接口在单个接口的消息管道上附加,使得可以保证与其他接口的严格FIFO顺序。
-
版本控制(Versioning):Mojom支持版本控制,可以在不影响向后兼容性的情况下扩展接口。结构体字段和接口方法可以使用
[MinVersion=N]
属性来标明它们是在哪个版本引入的。 -
文法参考(Grammar Reference):文档末尾提供了Mojom语言的BNF语法参考。
-
附加文档(Additional Documentation):还提供了关于消息格式和消息验证测试的额外文档链接。
Mojo IDL的基本数据类型
原始类型列表
Mojo IDL支持以下基本数据类型,它们可以组合成结构体或用作消息参数:
类型 | 描述 |
---|---|
bool | 布尔类型(true 或false ) |
int8 , uint8 | 有符号或无符号的8位整数 |
int16 , uint16 | 有符号或无符号的16位整数 |
int32 , uint32 | 有符号或无符号的32位整数 |
int64 , uint64 | 有符号或无符号的64位整数 |
float , double | 32位或64位浮点数 |
string | UTF-8编码的字符串 |
array<T> | 任意Mojom类型T的数组,如array<uint8> 或array<array<string>> |
array<T, N> | 固定长度数组,类型为T,长度为N(N必须是整数常量) |
map<S, T> | 映射,将S类型的值映射到T类型的值 |
handle | 通用的Mojo句柄,可以是任何类型的句柄 |
handle<message_pipe> | 消息管道句柄 |
handle<shared_buffer> | 共享缓冲区句柄 |
handle<data_pipe_producer> | 数据管道生产者句柄 |
handle<data_pipe_consumer> | 数据管道消费者句柄 |
handle<platform> | 本地平台/操作系统句柄 |
pending_remote<InterfaceType> | 用户定义的Mojom接口类型的强类型消息管道句柄 |
pending_receiver<InterfaceType> | 用户定义的Mojom接口类型的预期接收器 |
pending_associated_remote<InterfaceType> | 关联接口句柄 |
pending_associated_receiver<InterfaceType> | 预期的关联接收器 |
T? | 可选(nullable)值,除了基本数字类型和枚举,其他类型均可为空 |
属性介绍列表
属性用于修改Mojom定义的含义,以下是Mojo IDL支持的属性列表:
属性 | 描述 |
---|---|
[Sync] | 允许接口方法能够同步地等待响应 |
[NoInterrupt] | 在等待同步消息响应时,禁止被其他同步消息打断 |
[Default] | 为枚举或联合体指定默认值 |
[Extensible] | 允许枚举或联合体在未来扩展,不对接收到的新值进行验证 |
[Native] | 用于在C++绑定中与旧的IPC::ParamTraits 或IPC_STRUCT_TRAITS* 宏建立桥接 |
[MinVersion=N] | 指定字段、枚举值、接口方法或方法参数引入的版本号 |
[Stable] | 指定类型或接口定义在时间上是稳定的,适用于持久存储或版本偏差二进制之间的通信 |
[Uuid=<UUID>] | 与特定接口相关联的UUID,即使接口名称变化,UUID也应保持稳定 |
[EnableIf=value] | 根据GN文件中的enabled_features 列表条件性地启用定义 |
[EnableIfNot=value] | 根据GN文件中的enabled_features 列表条件性地禁用定义 |
[ServiceSandbox=value] | 在Chromium中指定实现接口的服务将在哪个沙盒中启动 |
[RequireContext=enum] | 标记只能传递给特权进程上下文的接口 |
[AllowedContext=enum] | 标记传递具有RequireContext 属性接口的方法 |
[RenamedFrom="OldName"] | 为重命名的版本化结构体指定原始名称 |
这些属性可能会影响生成的代码的行为或者是提供额外的编译时校验。例如,[Sync]
属性允许生成的绑定支持同步调用,而[MinVersion=N]
则用于版本控制,确保在不同版本的接口之间保持兼容性。其他属性如[Stable]
和[Uuid=<UUID>]
用于跨不同版本或不同二进制文件的接口使用。这些设计考虑到了接口的稳定性、安全性和跨平台的灵活性。
通过对Mojo IDL定义的了解,可以发现Mojo IDL与protobuffer有很大区别,更加加深理解为什么protobuffer无法在这里使用的原因了。
一个简单的例子
让我们通过一个简单的例子来说明Mojo IDL的使用。假设我们正在开发一个名为“文件存储服务”的应用程序,该服务允许客户端上传和下载文件。以下是我们设计的Mojo IDL接口:
// 定义模块命名空间
module file_storage.mojom;
// 使用枚举来表示可能的错误类型
enum Error {
OK,
FILE_NOT_FOUND,
PERMISSION_DENIED,
UNKNOWN
};
// 定义一个表示文件信息的结构体
struct FileInfo {
string name;
int64 size;
[MinVersion=1] string? mime_type; // 新增字段,表示文件的MIME类型,可空
};
// 定义一个文件存储接口
interface FileStorage {
// 上传文件的方法,接受一个文件名和字节数据,返回可能的错误
Upload(string file_name, array<uint8> data) => (Error error);
// 下载文件的方法,接受一个文件名,返回文件的信息和字节数据,以及可能的错误
Download(string file_name) => (FileInfo? info, array<uint8>? data, Error error);
// 新版本方法,获取服务的版本信息
[MinVersion=1]
GetVersion() => (string version);
};
在此例子中,我们定义了一个名为file_storage.mojom
的模块,其中包含了一个文件存储接口FileStorage
。这个接口包含两个方法Upload
和Download
,以及一个新版本中添加的GetVersion
方法。
Upload
方法接受文件名和文件数据,返回一个枚举类型的错误信息。Download
方法接受文件名,返回文件信息(FileInfo
结构体),文件数据以及错误信息。GetVersion
方法是在接口版本1中新增的,用于获取服务的版本信息。
我们需要使用Mojo绑定生成器来生成C++、JavaScript或Java的绑定代码。这些生成的代码将允许客户端和服务端通过Mojo IPC(Inter-Process Communication)机制进行通信。
设计细节
-
模块(Module):通过在
.mojom
文件中定义模块,我们可以将所有定义的符号聚集在一个共同的Mojom命名空间中。这样做的目的是为了避免命名冲突,并清晰地组织代码。 -
枚举(Enum):在这个例子中,
Error
枚举被用来表示不同的错误状态。枚举是一种类型安全的方式来表示一组有限的值,这在设计接口时非常有用,因为它可以清晰地表达函数返回的可能状态。 -
结构体(Struct):
FileInfo
结构体用于表示文件的信息。在第一版本的接口中可能没有考虑到文件类型(MIME类型),因此在后续版本中通过[MinVersion=1]
属性作为一个可选字段添加进来,这就展示了Mojo如何支持接口的向后兼容性扩展。 -
接口(Interface):
FileStorage
接口定义了服务的公共API。接口中的方法通过Mojo IPC进行通信。每个方法都可以定义输入参数和返回参数,这些参数可以是前面定义的基本类型、结构体或枚举等。 -
版本控制(Versioning):通过使用
[MinVersion]
属性,我们可以在不破坏现有客户端兼容性的情况下扩展接口。这样设计的原因是,随着软件的发展,接口可能需要添加新功能,但同时也需要保持与老版本客户端的兼容性。 -
属性(Attributes):在Mojo中,属性用于提供有关定义的额外信息。例如,
[MinVersion=1]
指出了GetVersion
方法是在接口版本1中引入的。这些属性在生成绑定代码时会被考虑进去,以确保正确的行为。
通过这个例子,我们可以看到Mojo为接口定义提供了强类型、版本控制和跨语言绑定的功能,这些都是为了支持跨进程通信的强大和灵活性。可以很好地支持在分布式系统或大型项目(如Chromium)中的模块化和可扩展性。
总结
经过前面两篇文章的粗略介绍,我们已经对Mojo框架有了一个总体的印象,如果对某项细节感兴趣,也可以有了着手的思路。
阅读Mojo框架对于阅读整个Chromium源码的价值,主要在于掌握Mojom的代码生成的约定规则。约定规则可以用这幅图来表示:
例子:
- 客户端: 通过 CompositorFrameSinkProxy 发送消息。
- 服务端: 通过 CompositorFrameSinkStub 接收消息,并通过CompositorFrameSinkStubDispatch分发到具体的CompositorFrameSink 实现。
- 服务端实现: 实现 CompositorFrameSink接口的具体业务逻辑。通常是CompositorFrameSinkImpl
对于跨语言的情况,在Chromium更为常见,我们从文件实体的角度通过下图来展示生成的文件关系:
- 客户端代码:通常包含了JavaScript绑定,可以是完整的或精简的(lite),或特定于Web UI的。
- 服务端代码:包含了服务端的头文件,可能包括内部使用的头文件、共享类型的定义、服务端特定的代码和实现。
- 公共代码:包含了可以由客户端和服务端共同使用的头文件,比如引入其他Mojom头文件的导入文件、转发声明和C++绑定特定的头文件等。
- 使用:客户端业务代码使用客户端生成的代码,服务端业务逻辑实现Mojom接口,并使用服务端生成的代码。
了解这些规则对阅读Chromium源码可以有很好的帮助。