在 Firefox(Android 版)堆栈中共享 Rust 库

Agi Sferro <[email protected]>

2021 年 3 月 20 日

问题

我们没有一个好的方案来集成 Rust 库,使其能够在 Gecko、GeckoView、AC 和 Fenix 中使用,并且能够让 Rust 代码直接调用其他 Rust 代码,避免使用 C FFI 层。

目标

  • 能够集成一个 Rust 库,该库可以从 Gecko、GeckoView、AC、Fenix 调用,包括拥有在整个堆栈中共享的单例式实例(每个进程一个)。

  • Rust 库应该能够直接调用和被其他 Rust 库或 Gecko 中的 Rust 代码调用(即,无需 C FFI 层)。

  • 构建时保证堆栈中的所有组件都编译相同的 Rust 库版本。

  • 轻松、快速和自动更新。应该能够在不到 24 小时内生成 Rust 库的 Chemspill 更新,并且只需很少的人工干预(除了安全检查/代码审查/质量保证)。

  • 对 Rust 库的非 Gecko 消费者提供支持至关重要。例如,提供一个不包含任何库的 Gecko 版本。

  • (可选)提供一种简单的方法来创建根据消费者需求打包的 Rust 库。

提议

  1. 将 libmegazord.so 重命名为 librustcomponents.so,以明确此构件的目的。

  2. 每个希望被调用或希望直接调用 Rust 代码的 Rust 库都将包含在 libxul.so 中(其中包含大部分 Gecko 本地代码),并在 mozilla-central 中引入。其中包括 Glean 和 Nimbus 等。

  3. libxul.so 将公开第 (2) 步中 mozilla-central 中引入的库所需的 Kotlin 包装器的必要 FFI 符号。

  4. 在 Gecko 的每次 nightly/beta/release 构建中,我们将生成一个(或可能是多个)额外的 librustcomponents.so 构件,并将作为 AAR 发布到 maven.mozilla.org。这还将发布 mozilla-central 中的所有引入库到 maven,这些库将依赖于作为此步骤的一部分生成的 librustcomponents.so。这样做将确保 libxul.so 和 librustcomponents.so 包含完全相同的代码,并且可以在依赖关系图中自由交换。

  5. 提供一个新的 GeckoView 构建,其 artifactId 为 geckoview-omni,它将依赖于所有 Rust 库。现有的 geckoview 将没有此类依赖项,并将保留供 GeckoView 的第三方用户使用。

  6. GeckoView 将依赖于第 (4) 步中构建的 .pom 文件中所有依赖于 librustcomponents.so 的库的 Kotlin 包装器。例如

<dependency>
  <groupId>org.mozilla.telemetry</groupId>
  <artifactId>glean</artifactId>
  <version>33.1.2</version>
  <scope>compile</scope>
</dependency>

它还将排除 org.mozilla.telemetry.glean 对 librustcomponents.so 的依赖项,因为本机代码现在作为第 (2) 步的一部分包含在 libxul.so 中。大概 Glean 将通过尝试 librustcomponents.so 或 libxul.so(或其他一些更好的方法,欢迎提供建议)来发现其本机代码所在的位置。

  1. Android 组件和 Fenix 将删除其对 Glean、Nimbus 和 GeckoView 提供的所有其他库的显式依赖项,而是使用 GeckoView 提供的依赖项(此步骤是可选的,请注意,任何版本冲突都将导致构建错误)。

优点

  • 我们免费获得了与 AC 的自动集成。当库的更新推送到 mozilla-central 时,将生成 GeckoView 的新 nightly 构建,AC 会自动使用该构建(并间接地集成到 Fenix 中)。

  • 发布基础设施到 maven 已经确定,我们可以重用 GeckoView 的现有流程来发布所有依赖项。

  • 如果消费者(例如 AC)使用不匹配的依赖项版本,将引发编译时错误。

  • 所有使用这种方式打包的 Rust 库的消费者都使用相同的版本(前提是他们保持最新版本)。

  • 非 Mozilla 消费者可以清楚地了解 GeckoView 中打包的内容,并且可以独立发现 Glean、Nimbus 等,因为我们在 pom 文件中定义了我们的依赖项。

  • Gecko 桌面版和 Gecko 移动版以相同的方式使用 Glean 和其他库,消除了不必要的差异。

值得注意

  • Fenix 的 beta/release 版本的升级将涉及更多检查,因为它们也会影响 Gecko。

缺点

  • 库需要在 mozilla-central 中引入。依赖项将遵循 Gecko 的发布周期,这可能不适合它们,因为某些依赖项实际上没有 nightly 和稳定版本之分。- 这种情况将来可能会改变,因为集成会越来越深入,并且库的更新会变得更加频繁/每次提交都会更新。

  • 本地测试 Rust 库中的更改需要重新构建整个 Gecko。这是将 Rust 库静态链接到 Gecko 的副作用。

  • Android 和 Gecko 都使用的所有 Rust 库都需要一起更新,我们不能在桌面/移动设备上使用不同的版本。虽然可以通过在库端提供灵活的依赖项来缓解此问题(例如,nimbus 不需要依赖于特定版本的 Glean,并且可以接受 Gecko 中的任何版本)。

  • 不在 mozilla-central 中的代码需要加倍工作才能进入产品 - 首先需要从本机存储库进行发布过程,然后是引入的 Phabricator 流程。

考虑的替代方案

遥测委托

GeckoView 提供了一个 Java 遥测委托接口,Glean 可以实现该接口以在 AC 层为消费者提供 Glean 功能。Glean 将为 Java 委托 API 提供一个 Rust 包装器,以透明地调用委托(当为移动设备构建时)或直接调用 Glean 实例(当为桌面构建时)。

缺点

  • 这需要 Glean 方面进行大量工作来构建和维护委托。

  • 很大一部分 Glean API 嵌入在 GeckoView API 中,没有直接依赖关系。

  • 我们预计遥测委托除了 Glean 本身之外不会有其他实现,尽管遥测委托具有明显的通用性。

  • Glean 和 GeckoView 工程师需要协调每次 API 更新,因为 Glean API 的更新可能会触发 GV API 的更新。

  • Gecko 桌面版和 Gecko 移动版以有意义的不同方式使用 Glean。

  • 没有解决依赖项问题:即使理论上这允许 Gecko 使用多个 Glean 版本,但实际上 GV 遥测委托将紧密跟踪 Glean,因此不可避免地需要非常具体的 Glean 版本才能工作。

优点

  • 明确的代码依赖关系,不明真相的观察者可以通过查看 API 来了解如何从 GeckoView 中提取遥测数据。

  • 没有严格的 Glean 版本要求,AC 可以(理论上)使用与 Gecko 不同的 Glean 版本构建,并且仍然可以工作。

我们为什么不选择它

Glean 方面涉及的大量持续维护工作远远超过了少量优势,即不将 AC 绑定到特定 Glean 版本。这会大大复杂化堆栈。

动态发现

Gecko 通过在 Glean 库上调用 dlsym 来发现它何时作为 Fenix(或其他一些基于 Gecko 的浏览器)的一部分加载。当发现成功并且 Glean 版本匹配时,Gecko 将直接使用 Fenix 提供的 Glean。

缺点

  • 非标准,非 Mozilla 应用程序不会期望它按预期工作。

  • “魔法”:除非你知道它在那里,否则无法知道是否正在发生发现(或 Gecko 提供了哪个版本的 Glean)。

  • 标准故障模式是在运行时,因为没有内置方法可以在构建时检查 Gecko 提供的版本是否与 Fenix 提供的版本相同。

  • 没有解决同步问题:Gecko 和 Fenix 必须使用相同的 Glean 版本才能使此方法工作。

  • Gecko 移动版在其使用 Glean 的方式上与桌面版存在有意义的差异,而没有内在原因。

优点

  • 此系统对使用应用程序透明,例如,Nimbus 可以照常使用 Glean,无需进行重大修改。

我们为什么不选择它

  • 此替代方案相较于本文档中概述的提议没有提供实质性益处,并且存在重大缺点,例如运行时故障案例和非标准链接过程。

混合动态发现

这是动态发现的一种变体,其中 Gecko 和 GeckoView 直接包含 Glean,而消费者从 Gecko 动态获取 Glean(即,他们使用 dlsym libxul.so)。

缺点

  • Glean 仍然需要为希望直接调用 Glean 的未包含在 Gecko 中的库(如 Nimbus)构建一个包装器。

优点

  • 从不明真相的观察者角度来看,对 Glean 的依赖关系是明确且清晰的。

  • 范围较小,只需要将 Glean 移到 mozilla-central。

我们为什么不选择它

相较于提议,优势不足,Glean 方面需要进行大量持续维护工作。

未解决的问题

  • iOS 如何使用 megazord?他们是否有类似 maven 的依赖项系统,我们可以用来发布 iOS megazord?

  • 我们如何在 about:license 中处理许可证?Application-services 有一个构建步骤,可以提取 Rust 依赖项并将其放入 pom 文件中。

  • 协调 a-c 中的重大变更的流程是什么?

  • 即使这不是 Rust 代码,是否也希望引入?

常见问题

  • **我们如何确保 GV/AC/Gecko 使用相同版本的本机库?** GeckoView 中的 pom 依赖项确保任何 GeckoView 消费者都依赖于给定库的相同版本,包括 AC 和 Fenix。

  • **megazord 的非 Gecko 消费者会发生什么情况?** 此计划对 megazord 的非 Gecko 消费者来说是透明的,因为他们仍然会通过 Glean/Nimbus 等中的 megazord 依赖项使用本机库。额外的好处是,如果消费者保持 megazord 依赖项的最新状态,他们将使用与 Gecko 使用的相同版本。

  • **发布 megazord 更新的流程是什么?** 当团队想要发布 megazord 更新时,需要将更新提交到 mozilla-central。下一个 nightly 循环将生成一个新的构建,生成 megazord 的更新版本。据我了解,当前的 megazord 版本是稳定的(并且没有 beta/nightly 循环),因此对于外部消费者来说,使用 nightly 构建可能就足够了,并且可以提供最快的更新周期。对于 Gecko 消费者来说,周期与 Firefox 桌面版相同(即,从提交到发布构建大约需要 6-8 周)。

  • **我们如何处理安全升级?** 如果你有一个 Rust 库的安全版本,你需要请求升级到 beta/release 分支(取决于影响),就像所有其他 Gecko 更改一样。这个过程本身可以加快速度,并在需要时快速完成(不到 24 小时)。我们一直使用此流程来处理所有 Gecko 更改,因此我不认为会出现特殊问题。

  • 面向对象编程(OOP)案例怎么办?例如,将 GeckoView 作为服务? 我们在邮件链中简要讨论过这个问题,有一些方法可以实现(例如,提供一个 IPC shim)。细节比较模糊,但由于我们目前没有对此类支持的迫切需求,知道它可以通过合理的工作量实现就足够了。

  • 将代码库引入 mozilla-central 似乎有些过分。 我同意。这是一个源于一些假设的不幸要求(这些假设是可以质疑的!我们选择不质疑)。

    • Gecko 希望将它用于 Rust 的任何内容都引入代码库。

    • 我们希望 Rust 直接调用 Rust(无需 C FFI 层)。

    • 我们希望添加新库成为一种轻松愉快的体验。

    由于上述原因,将代码库引入 mozilla-central 似乎是实现我们目标的最佳,如果不是唯一的方式。