IPDL:线程间和进程间消息传递

理念

IPDL,“线程间/进程间协议定义语言”,是 Mozilla 专用的语言,允许代码以标准化、高效、安全、可靠和平台无关的方式在系统线程或进程之间进行通信。IPDL 通信发生在称为 actor 对象之间。该架构的灵感来自 actor 模型

注意

IPDL actor 在一个重要方面与 actor 模型不同——所有 IPDL 通信 发生在父级及其唯一的子级之间。

构成父子对的 actor 称为 对等体。对等 actor 通过 端点 进行通信,端点是消息管道的一端。actor 明确绑定到其端点,而端点在其构建后不久就绑定到特定线程。actor 永远不会更改其端点,并且只能从/向该端点、在该线程上发送和接收预先声明的 消息。违反此规则会导致运行时错误。一个线程可以绑定到许多其他无关的 actor,但一个端点支持 顶级 actor 和它们 管理 的任何 actor(见下文)。

注意

更准确地说,端点可以绑定到任何 nsISerialEventTarget,它们本身与特定线程相关联。默认情况下,IPDL 将绑定到当前线程的“主”串行事件目标,如果存在,则使用 GetCurrentSerialEventTarget 检索。为了清楚起见,本文档经常将 actor 称为绑定到线程,尽管串行事件目标的更精确解释始终有效。

注意

在内部,我们使用 Chromium Mojo 库的“端口”组件来 多路复用 多个端点(因此,多个顶级 actor)。这意味着端点通过相同的原生管道进行通信,从而节省了有限的操作系统资源。这方面的含义将在 IPDL 最佳实践 中讨论。

父级和子级 actor 可以绑定到不同进程中的线程、同一进程中不同线程中的线程,甚至同一进程中同一线程中的线程。最后一个选项似乎不合理,但 actor 非常灵活,并且它们的布局可以在运行时建立,因此这在理论上可能作为运行时选择的的结果出现。一个大型示例是 PCompositorBridge actor,在不同的情况下,它连接主进程和 GPU 进程中的端点(用于 Windows 上的 UI 渲染)、内容进程和 GPU 进程中的端点(用于 Windows 上的内容渲染)、主进程和内容进程中的端点(用于 Mac 上的内容渲染,因为没有 GPU 进程),或者主进程中的线程之间(用于 Mac 上的 UI 渲染)。在大多数情况下,这不需要复杂或冗余的编码;它只需要在运行时明智地绑定端点。中的示例 连接其他进程 显示了一种可以执行此操作的方法。它还表明,如果没有对端点配置的所有方式进行适当的纯文本记录,这很快就会导致难以维护的代码。一定要彻底记录您的端点绑定!!!

方法

actor 框架将安排任务在其关联的事件目标上运行,以响应它接收到的消息。消息在 IPDL 协议 文件中指定,并且响应处理程序任务由 C++ 方法按每条消息定义。由于 actor 仅成对通信,并且每个 actor 都绑定到一个线程,因此发送始终按顺序进行,永远不会并发(接收也是如此)。这意味着它可以并且确实保证 actor 将始终按其相关 actor 发送的顺序接收消息——并且此顺序是明确定义的,因为相关 actor 只能从一个线程发送。

警告

消息顺序保证有一些(罕见)例外。它们包括 同步嵌套 消息和带有 [Priority][Compress] 注释的消息。

IPDL 协议文件指定可以在父级和子级 actor 之间发送的消息,以及这些消息的方向和有效负载。消息看起来像函数调用,但从调用者的角度来看,它们可能在未来的任何时间开始和结束——它们是 异步 的,因此它们不会阻塞其发送 actor 或可能在 actor 线程的 MessageLoop 中运行的任何其他组件。

注意

并非所有 IPDL 消息都是异步的。同样,我们遇到了同步或 同步嵌套 消息的异常情况。强烈建议不要使用同步和嵌套消息,但可能并非总是可以避免。稍后将定义它们,以及在几乎所有情况下都应该使用的两种方法的更好替代方案。

协议文件在构建过程的早期阶段由 IPDL 编译器 编译。编译器生成反映协议的 C++ 代码。具体来说,它创建一个表示父级 actor 的 C++ 类和一个表示子级的 C++ 类。然后,生成的文 件会自动包含在 C++ 构建过程中。生成的类包含用于发送协议消息的公共方法,客户端代码将使用这些方法作为 IPC 通信的入口点。生成的 方法构建在我们位于 /ipc 中的 IPC 框架之上,该框架标准化了在所有支持的平台上安全可靠地使用套接字、管道、共享内存等。有关与构建过程集成的更多信息,请参阅 使用 IPDL 编译器

必须编写客户端代码来对这些生成的类进行子类化,以便为生成的响应每个消息的任务添加处理程序。它还必须添加例程 (ParamTraits),这些例程定义了消息有效负载中使用的任何类型(IPDL 系统尚不知道)的序列化和反序列化。原始类型和许多 Mozilla 类型具有预定义的 ParamTraits此处此处)。

注意

除其他事项外,使用生成代码的客户端代码必须在其 moz.build 文件中包含 chromium-config.mozbuild。有关所需构建更改的完整列表,请参阅 使用 IPDL 编译器

创建新 Actor 的步骤

  1. 确定您将在哪个文件夹中工作并创建

    1. 一个 IPDL 协议文件,以您的 actor 命名(例如 PMyActor.ipdl——actor 协议必须以 P 开头)。请参阅 协议语言

    2. actor 的父级和子级实现的正确命名源文件(例如 MyActorParent.hMyActorChild.h 以及可选的相邻 .cpp 文件)。请参阅 C++ 接口

    3. moz.build 文件进行 IPDL 特定的更新。请参阅 使用 IPDL 编译器

  2. 编写您的 actor 协议 (.ipdl) 文件

    1. 确定您是否需要顶级 actor 或受管 actor。请参阅 顶级 Actor

    2. 查找/编写您将在通信中使用的 IPDL 和 C++ 数据类型。为没有 ParamTraits 的 C++ 数据类型编写 ParamTraits。有关 IPDL 结构,请参阅 生成 IPDL 感知 C++ 数据类型:IPDL 结构体和联合体。有关 C++ 数据类型,请参阅 引用外部定义的数据类型:IPDL 包含ParamTraits

    3. 编写您的 actor 及其消息。请参阅 定义 Actor

  3. 编写 C++ 代码以在运行时创建和销毁 actor 实例。

  4. 为 actor 的消息编写处理程序。请参阅 C++ 中的 Actor 和消息

  5. 开始通过您的 actor 发送消息!同样,请参阅 C++ 中的 Actor 和消息

协议语言

本文档将遵循两个 actor 集成到 Firefox 中的过程——PMyManagerPMyManagedPMyManager 将管理 PMyManaged。一个好的起点是 IPDL actor 定义。这些文件以 actor 命名(例如 PMyManager.ipdl),并声明协议理解的消息。这些 actor 用于演示目的,并涉及大量功能。大多数 actor 将使用这些功能的一小部分。

include protocol PMyManaged;
include MyTypes;                          // for MyActorPair

using MyActorEnum from "mozilla/myns/MyActorUtils.h";
using mozilla::myns::MyData from "mozilla/MyDataTypes.h";
[MoveOnly] using class mozilla::myns::MyOtherData from "mozilla/MyDataTypes.h";
[RefCounted] using class mozilla::myns::MyThirdData from "mozilla/MyDataTypes.h";

namespace mozilla {
namespace myns {

[Comparable] union MyUnion {
    float; 
    MyOtherData;
};

[ChildProc=any]
sync protocol PMyManager {
  manages PMyManaged;
  parent:
    async __delete__(nsString aNote);
    sync SomeMsg(MyActorPair? aActors, MyData[] aMyData)
        returns (int32_t x, int32_t y, MyUnion aUnion);
    async PMyManaged();
  both:
    [Tainted] async AnotherMsg(MyActorEnum aEnum, int32_t aNumber)
        returns (MyOtherData aOtherData);
};

}    // namespace myns
}    // namespace mozilla
include protocol PMyManager;

namespace mozilla {
namespace myns {

protocol PMyManaged {
  manager PMyManager;
  child:
    async __delete__(Shmem aShmem);
};

}    // namespace myns
}    // namespace mozilla

这些文件引用了三个其他文件。MyTypes.ipdlh 是一个“IPDL 头文件”,可以包含到 .ipdl 文件中,就像它是在内联一样,除了它还需要包含它使用的任何外部 actor 和数据类型

include protocol PMyManaged;

struct MyActorPair {
    PMyManaged actor1;
    nullable PMyManaged actor2;
};

MyActorUtils.hMyDataTypes.h 是正常的 C++ 头文件,包含这些消息传递的数据类型的定义,以及序列化它们的说明。它们将在 C++ 接口 中介绍。

使用 IPDL 编译器

要构建 IPDL 文件,请在 moz.build 文件中列出它们(按字母顺序排序)。在此示例中,.ipdl.ipdlh 文件将与包含以下内容的 moz.build 并排

IPDL_SOURCES += [
    "MyTypes.ipdlh",
    "PMyManaged.ipdl",
    "PMyManager.ipdl",
]

UNIFIED_SOURCES += [
    "MyManagedChild.cpp",
    "MyManagedParent.cpp",
    "MyManagerChild.cpp",
    "MyManagerParent.cpp",
]

include("/ipc/chromium/chromium-config.mozbuild")

chromium-config.mozbuild 设置路径,以便生成的 IPDL 头文件位于正确的范围内。如果没有包含它,构建将失败,并在您的 actor 代码和一些内部 ipc 头文件中出现 #include 错误。例如

c:/mozilla-src/mozilla-unified/obj-64/dist/include\ipc/IPCMessageUtils.h(13,10): fatal error: 'build/build_config.h' file not found

`.ipdl` 文件在配置后构建的早期步骤中被编译成 C++ 文件。这些文件随后会在源代码和构建过程中被引用。编译器会从 `PMyManager.ipdl` 生成两个头文件,并添加到构建上下文中并全局导出:`mozilla/myns/PMyManagerParent.h` 和 `mozilla/myns/PMyManagerChild.h`,如下面的 命名空间 部分所述。这些文件包含 actor 的基类。它还会生成其他几个文件,包括 C++ 源文件和另一个头文件,这些文件会自动包含到构建中,不需要额外关注。

IPDL 需要 actor 的 C++ 定义。它们定义了响应消息时采取的操作——如果没有这些定义,它们将毫无价值。在讨论 C++ 中的 Actor 和消息 时,我们将对此进行更详细的介绍,但请注意,IPDL `编译器` 需要以 actor 命名的 C++ 头文件。示例中需要 `mozilla/myns/MyManagedChild.h`、`mozilla/myns/MyManagedParent.h`、`mozilla/myns/MyManagerChild.h` 和 `mozilla/myns/MyManagerParent.h`,如果没有这些文件,构建将无法进行。

引用外部定义的数据类型:IPDL 包含

让我们从 `PMyManager.ipdl` 开始。它首先包含它将从其他地方需要的类型

include protocol PMyManaged;
include MyTypes;                          // for MyActorPair

using MyActorEnum from "mozilla/myns/MyActorUtils.h";
using struct mozilla::myns::MyData from "mozilla/MyDataTypes.h";
[MoveOnly] using mozilla::myns::MyOtherData from "mozilla/MyDataTypes.h";
[RefCounted] using class mozilla::myns::MyThirdData from "mozilla/MyDataTypes.h";

第一行包含了 PMyManager 将管理的协议。该协议在它自己的 `.ipdl` 文件中定义。循环引用是预期的,并且不会造成任何问题。

第二行包含文件 `MyTypes.ipdlh`,该文件定义了诸如结构体和联合体之类的类型,但在 IPDL 中,这意味着它们具有超越类似 C++ 概念的行为。详细信息请参见 生成 IPDL 感知 C++ 数据类型:IPDL 结构体和联合体

最后几行包含来自 C++ 头文件的类型。此外,`[RefCounted]` 和 `[MoveOnly]` 属性告诉 IPDL 这些类型具有对操作很重要的特殊功能。这些是 IPDL 当前理解的数据类型属性

[RefCounted]

类型 `T` 是引用计数的(通过 `AddRef`/`Release`)。作为消息的参数或 IPDL 结构体/联合体中的类型,它被引用为 `RefPtr<T>`。

[MoveOnly]

类型 `T` 被视为不可复制的。当用作消息或 IPDL 结构体/联合体中的参数时,它作为一个右值 `T&&`。

最后,请注意 `using`、`using class` 和 `using struct` 都是有效的语法。`class` 和 `struct` 关键字是可选的。

命名空间

从 IPDL 文件

namespace mozilla {
namespace myns {

    // ... data type and actor definitions ...

}    // namespace myns
}    // namespace mozilla

命名空间的工作方式与在 C++ 中类似。它们也模仿了符号,试图使其易于使用。当 IPDL actor 编译成 C++ actor 时,命名空间作用域会被保留。如前所述,当 C++ 类型包含到 IPDL 文件中时,情况也是如此。它们之间最主要的区别在于,IPDL 还使用命名空间来建立生成文件的路径。因此,该示例定义了 IPDL 数据类型 `mozilla::myns::MyUnion` 和 actor `mozilla::myns::PMyManagerParent` 和 `mozilla::myns::PMyManagerChild`,它们可以分别从 `mozilla/myns/PMyManagerParent.h`、`mozilla/myns/PMyManagerParent.h` 和 `mozilla/myns/PMyManagerChild.h` 包含。命名空间成为路径的一部分。

生成 IPDL 感知 C++ 数据类型:IPDL 结构体和联合体

PMyManager.ipdl` 和 `MyTypes.ipdlh` 定义

[Comparable] union MyUnion {
    float;
    MyOtherData;
};

struct MyActorPair {
    PMyManaged actor1;
    nullable PMyManaged actor2;
};

根据这些描述,IPDL 生成 C++ 类,这些类近似于 C++ 结构体和联合体的行为,但附带预定义的 `ParamTraits` 实现。这些对象也可以像往常一样在 IPDL 外部使用,尽管无法控制生成的代码意味着它们有时不适合用作纯数据。有关详细信息,请参见 ParamTraits

`[Comparable]` 属性告诉 IPDL 为新类型生成 `operator==` 和 `operator!=`。为了做到这一点,新类型内部的字段需要定义这两个运算符。

最后,`nullable` 关键字表示在序列化时,actor 可能为 null。它旨在帮助用户避免空对象解引用错误。它仅适用于 actor 类型,也可以附加到消息声明中的参数。

定义 Actor

任何 `.ipdl` 文件的真正意义在于每个文件都定义了一个 actor 协议。定义始终与 `.ipdl` 文件名匹配。重复 `PMyManager.ipdl` 中的一个

[ChildProc=Content]
sync protocol PMyManager {
    manages PMyManaged;

    async PMyManaged();
    // ... more message declarations ...
};

重要

IPDL 在内部 `始终` 使用某种形式的引用计数来确保它及其客户端永远不会访问另一个组件删除的 actor,但这在客户端代码不尊重引用计数时会变得脆弱,有时也会失败。例如,当 IPDL 检测到连接由于远程进程崩溃而断开时,删除 actor 会留下悬空指针,因此 IPDL `不能` 删除它。另一方面,在许多情况下,IPDL 是唯一持有某些 actor 的引用的实体(对于托管 actor 的一方来说,这很常见),因此 IPDL `必须` 删除它。如果所有这些对象都是引用计数的,那么这里就不会有任何复杂性。实际上,在没有非常有说服力的理由的情况下,不应批准使用 `[ManualDealloc]` 的新 actor。使用 `[ManualDealloc]` 的新 actor 很快可能会被禁止。

`sync` 关键字告诉 IPDL 该 actor 包含使用 `sync` 阻塞发送方线程的消息,因此发送线程会等待消息的响应。有关它和其他阻塞模式的含义,请参见 IPDL 消息。现在,只需知道这是一个冗余信息,其价值主要在于使其他开发人员能够轻松地知道此处定义了 `sync` 消息。此列表提供了消息 actor 阻塞策略选项的初步定义

async

Actor 只能包含异步消息。

sync

Actor 具有 `async` 功能并添加 `sync` 消息。`sync` 消息只能从子 actor 发送到父 actor。

除了这些协议阻塞策略之外,IPDL 还支持注释,这些注释指示 actor 具有可能以与其发送顺序不同的顺序接收的消息。这些排序尝试按“消息线程”顺序处理消息(例如在邮件列表中)。这些行为可能难以设计。不鼓励使用它们,但有时是必要的。我们将在 嵌套消息 中进一步讨论它们。

[NestedUpTo=inside_sync]

Actor 具有高优先级消息,可以在等待 `sync` 响应时处理。

[NestedUpTo=inside_cpow]

Actor 具有最高优先级消息,可以在等待 `sync` 响应时处理。

此外,顶级协议使用 `[ParentProc=*]` 和 `[ChildProc=*]` 属性注释了每一方应绑定到的进程。`[ParentProc]` 属性是可选的,默认为 `Parent` 进程。`[ChildProc]` 属性是必需的。有关可能的值,请参见 进程类型属性

`manages` 子句告诉 IPDL `PMyManager` 管理先前 `include` 的 `PMyManaged` actor。与任何托管协议一样,`PMyManaged.ipdl` 也必须包含 `PMyManager` 并声明 `PMyManaged` 由 `PMyManager` 管理。回顾代码

// PMyManaged.ipdl
include protocol PMyManager;
// ...

protocol PMyManaged {
  manager PMyManager;
  // ...
};

一个 actor 有一个 `manager`(例如 `PMyManaged`)或者它是一个顶级 actor(例如 `PMyManager`)。一个 actor 协议可以由多个 actor 类型管理。例如,`PMyManaged` 也可能由此处未显示的某些 `PMyOtherManager` 管理。在这种情况下,`manager` 以列表形式呈现,并以 `or` 分隔——例如 `manager PMyManager or PMyOtherManager`。当然,托管 actor 类型的**实例**只有一个管理器 actor(因此仅由其中一种类型的管理器管理)。托管 actor 实例的管理器始终是构造该托管 actor 的 actor。

最后,是消息声明 `async PMyManaged()`。此消息是 `MyManaged` actor 的构造函数;与 C++ 类不同,它位于 `MyManager` 中。每个管理器都需要公开构造函数来创建其托管类型。这些构造函数是创建托管 actor 的唯一方法。它们可以像普通消息一样接受参数并返回结果。IPDL 构造函数的实现将在 C++ 中的 Actor 生命周期 中讨论。

我们还没有讨论构造新的顶级 actor 的方法。这是一个更高级的主题,将在 顶级 Actor 中单独介绍。

声明 IPDL 消息

actor 定义的最后一部分是消息的声明

sync protocol PMyManager {
  // ...
  parent:
    async __delete__(nsString aNote);
    sync SomeMsg(MyActorPair? aActors, MyData[] aMyData)
        returns (int32_t x, int32_t y, MyUnion aUnion);
    async PMyManaged();
  both:
    [Tainted] async AnotherMsg(MyActorEnum aEnum, int32_t a number)
        returns (MyOtherData aOtherData);
};

消息按 `parent:`、`child:` 和 `both:` 分组。这些标签的工作方式与 C++ 中的 `public:` 和 `private:` 类似——这些描述符后面的消息仅按指定的方向发送/接收。

注意

为了便于记忆它们指示的方向,请记住在前面加上“to”。例如,`parent:` 在 `__delete__` 之前,这意味着 `__delete__` 从子进程**发送到**父进程,而 `both:` 表示 `AnotherMsg` 可以**发送到**任一端点。

IPDL 消息支持以下注释

[Compress]

指示此类型的重复消息将合并。

[Tainted]

在使用参数之前,需要对其进行验证。

[Priority=Foo]

运行 C++ 消息处理程序的 `MessageTask` 的优先级。`Foo` 是以下之一:`normal`、`input`、`vsync`、`mediumhigh` 或 `control`。请参见 `IPC::Message::PriorityValue` 枚举。

[Nested=inside_sync]

指示消息有时可以在同步消息等待响应时处理。

[Nested=inside_cpow]

指示消息有时可以在同步消息等待响应时处理。

[LazySend]

带有此注释的消息将被排队,以便在非 LazySend 消息之前或从直接任务中一起发送。

[Compress] 提供了防止使用大量消息进行垃圾邮件发送的粗略保护。当类型为 M 的消息被压缩时,actor 之间未处理消息的队列永远不会包含一个紧挨着另一个的 M;它们总是会被不同类型的消息隔开。这是通过在发送新消息会破坏规则时丢弃较旧的消息来实现的。这已被用于限制主进程和内容进程之间的指针事件。

[Compress=all] 类似,但无论消息在消息队列中是否相邻都适用。

[Tainted] 是一种 C++ 机制,旨在鼓励关注参数安全性。在您保证参数安全之前,不能使用其值。它们在C++ 中的 Actor 和消息中进行了讨论。

Nested 注释与紧随其后的消息的阻塞策略密切相关,并在定义 Actor中进行了简要讨论。有关详细信息,请参阅嵌套消息

[LazySend] 表示消息不需要立即发送,可以稍后从直接任务中发送。不支持直接任务分派的 worker 线程将忽略此属性。带有此注释的消息仍将按顺序与其他消息一起传递,这意味着如果发送了普通消息,则将首先发送任何排队的 [LazySend] 消息。该属性允许传输层将要一起发送的消息组合起来,从而潜在地减少 I/O 和接收线程的线程唤醒次数。

以下是可用阻塞策略的完整列表。它类似于定义 Actor中的列表。

async

Actor 只能包含异步消息。

sync

Actor 具有 `async` 功能并添加 `sync` 消息。`sync` 消息只能从子 actor 发送到父 actor。

策略定义了当 actor 发送特定类型的消息时是否会等待响应。一个 sync actor 会在发送 sync 消息后立即等待,阻塞其线程,直到收到响应。这是导致浏览器停顿的常见原因。很少需要消息是同步的。因此,新的 sync 消息需要获得 IPC 对等体的批准。IPDL 编译器将要求此类消息列在 sync-messages.ini 文件中。

只有子 actor 可以发送 sync 消息的概念是为了避免潜在的死锁而引入的。它依赖于这样的信念:同步消息的循环(死锁)是不可能的,因为它们都指向一个方向。这种情况不再存在,因为任何端点都可以是子端点或父端点,并且某些端点(如主进程)有时充当两者。这意味着应谨慎使用同步消息。

注意

同步消息单向流动的概念仍然是 IPDL 用于避免死锁的主要机制。新的 actor 应避免违反此规则,因为后果很严重(而且复杂)。除非有**极端**的特殊情况,否则不应批准违反这些规则的 actor。如果您认为需要这样做,请先与 Element 上的 IPC 团队联系(#ipc)。

一个 async actor 不会等待。 async 响应本质上等同于发送另一个 async 消息。它可以在处理接收到的消息时随时处理。 async 响应消息的价值在于其人体工程学——异步响应通常由 C++ lambda 函数处理,这些函数更像是延续而不是方法。这使得它们更容易编写和阅读。此外,它们允许响应返回消息失败,而如果我们期望发送一个新的异步消息并失败,则不会有这样的响应。

接下来是同步,即消息及其参数列表的名称。消息 __delete__ 看起来很奇怪——实际上,它会终止 actor 的连接。 它本身不会删除任何 actor 对象!它会切断 actor 及其管理的任何 actor在两个端点的连接。actor 在发送或接收 __delete__ 后将永远不会发送或接收任何消息。请注意,所有发送和接收都必须发生在任何 actor 树的特定worker线程上,以便发送/接收顺序定义明确。在 actor 处理 __delete__ 后发送的任何内容都将被忽略(发送返回错误,尚未接收的消息将无法传递)。换句话说,一些未来的操作可能会失败,但不会出现任何意外行为。

在我们的示例中,子 actor 可以通过向父 actor 发送 __delete__ 来断开连接。父 actor 可以采取的唯一断开连接的操作是失败,例如崩溃。这种单向控制既常见又理想。

PMyManaged() 是一个受管理的 actor 构造函数。请注意不对称性——actor 包含其受管理 actor 的构造函数,但包含其自身的析构函数。

消息的参数列表非常简单。参数可以是任何具有 C++ ParamTraits 特化并由指令导入的类型。也就是说,消息列表中有一些意外情况。

int32_t,…

包含标准的原始类型。有关列表,请参阅builtin.py。指针类型不出所料地是被禁止的。

?

在跟随类型 T 时,参数在 C++ 中被转换为 Maybe<T>

[]

在跟随类型 T 时,参数在 C++ 中被转换为 nsTArray<T>

最后,返回列表声明了作为类型化参数元组的响应中发送的信息。如前所述,即使是 async 消息也可以接收响应。 sync 消息将始终等待响应,但 async 消息除非有 returns 子句,否则不会收到响应。

这结束了我们对 IPDL 示例文件的介绍。下一章将讨论与 C++ 的连接;特别是消息在C++ 中的 Actor 和消息中进行了介绍。有关在设计 IPDL actor 方法时最佳实践的建议,请参阅IPDL 最佳实践

IPDL 语法快速参考

以下是为在 IPDL 文件中使用而引入的关键字和运算符列表。

include

包含 C++ 头文件(带引号的文件名)或 .ipdlh 文件(不带引号且没有文件后缀)。

using (class|struct) from

类似于 include,但仅导入特定的数据类型。

include protocol

包含另一个 actor 以用于管理语句、IPDL 数据类型或作为消息的参数。

[RefCounted]

指示导入的 C++ 数据类型是引用计数的。引用计数类型需要与非引用计数类型不同的 ParamTraits 接口。

[ManualDealloc]

指示 IPDL 接口使用旧版手动分配/释放接口,而不是现代引用计数。

[MoveOnly]

[MoveOnly]

指示不应复制导入的 C++ 数据类型。IPDL 代码将改为移动它。

namespace

指定 IPDL 生成的代码的命名空间。

union

IPDL 联合定义。

struct

IPDL 结构定义。

[Comparable]

指示 IPDL 应为给定的 IPDL 结构/联合生成 operator==operator!=

nullable

指示在 IPDL 类型中,actor 引用在通过 IPC 发送时可能是 null。

protocol

IPDL 协议(actor)定义。

sync/async

[NestedUpTo=inside_sync]

在两种情况下使用这些关键字:(1) 指示消息在等待结果时是否阻塞;(2) 因为包含 sync 消息的 actor 本身必须标记为 sync

[NestedUpTo=inside_cpow]

[Nested=inside_sync]

[Nested=inside_sync]

指示 actor 除了普通消息之外还包含 [Nested=inside_sync] 消息。

[Nested=inside_cpow]

[Nested=inside_cpow]

指示 actor 除了普通消息之外还包含 [Nested=inside_cpow] 消息。

[AllowParentSend]

指示消息可以在等待较低优先级或消息线程中的同步响应时处理。

[ChildSendOnly]

指示消息可以在等待较低优先级或消息线程中的同步响应时处理。父 actor 不能发送。

manager

在协议定义中使用,以指示此 actor 管理另一个 actor。

manages

在协议定义中使用,以指示此 actor 由另一个 actor 管理。

or

在具有多个潜在管理器 actor 的 manager 子句中使用。

parent: / child: / both:

int32_t,…

指示后续 actor 消息的方向。作为记忆它们指示方向的助记符,在它们前面加上“to”一词。

returns

定义消息的返回值。所有类型的消息,包括 async,都支持返回值。

?

__delete__

[]

一条特殊的指令,当发送时会销毁两个端点上的相关 actor。在销毁另一个端点上的 actor 之前,会调用 Recv__delete__ActorDestroy,以允许进行清理。

[Tainted]

包含标准的原始类型。

[Compress]

String

在 C++ 中转换为 nsString

[Maybe]

[Priority=Foo]

在 IPDL 数据结构或消息参数中跟随类型 T 时,参数在 C++ 中被转换为 Maybe<T>

[LazySend]

[Array]

在 IPDL 数据结构或消息参数中跟随类型 T 时,参数在 C++ 中被转换为 nsTArray<T>

[Tainted]

用于指示消息的处理程序应接收其需要手动验证的参数。类型为 T 的参数在 C++ 中变为 Tainted<T>

[Compress]

指示此类型的重复消息将合并。当发送此类型的两条消息并最终在消息队列中并排出现时,将丢弃(不发送)较旧的消息。

[Compress=all]

类似于 [Compress],但无论消息在消息队列中是否相邻,都会丢弃较旧的消息。

[TaskPriority]

运行 C++ 消息处理程序的 MessageTask 的优先级。 Foo 是以下之一: normalinputvsyncmediumhighcontrol

指示 Actor 的子端预期绑定到的进程。在创建 Actor 时,将断言此值。顶级 Actor 必须指定此值。有关可能的值,请参阅进程类型属性

[ParentProc=...]

指示 Actor 的父端预期绑定到的进程。在创建 Actor 时,将断言此值。对于顶级 Actor,默认为Parent。有关可能的值,请参阅进程类型属性

进程类型属性

以下是协议上[ChildProc=...][ParentProc=...]属性的有效值,每个值对应于特定的进程类型。

Parent

主要的“父”或“主”进程。

Content

内容进程,例如用于托管网页、工作进程和扩展程序的进程。

IPDLUnitTest

IPDL gtest 中使用的仅限测试的进程。

GMPlugin

Gecko 媒体插件 (GMP) 进程。

GPU

GPU 进程。

VR

VR 进程。

RDD

远程数据解码器 (RDD) 进程。

Socket

Socket/网络进程。

ForkServer

Fork Server 进程。

Utility

实用程序进程。

这些属性还支持一些通配符值,当 Actor 可以绑定到多个进程时可以使用。如果您正在添加需要新通配符值的 Actor,请联系 IPC 团队,我们可以为您用例添加一个。它们如下所示。

any

任何进程。如果适用更具体的数值,应尽可能优先使用。

anychild

Parent之外的任何进程。通常用于基于每个进程绑定的实用程序 Actor,例如分析。

compositor

GPUParent进程。通常用于绑定到合成器线程的 Actor。

anydom

ParentContent进程。通常用于用于实现 DOM API 的 Actor。

请注意,这些断言不提供安全保证,主要用于审计以及作为 Actor 使用方式的文档。

C++ 接口

ParamTraits

在讨论 C++ 如何表示 Actor 和消息之前,我们先看看 IPDL 如何连接到导入的 C++ 数据类型。为了使任何 C++ 类型能够被 (反)序列化,它需要一个 ParamTraits C++ 类型类的实现。ParamTraits 是您的代码告诉 IPDL 要写入哪些字节来序列化您的对象以进行发送,以及如何将这些字节转换回另一端点的对象的方式。由于ParamTraits 需要 IPDL 代码能够访问,因此需要在 C++ 头文件中声明并由您的协议文件导入。否则会导致构建错误。

大多数基本类型和许多重要的 Mozilla 类型始终可用,无需包含。不完整的列表包括:C++ 原语、字符串(stdmozilla)、向量(stdmozilla)、RefPtr<T>(对于可序列化的T)、UniquePtr<T>nsCOMPtr<T>nsTArray<T>std::unordered_map<T>nsresult等。请参阅builtin.pyipc_message_utils.hIPCMessageUtilsSpecializations.h

ParamTraits 通常使用更基本类型的ParamTraits引导,直到它们达到基础(例如,上面列出的基本类型之一)。在最极端的情况下,ParamTraits 作者可能需要诉诸为类型设计二进制数据格式。这两种选择都可用。

我们还没有看到任何这些 C++ 代码。让我们看看从MyDataTypes.h中包含的数据类型。

// MyDataTypes.h
namespace mozilla::myns {
    struct MyData {
        nsCString s;
        uint8_t bytes[17];
        MyData();      // IPDL requires the default constructor to be public
    };

    struct MoveonlyData {
        MoveonlyData();
        MoveonlyData& operator=(const MoveonlyData&) = delete;

        MoveonlyData(MoveonlyData&& m);
        MoveonlyData& operator=(MoveonlyData&& m);
    };

    typedef MoveonlyData MyOtherData;

    class MyUnusedData {
    public:
        NS_INLINE_DECL_REFCOUNTING(MyUnusedData)
        int x;
    };
};

namespace IPC {
    // Basic type
    template<>
    struct ParamTraits<mozilla::myns::MyData> {
        typedef mozilla::myns::MyData paramType;
        static void Write(MessageWriter* m, const paramType& in);
        static bool Read(MessageReader* m, paramType* out);
    };

    // [MoveOnly] type
    template<>
    struct ParamTraits<mozilla::myns::MyOtherData> {
        typedef mozilla::myns::MyOtherData paramType;
        static void Write(MessageWriter* m, const paramType& in);
        static bool Read(MessageReader* m, paramType* out);
    };

    // [RefCounted] type
    template<>
    struct ParamTraits<mozilla::myns::MyUnusedData*> {
        typedef mozilla::myns::MyUnusedData paramType;
        static void Write(MessageWriter* m, paramType* in);
        static bool Read(MessageReader* m, RefPtr<paramType>* out);
    };
}

MyData 是一个结构体,MyOtherData 是一个 typedef。IPDL 对两者都可以正常处理。此外,MyOtherData 不可复制,与其 IPDL [MoveOnly] 注解相匹配。

ParamTraits 必须在IPC命名空间中定义。它们必须包含一个具有正确签名的Write方法,用于序列化,以及一个具有正确签名的Read方法,用于反序列化。

这里我们有三个声明示例:一个用于未注释类型的,一个用于[MoveOnly]的,一个用于[RefCounted]的。请注意[RefCounted]类型的方法签名中的差异。函数类型中可能不清楚的唯一区别是,在非引用计数的情况下,会向Read提供一个默认构造的对象,但在引用计数的情况下,Read会获得一个空的RefPtr<MyUnusedData>,并且只有在需要时才应分配MyUnusedData以返回。

这些是MyDataParamTraits方法的简单实现。

/* static */ void IPC::ParamTraits<MyData>::Write(MessageWriter* m, const paramType& in) {
    WriteParam(m, in.s);
    m->WriteBytes(in.bytes, sizeof(in.bytes));
}
/* static */ bool IPC::ParamTraits<MyData>::Read(MessageReader* m, paramType* out) {
    return ReadParam(m, &out->s) &&
           m->ReadBytesInto(out->bytes, sizeof(out->bytes));
}

WriteParamReadParam会调用您传递给它们的数据的ParamTraits,使用提供的对象的类型确定。WriteBytesReadBytesInto按预期处理原始的连续字节。MessageWriterMessageReader是 IPDL 内部对象,它们将传入/传出的消息作为字节流和流中的当前位置保存。客户端代码很少需要创建或操作这些对象。它们的高级用法超出了本文档的范围。

重要

Read中潜在的失败包括常见的 C++ 失败,例如内存不足情况,可以像往常一样处理。但Read也可能由于数据验证错误等原因而失败。ParamTraits读取被认为是不安全的数据。重要的是它们能够捕获损坏并正确处理它。从Read返回 false 通常会导致进程崩溃(主进程除外)。这是正确的行为,因为浏览器将处于意外状态,即使序列化失败不是恶意的(因为它无法处理消息)。其他响应,例如以崩溃断言失败,效果较差。IPDL 模糊测试依赖于ParamTraits不会因损坏错误而崩溃。有时,验证需要访问ParamTraits难以轻松访问的状态。(仅)在这些情况下,可以在消息处理程序中合理地进行验证。此类情况是Tainted注释的良好用途。有关详细信息,请参阅C++ 中的 Actor 和消息

注意

过去,如果您需要在序列化或反序列化期间使用 Actor 对象本身,则需要专门化mozilla::ipc::IPDLParamTraits<T>而不是IPC::ParamTraits<T>。如今,可以使用IPC::Message{Reader,Writer}::GetActor()IPC::ParamTraits中获取 Actor,因此此特性应用于所有新的序列化。

值得一提的一个特殊情况是枚举。枚举是安全漏洞的常见来源,因为代码在处理无效的枚举值时很少安全。由于通过 IPDL 消息获得的数据应被视为受污染的,因此枚举是主要关注点。ContiguousEnumSerializerContiguousEnumSerializerInclusive安全地为仅对连续值集有效的枚举(其中大多数)实现了ParamTraits。生成的ParamTraits确认枚举在有效范围内;否则Read将返回 false。例如,这是从MyActorUtils.h中包含的MyActorEnum

enum MyActorEnum { e1, e2, e3, e4, e5 };

template<>
struct ParamTraits<MyActorEnum>
  : public ContiguousEnumSerializerInclusive<MyActorEnum, MyActorEnum::e1, MyActorEnum::e5> {};

C++ 中的 IPDL 结构体和联合

IPDL 结构体和联合变为 C++ 类,提供相当容易理解的接口。回想一下IPDL 结构体和联合中的MyUnionMyActorPair

union MyUnion {
    float;
    MyOtherData;
};

struct MyActorPair {
    PMyManaged actor1;
    nullable PMyManaged actor2;
};

这些编译为:

class MyUnion {
    enum Type { Tfloat, TMyOtherData };
    Type type();
    MyUnion(float f);
    MyUnion(MyOtherData&& aOD);
    MyUnion& operator=(float f);
    MyUnion& operator=(MyOtherData&& aOD);
    operator float&();
    operator MyOtherData&();
};

class MyActorPair {
    MyActorPair(PMyManagedParent* actor1Parent, PMyManagedChild* actor1Child,
                PMyManagedParent* actor2Parent, PMyManagedChild* actor2Child);
    // Exactly one of { actor1Parent(), actor1Child() } must be non-null.
    PMyManagedParent*& actor1Parent();
    PMyManagedChild*& actor1Child();
    // As nullable, zero or one of { actor2Parent(), actor2Child() } will be non-null.
    PMyManagedParent*& actor2Parent();
    PMyManagedChild*& actor2Child();
}

生成的ParamTraits使用 IPDL 结构体或联合引用的类型的ParamTraits。字段尊重其类型的任何注解(请参阅IPDL 包含)。例如,[RefCounted]类型T会生成RefPtr<T>字段。

请注意,Actor 成员会导致父和子 Actor 类型的成员,如MyActorPair中所示。当 Actor 用于桥接进程时,在给定端点上只能使用其中一个。IPDL 确保当您发送一种类型(例如,PMyManagedChild)时,会接收另一种类型的相邻 Actor(PMyManagedParent)。这不仅适用于消息参数和 IPDL 结构体/联合,也适用于自定义ParamTraits实现。如果您Write一个PFooParent*,那么您必须Read一个PFooChild*。这在消息处理程序中不容易混淆,因为它们是命名为其操作端类的成员,但编译器无法强制执行。如果您正在编写MyManagerParent::RecvSomeMsg(Maybe<MyActorPair>&& aActors, nsTArray<MyData>&& aMyData),那么actor1Childactor2Child字段无效,因为子进程(通常)存在于另一个进程中。

C++ 中的 Actor 和消息

使用 IPDL 编译器中所述,IPDL 编译器为协议PMyManager生成两个头文件:PMyManagerParent.hPMyManagerChild.h,它们声明 Actor 的基类。在那里,我们讨论了这些头文件如何对包含chromium-config.mozbuild的 C++ 组件可见。反过来,我们始终需要定义两个文件来声明我们的 Actor 实现子类(MyManagerParent.hMyManagerChild.h)。IPDL 文件如下所示。

include protocol PMyManaged;
include MyTypes;                          // for MyActorPair

using MyActorEnum from "mozilla/myns/MyActorUtils.h";
using mozilla::myns::MyData from "mozilla/MyDataTypes.h";
[MoveOnly] using class mozilla::myns::MyOtherData from "mozilla/MyDataTypes.h";
[RefCounted] using class mozilla::myns::MyThirdData from "mozilla/MyDataTypes.h";

namespace mozilla {
namespace myns {

[Comparable] union MyUnion {
    float; 
    MyOtherData;
};

[ChildProc=any]
sync protocol PMyManager {
  manages PMyManaged;
  parent:
    async __delete__(nsString aNote);
    sync SomeMsg(MyActorPair? aActors, MyData[] aMyData)
        returns (int32_t x, int32_t y, MyUnion aUnion);
    async PMyManaged();
  both:
    [Tainted] async AnotherMsg(MyActorEnum aEnum, int32_t aNumber)
        returns (MyOtherData aOtherData);
};

}    // namespace myns
}    // namespace mozilla

因此MyManagerParent.h如下所示。

#include "PMyManagerParent.h"

namespace mozilla {
namespace myns {

class MyManagerParent : public PMyManagerParent {
    NS_INLINE_DECL_REFCOUNTING(MyManagerParent, override)
protected:
    IPCResult Recv__delete__(const nsString& aNote);
    IPCResult RecvSomeMsg(const Maybe<MyActorPair>& aActors, const nsTArray<MyData>& aMyData,
                          int32_t* x, int32_t* y, MyUnion* aUnion);
    IPCResult RecvAnotherMsg(const Tainted<MyActorEnum>& aEnum, const Tainted<int32_t>& a number,
                             AnotherMsgResolver&& aResolver);

    already_AddRefed<PMyManagerParent> AllocPMyManagedParent();
    IPCResult RecvPMyManagedConstructor(PMyManagedConstructor* aActor);

    // ... etc ...
};

} // namespace myns
} // namespace mozilla

可以发送到 Actor 的所有消息都必须由正确 Actor 子类中的Recv方法处理。成功时应返回IPC_OK(),如果发生错误(其中actorthisreason是人类可读的文本解释),则应返回IPC_FAIL(actor, reason),这应被视为处理消息失败。此类失败的处理方式特定于进程类型。

Recv方法由 IPDL 通过将一个任务排队以在绑定到的线程的MessageLoop上运行它们来调用。此线程是 Actor 的工作线程。托管 Actor 树中的所有 Actor 具有相同的工作线程——换句话说,Actor 从其管理器继承工作线程。顶级 Actor 在绑定时建立其工作线程。有关线程的更多信息,请参阅顶级 Actor。在大多数情况下,客户端代码永远不会在其工作线程之外与 IPDL Actor 互动。

接收到的参数成为栈变量,并使用std::move移动到Recv方法中。它们可以作为常量左值引用、右值引用或按值接收(类型允许的情况下)。[MoveOnly]类型不应作为常量左值接收。同步消息的返回值通过写入非const(指针)参数来赋值。异步消息的返回值处理方式不同——它们传递给一个解析器函数。在我们的示例中,AnotherMsgResolver将是一个std::function<>,而aResolver将通过传递对MyOtherData对象的引用来获得要返回的值。

MyManagerParent也能够发送返回值的异步消息:AnotherMsg。这是通过SendAnotherMsg完成的,该方法由IPDL在基类PMyManagerParent中自动定义。Send有两个签名,它们看起来像这样

// Return a Promise that IPDL will resolve with the response or reject.
RefPtr<MozPromise<MyOtherData, ResponseRejectReason, true>>
SendAnotherMsg(const MyActorEnum& aEnum, int32_t a number);

// Provide callbacks to process response / reject.  The callbacks are just
// std::functions.
void SendAnotherMsg(const MyActorEnum& aEnum, int32_t a number,
    ResolveCallback<MyOtherData>&& aResolve, RejectCallback&& aReject);

响应通常由在Send调用处定义的lambda函数处理,要么通过将它们附加到返回的promise上(例如,使用MozPromise::Then),要么通过将它们作为回调参数传递。有关其用法的更多信息,请参阅MozPromise的文档。当接收到有效回复或端点确定通信失败时,IPDL会解析或拒绝promise本身。ResponseRejectReason是IPDL提供的用于解释失败的枚举。

此外,AnotherMsg处理程序具有Tainted参数,这是协议文件中的[Tainted]注释导致的结果。回想一下,Tainted用于强制在消息处理程序中对参数进行显式验证,然后才能使用它们的值(而不是在ParamTraits中进行验证)。因此,它们可以访问消息处理程序的任何状态。它们的API以及用于验证它们的宏列表,详见此处

非用于返回值的异步消息的发送方法遵循更简单的形式;它们返回一个bool值指示成功或失败,并在非const参数中返回响应值,就像Recv方法一样。例如,PMyManagerChild定义了以下内容来发送同步消息SomeMsg

// generated in PMyManagerChild
bool SendSomeMsg(const Maybe<MyActorPair>& aActors, const nsTArray<MyData>& aMyData,
                 int32_t& x, int32_t& y, MyUnion& aUnion);

由于它是同步的,因此此方法在接收到响应或检测到错误之前不会返回到其调用方。

所有对Send方法的调用,就像所有消息处理程序Recv方法一样,都必须仅在actor的工作线程上调用。

构造函数,例如MyManaged的构造函数,显然是这些规则的例外。它们将在下一节中讨论。

C++中的Actor生命周期

MyManaged的构造函数消息在接收端变为两个方法。AllocPMyManagedParent构造托管actor,然后调用RecvPMyManagedConstructor来更新新的actor。下图显示了MyManagedactor对的构造

%%{init: {'sequence': {'boxMargin': 4, 'actorMargin': 10} }}%% sequenceDiagram participant d as Driver participant mgdc as MyManagedChild participant mgrc as MyManagerChild participant ipc as IPC Child/Parent participant mgrp as MyManagerParent participant mgdp as MyManagedParent d->>mgdc: new mgdc->>d: [mgd_child] d->>mgrc: SendPMyManagedConstructor<br/>[mgd_child, params] mgrc->>ipc: Form actor pair<br/>[mgd_child, params] par mgdc->>ipc: early PMyManaged messages and ipc->>mgrp: AllocPMyManagedParent<br/>[params] mgrp->>mgdp: new mgdp->>mgrp: [mgd_parent] ipc->>mgrp: RecvPMyManagedConstructor<br/>[mgd_parent, params] mgrp->>mgdp: initialization ipc->>mgdp: early PMyManaged messages end Note over mgdc,mgdp: 双向发送和接收现在将并发发生。

某个Driver对象正在创建MyManagedactor对。为了简洁起见,父进程和子进程中的内部IPC对象合并在一起。连接的par块并发运行。这表明在父进程仍在构造时可以安全地发送消息。

下一张图显示了MyManagedactor对的销毁,由对Send__delete__的调用发起。__delete__从子进程发送,因为这是唯一可以调用它的进程,如IPDL协议文件中声明的那样。

%%{init: {'sequence': {'boxMargin': 4, 'actorMargin': 10} }}%% sequenceDiagram participant d as Driver participant mgdc as MyManagedChild participant ipc as IPC Child/Parent participant mgdp as MyManagedParent d->>mgdc: Send__delete__ mgdc->>ipc: Disconnect<br/>actor pair par ipc->>mgdc: ActorDestroy ipc->>mgdc: Release and ipc->>mgdp: Recv__delete__ ipc->>mgdp: ActorDestroy ipc->>mgdp: Release end

由于子进程中某个Driver对象发送了__delete__,导致MyManagedactor对断开连接。

最后,让我们看一下对等方已丢失(例如,由于进程崩溃)的actor的行为。

%%{init: {'sequence': {'boxMargin': 4, 'actorMargin': 10} }}%% sequenceDiagram participant mgdc as MyManagedChild participant ipc as IPC Child/Parent participant mgdp as MyManagedParent Note over mgdc: CRASH!!! ipc->>ipc: Notice fatal error. ipc->>mgdp: ActorDestroy ipc->>mgdp: Release

MyManagedactor对的对等方由于致命错误而丢失时,该对断开连接。请注意,不会调用Recv__delete__

AllocRecv...Constructor方法在某种程度上与Recv__delete__ActorDestroy相对应,但有一些区别。首先,Alloc方法确实创建了actor,但ActorDestroy方法不会删除它。此外,ActorDestroySend__delete__期间或Recv__delete__之后在两个端点上运行。最后也是最重要的是,只有在接收到__delete__消息时才会调用Recv__delete__,但如果例如远程进程崩溃,则可能不会调用它。ActorDestroy另一方面,保证会为每个actor运行,除非进程非正常终止。因此,ActorDestroy是大多数actor关闭代码的正确位置。Recv__delete__很少有用,尽管有时它可以接收一些最终数据是有益的。

父类的相关部分如下所示

class MyManagerParent : public PMyManagerParent {
    already_AddRefed<PMyManagedParent> AllocPMyManagedParent();
    IPCResult RecvPMyManagedConstructor(PMyManagedParent* aActor);

    IPCResult Recv__delete__(const nsString& aNote);
    void ActorDestroy(ActorDestroyReason why);

    // ... etc ...
};

Alloc方法是为由IPDL接收Send消息构造的托管actor所需的。它不是调用Send的端点上的actor所需的。Recv...Constructor消息不是必需的——它有一个什么都不做的基本实现。

如果构造函数消息具有参数,则它们将发送到这两个方法。参数通过常量引用传递给Alloc方法,但移动到Recv方法中。它们的区别在于,可以从Recv方法发送消息,但在Alloc中,新创建的actor尚未投入运行。

构造函数的Send方法与其他Send方法也存在差异。在子actor中,我们的看起来像这样

IPCResult SendPMyManagedConstructor(PMyManagedChild* aActor);

该方法期望一个PMyManagedChild,调用方将已经构造了它,大概是用new(这就是为什么它不需要Alloc方法的原因)。一旦调用了Send...Constructor,actor就可以用于发送和接收消息。远程actor可能尚未创建并不重要,因为存在异步性。

actor的销毁与它们的构造一样不寻常。与构造不同,对于托管actor和顶级actor,它都是相同的。避免[ManualDealloc]actor消除了很多复杂性,但仍然需要理解一个过程。当发送__delete__消息时,actor的销毁开始。在PMyManager中,此消息从子进程到父进程声明。当方法返回时,调用Send__delete__的actor不再连接到任何内容。将来对Send的调用将返回错误,并且不会接收任何将来的消息。对于已运行Recv__delete__的actor,情况也是如此;它不再连接到另一个端点。

注意

由于Send__delete__可能会释放对自身的最后一个引用,因此它不能安全地作为类实例方法。相反,与其他Send方法不同,它是一个static类方法,并以actor作为参数

static IPCResult Send__delete__(PMyManagerChild* aToDelete);

此外,__delete__消息告诉IPDL断开给定actor及其所有托管actor的连接。因此,它实际上是在删除actor子树,尽管Recv__delete__仅针对发送到的actor调用。

在调用Send__delete__期间或调用Recv__delete__之后,actor的ActorDestroy方法会被调用。此方法使客户端代码有机会执行必须在所有可能的情况下发生的任何拆卸操作——预期和意外情况。这意味着当例如IPDL检测到另一个端点意外终止时,ActorDestroy也将被调用,因此它正在释放对actor的引用,或者因为祖先管理器(管理器或管理器的管理器……)接收到了__delete__。actor避免ActorDestroy的唯一方法是其进程首先崩溃。ActorDestroy总是在其actor断开连接后运行,因此尝试从中发送消息毫无意义。

为什么使用ActorDestroy而不是actor的析构函数?ActorDestroy提供了一个机会来清理仅用于通信的事物,因此不需要在actor的(引用计数)对象存在的时间内一直存在。例如,您可能有一些对共享内存(Shmems)的引用,这些引用现在不再有效。或者,actor现在可以释放仅用于处理消息的数据缓存。在ActorDestroy中处理与通信相关的对象更清晰,因为它们在那里变得无效,而不是让它们处于悬而未决的状态,直到析构函数运行。

将actor视为正常的引用计数对象,但IPDL在连接将存在或存在时会持有引用。一个常见的架构是IPDL持有actor的唯一引用。这在通过发送构造函数消息创建的actor中很常见,但这个想法适用于任何actor。当发送或接收__delete__消息时,该唯一引用就会被释放。

IPDL持有唯一引用的对偶是让客户端代码持有唯一引用。实现此目的的一种常见模式是覆盖actor的AddRef,使其仅在其计数降至一个引用时发送__delete__(如果actor.CanSend()为true,则该引用必须是IPDL)。更好的方法是为您的actor创建一个引用计数委托,它可以在其析构函数中发送__delete__。IPDL不保证它不会持有对您actor的多个引用。

顶级Actor

回想一下,顶级actor是没有管理器的actor。它们是每棵actor树的根。我们使用顶级actor的两种设置差别很大。第一种类型是创建和维护的方式类似于托管actor的顶级actor,但有一些重要的区别,我们将在本节中介绍。第二种类型的顶级actor是新进程中的第一个actor——这些actor通过不同的方式创建,关闭它们(通常)会终止进程。新进程示例演示了这两种情况。在添加新的进程类型中对其进行了详细讨论。

顶级Actor的价值

顶级actor比普通actor更难创建和销毁。它们过去比托管actor更重量级,但最近已大幅减少。

注意

之前,顶级 Actor 需要一个专用的消息通道,而消息通道是有限的操作系统资源。现在情况不再如此——消息通道现在由连接相同两个进程的 Actor 共享。这种消息交错可能会影响消息传递延迟,但分析表明,这种更改基本无关紧要。

那么为什么要使用新的顶级 Actor 呢?

  • 区分顶级 Actor 最显著的特性是能够将其绑定到任何他们选择的EventTarget。这意味着任何运行MessageLoop的线程都可以使用该循环的事件目标作为发送传入消息的位置。换句话说,Recv方法将在该消息循环中、在该线程上运行。IPDL 机制将异步地将消息分派到这些事件目标,这意味着多个线程可以同时处理传入消息。PBackground 方法源于一种希望使其更容易利用这一点的想法,尽管它有一些复杂性,在该部分有详细说明,这限制了它的价值。

  • 顶级 Actor 建议使用模块化。Actor 协议很难调试,就像跨越进程边界的任何事物一样。模块化可以为其他开发者提供有关他们在阅读 Actor 代码时需要知道什么(以及不需要知道什么)的线索。另一种选择是谚语中的垃圾桶类,它们对操作至关重要(因为它们做了很多事情),但也很难学习(因为它们做了很多事情)。

  • 无论 Actor 是否是进程中的第一个 Actor,顶级 Actor 都需要连接两个进程。如上所述,第一个 Actor 是通过特殊方式创建的,但其他 Actor 是通过消息创建的。在 Gecko 中,除了启动器和主进程之外,所有新的进程 X 都是与其第一个 Actor(位于 X 和主进程之间)一起创建的。要创建 X 与(例如)内容进程之间的连接,主进程必须将连接的Endpoints发送到 X 和内容进程,而内容进程又使用这些端点创建形成 Actor 对的新顶级 Actor。这在连接到其他进程中进行了详细讨论。

顶级 Actor 并非像预期的那样流畅,但相对于其功能而言,它们可能被低估了。在支持的情况下,PBackground有时是实现相同目标的更简单的替代方案。

从其他 Actor 创建顶级 Actor

创建新顶级 Actor 的最常见方法是创建一对连接的端点并将其中一个发送到另一个 Actor。这正是按字面意思进行的。例如

bool MyPreexistingActorParent::MakeMyActor() {
    Endpoint<PMyActorParent> parentEnd;
    Endpoint<PMyActorChild> childEnd;
    if (NS_WARN_IF(NS_FAILED(PMyActor::CreateEndpoints(&parentEnd, &childEnd)))) {
        // ... handle failure ...
        return false;
    }
    RefPtr<MyActorParent> parent = new MyActorParent;
    if (!parentEnd.Bind(parent)) {
        // ... handle failure ...
        delete parent;
        return false;
    }
    // Do this second so we skip child if parent failed to connect properly.
    if (!SendCreateMyActorChild(std::move(childEnd))) {
        // ... assume an IPDL error will destroy parent.  Handle failure beyond that ...
        return false;
    }
    return true;
}

这里MyPreexistingActorParent用于在连接父端后,将新顶级 Actor 的子端点发送到MyPreexistingActorChild。在此示例中,我们将新 Actor 绑定到我们正在运行的同一线程——这必须与MyPreexistingActorParent绑定的线程相同,因为我们正在从中发送CreateMyActorChild。我们本可以绑定到不同的线程。

此时,可以向父端发送消息。最终,它也将开始接收它们。

MyPreexistingActorChild仍然必须接收创建消息。该处理程序的代码非常相似

IPCResult MyPreexistingActorChild::RecvCreateMyActorChild(Endpoint<PMyActorChild>&& childEnd) {
    RefPtr<MyActorChild> child = new MyActorChild;
    if (!childEnd.Bind(child)) {
        // ... handle failure and return ok, assuming a related IPDL error will alert the other side to failure ...
        return IPC_OK();
    }
    return IPC_OK();
}

与父端一样,子端在Bind完成后即可发送。它很快将在其绑定的线程的事件目标上开始接收消息。

创建第一个顶级 Actor

进程中的第一个 Actor 是一个高级主题,在添加新进程的文档中进行了介绍。

PBackground

作为顶级 Actor 的便捷替代方案而开发的PBackground是一个 IPDL 协议,其被管理者在子进程中选择其工作线程,并在父进程中共享一个专门用于它们的线程。当某个 Actor(父或子)应该在不占用主线程的情况下运行时,将其设为PBackground的被管理者(又名后台 Actor)是一个选项。

警告

后台 Actor 可能难以正确使用,本节对此进行了详细说明。建议改用其他选项——即顶级 Actor。

后台 Actor 只能在有限的情况下使用

  • PBackground仅支持以下进程连接(其中顺序为父 <-> 子):主 <-> 主、主 <-> 内容、主 <-> 套接字和套接字 <-> 内容。

重要

套接字进程PBackground Actor 支持是在其他选项之后添加的。它有一些难以预料的粗糙边缘。将来,它们的支撑可能会分解成不同的 Actor 或完全删除。强烈建议在与套接字进程工作线程通信时,使用新的顶级 Actor而不是PBackground Actor。

  • 后台 Actor 的创建始终由子进程发起。当然,任何其他方式都可以向子进程发送创建请求。

  • 所有父后台 Actor 都在同一线程中运行。此线程专门用于充当父后台 Actor 的工作线程。虽然它没有其他功能,但它应该对所有连接的后台 Actor 保持响应。因此,在父后台 Actor 中执行长时间操作不是一个好主意。对于此类情况,请创建一个顶级 Actor 和一个父端的独立线程。

  • 后台 Actor 当前进行引用计数。必须仔细遵守 IPDL 的所有权,并且必须定义新 Actor 的(取消)分配器。有关详细信息,请参阅旧方法

下图显示了PBackground线程的假设布局,演示了一些进程类型限制。

flowchart LR subgraph content #1 direction TB c1tm[main] c1t1[worker #1] c1t2[worker #2] c1t3[worker #3] end subgraph content #2 direction TB c2tm[main] c2t1[worker #1] c2t2[worker #2] end subgraph socket direction TB stm[main] st1[background parent /\nworker #1] st2[worker #2] end subgraph main direction TB mtm[main] mt1[background parent] end %% PBackground connections c1tm --> mt1 c1t1 --> mt1 c1t2 --> mt1 c1t3 --> mt1 c1t3 --> st1 c2t1 --> st1 c2t1 --> mt1 c2t2 --> mt1 c2tm --> st1 stm --> mt1 st1 --> mt1 st2 --> mt1

假设的PBackground线程设置。箭头方向指示子到父PBackground被管理者关系。父端始终共享一个线程,并且可能连接到多个进程。子线程可以是任何线程,包括主线程。

创建后台 Actor 的方式与普通被管理者略有不同。新的被管理类型和构造函数仍然像普通被管理者一样添加到PBackground.ipdl中,但与创建子 Actor 并将其传递到SendFooConstructor调用不同,后台 Actor 将发送调用发出到BackgroundChild管理器,后者返回新的子级

// Bind our new PMyBackgroundActorChild to the current thread.
PBackgroundChild* bc = BackgroundChild::GetOrCreateForCurrentThread();
if (!bc) {
    return false;
}
PMyBackgroundActorChild* pmyBac = bac->SendMyBackgroundActor(constructorParameters);
if (!pmyBac) {
    return false;
}
auto myBac = static_cast<MyBackgroundActorChild*>(pmyBac);

注意

PBackgroundParent仍然需要一个RecvMyBackgroundActorConstructor处理程序,这与往常一样。这必须在ParentImpl类中完成。ParentImpl是用于PBackgroundParent实现的非标准名称。

总而言之,PBackground试图简化 Gecko 中的一个常见需求:运行在主进程和内容进程之间进行通信的任务,但避免与任一进程的主线程过多交互。不幸的是,它可能难以正确使用,并且错过了 IPDL 的一些有利改进,例如引用计数。虽然顶级 Actor 始终是需要大量资源的独立作业的完整选项,但PBackground为某些情况提供了一种折衷方案。

IPDL 最佳实践

IPC 性能受许多因素影响。其中许多因素是我们无法控制的,例如系统线程调度程序对延迟的影响,或者内部传输需要多条线路才能出于安全原因的消息。另一方面,有些事情我们可以也应该控制

  • 消息由于多种原因而产生固有的性能开销:IPDL 内部线程延迟(例如 I/O 线程)、参数(反)序列化等。虽然通常不明显,但这种成本可能会累积。此外,每条消息都会生成相当数量的 C++ 代码。出于这些原因,明智的做法是尽可能减少发送的消息数量。这可以像合并始终连续的两个异步消息一样简单。或者它可能更复杂,例如通过合并其参数列表并将可能不需要的参数标记为可选来合并两个略有重叠的消息。走得太远很容易,但仔细的消息优化可以带来巨大的收益。

  • 即使是[moveonly]参数也会在某种意义上被“复制”,因为它们会被序列化。传输数据的管道大小有限,需要分配。因此,请了解传输的性能将与内容的大小成反比。过滤掉您不需要的数据。由于与 Linux 管道写入原子性相关的复杂原因,最好将消息大小保持在 4K 以下(包括少量消息元数据)。

  • 另一方面,IPDL 不允许非常大的消息,这会导致运行时错误。当前限制为 256M,但即使消息稍小,也经常会出现消息失败。

  • 消息的参数是 C++ 类型,因此在某种意义上可能非常复杂,因为它们通常表示对象的树(或图)。如果这棵树包含很多对象,并且每个对象都由ParamTraits序列化,那么我们会发现序列化正在分配和构造很多对象,这将给分配器带来压力并导致内存碎片。通过使用更大的对象或通过小心使用共享内存来共享此类数据来避免这种情况。

  • 与所有事物一样,并发对于 IPDL 的性能至关重要。对于 Actor 而言,这主要体现在绑定线程的选择上。虽然将被管理的 Actor 添加到现有的 Actor 树中可能是一个快速的实现,但这个新 Actor 将绑定到与旧 Actor 相同的线程。这种争用可能是不可取的。其他时候,它可能是必要的,因为消息处理程序可能需要使用不是线程安全的或需要保证两个 Actor 的消息按顺序接收的数据。提前计划您的 Actor 层次结构及其线程模型。认识到何时最好使用新的顶级 Actor 或PBackground被管理者来促进同时处理消息。

  • 请记住,延迟会减慢整个线程的速度,包括该线程上的任何其他 Actor/消息。如果您有一些需要很长时间才能处理但可以并发运行的消息,那么它们应该使用在单独线程上运行的 Actor。

  • 顶级 Actor 决定了其被管理者的许多属性。可能最重要的属性是 Actor 的进程布局(包括哪个进程是“父”进程,哪个进程是“子”进程)和线程。每个顶级 Actor 应该清楚地记录这一点,理想情况下在它们的 .ipdl 文件中。

旧方法

待办事项

恐惧、不确定和怀疑

待办事项

其余部分

嵌套消息

嵌套消息注解指示消息的嵌套类型。它们尝试按照“对话线程”的嵌套顺序处理消息,例如在邮件列表客户端中找到的顺序。这是一个高级概念,应被视为不鼓励使用的、遗留的功能。从本质上讲,Nested 消息可能会导致其他 sync 消息破坏阻止其线程的策略——在同步消息等待响应时,允许接收嵌套消息。嵌套消息何时可以处理的规则有些复杂,但它们试图安全地允许 sync 消息 M 处理并响应某些特殊(嵌套)消息,这些消息可能是另一个端点完成处理 M 所必需的。在 MessageChannel 中有一条注释,其中包含有关如何做出处理嵌套消息决策的信息。对于同步嵌套消息,请注意,这意味着端点之间存在中继,这可能会极大地影响它们的吞吐量。

声明消息嵌套需要在 actor 和消息本身添加注解。嵌套注解在 定义 Actor声明 IPDL 消息 中列出。我们在这里重复它们。actor 注解指定 actor 中消息的最大优先级级别。IPDL 编译器会对其进行验证。注解如下:

[NestedUpTo=inside_sync]

指示 actor 包含优先级 [Nested=inside_sync] 或更低的级消息。

[NestedUpTo=inside_cpow]

指示 actor 包含优先级 [Nested=inside_cpow] 或更低的级消息。

注意

嵌套优先级的顺序为:(无嵌套优先级)< inside_sync < inside_cpow

消息注解如下:

[Nested=inside_sync]

指示 actor 除了普通消息之外还包含 [Nested=inside_sync] 消息。

[Nested=inside_cpow]

[Nested=inside_cpow]

注意

[Nested=inside_sync] 消息必须是同步的(由 IPDL 编译器强制执行),但 [Nested=inside_cpow] 可以是异步的。

嵌套消息显然只有在发送到执行同步等待的 actor 时才有趣。因此,我们将假设我们处于这种状态。假设 actorX 正在等待来自 actorY 的消息 m1 的同步回复,而 actorYactorX 发送消息 m2。我们在这里区分两种情况:(1)当处理 m1 时发送 m2(因此 m2RecvM1() 方法发送——这就是我们所说的“嵌套”的意思)和(2)当 m2m1 无关时。情况(2)很简单;只有当 priority(m2) > priority(m1) > (no priority) 且消息由父级接收,或者当 priority(m2) >= priority(m1) > (no priority) 且消息由子级接收时,才会在 m1 等待时分派 m2。情况(1)不太简单。

为了分析情况(1),我们再次区分导致嵌套情况的两种可能方式:(A)m1 由父级发送到子级,m2 由子级发送到父级,或者(B)方向相反。下表说明了所有情况下的发生情况

情况 (A):子级向正在等待同步响应的父级发送消息

同步 m1 类型(来自父级)

m2 类型(来自子级)

m2 处理或拒绝

同步(无优先级)

*

IPDL 编译器错误:父级无法发送同步(无优先级)

同步 inside_sync

异步(无优先级)

m2 延迟到 m1 完成之后
目前 m2 在同步等待期间处理(错误?)

同步 inside_sync

同步(无优先级)

m2 发送失败:优先级低于 m1
目前 m2 在同步等待期间处理(错误?)

同步 inside_sync

同步 inside_sync

m2m1 同步等待期间处理:相同的邮件线程和相同的优先级

同步 inside_sync

异步 inside_cpow

m2m1 同步等待期间处理:更高的优先级

同步 inside_sync

同步 inside_cpow

m2m1 同步等待期间处理:更高的优先级

同步 inside_cpow

*

IPDL 编译器错误:父级不能使用 inside_cpow 优先级

情况 (B):父级向正在等待同步响应的子级发送消息

同步 m1 类型(来自子级)

m2 类型(来自父级)

m2 处理或拒绝

*

异步(无优先级)

m2 延迟到 m1 完成之后

*

同步(无优先级)

IPDL 编译器错误:父级无法发送同步(无优先级)

同步(无优先级)

同步 inside_sync

m2 发送失败:无优先级同步消息在等待期间无法处理传入的消息

同步 inside_sync

同步 inside_sync

m2m1 同步等待期间处理:相同的邮件线程和相同的优先级

同步 inside_cpow

同步 inside_sync

m2 发送失败:优先级低于 m1

*

异步 inside_cpow

IPDL 编译器错误:父级不能使用 inside_cpow 优先级

*

同步 inside_cpow

IPDL 编译器错误:父级不能使用 inside_cpow 优先级

我们还没有看到 MessageChannel 中的注释 中的规则 #2 处于活动状态,但正如注释中提到的,它需要在父级和子级同时启动消息线程的情况下打破死锁。它通过在决定首先追求哪个消息线程时优先考虑父级发送的消息而不是子级发送的消息来实现这一点(并阻止另一个线程直到第一个线程完成)。由于这种区别完全基于线程计时,因此客户端代码只需要知道 IPDL 内部不会因为这种类型的竞争而死锁,并且这种保护仅限于单个 actor 树——只有在同一顶级 actor 下,父级/子级消息才会被良好排序,因此跨树的同时同步消息仍然可能导致死锁。

显然,需要对这些类型的协议进行严格控制,才能预测它们如何在自身以及应用程序的其他对象之间进行协调。控制流,以及状态,可能非常难以预测,并且同样难以维护。这是我们一再强调应尽可能避免消息优先级的主要原因之一。

消息日志记录

环境变量 MOZ_IPC_MESSAGE_LOG 控制 IPC 消息的日志记录。它记录有关消息传输和接收的详细信息。这不是由 MOZ_LOG 控制的——它是一个单独的系统。将此变量设置为 1 以记录所有 IPDL 消息的信息,或指定要记录的协议的逗号分隔列表。如果给出了 ChildParent 后缀,则仅记录给定侧面的活动;否则,记录双方活动。所有协议名称都必须包含 P 前缀。

例如

MOZ_IPC_MESSAGE_LOG="PMyManagerChild,PMyManaged"

这请求记录 PMyManager 的子侧活动,以及 PMyManaged 的父侧和子侧活动。

使用 IPDL 日志记录进行调试 提供了一个 IPDL 日志记录在跟踪错误时很有用的示例。