使用 UniFFI 自动生成 Rust-JS 绑定

我在 Mozilla 的 Firefox 同步团队工作。四年前,我们写了一篇 博客文章,描述了我们在所有平台上发布跨平台 Rust 组件的策略。我们的愿景是整合 Firefox 桌面、Android 和 iOS 上存在的历史记录、登录和同步等功能的独立实现。

我们将用 Rust 编写的核心代码和针对每个平台的手写外语包装器来替换这些实现:桌面版使用 JavaScript,Android 版使用 Kotlin,iOS 版使用 Swift。

从那时起,我们吸取了一些经验教训,不得不修改我们的策略。事实证明,用多种语言创建手写包装器会消耗大量时间。包装器需要大量时间编写,但更重要的是,它们造成了许多严重的错误。

这些错误很难发现,也很难调试,而且经常导致崩溃。Rust 最大的优势之一是内存安全,但这些手写包装器抵消了很大一部分优势。

为了解决这个问题,我们开发了 UniFFI:一个用于自动生成外语绑定的 Rust 库。UniFFI 让我们能够快速安全地创建包装器,但有一个问题:UniFFI 支持 Kotlin 和 Swift,但不支持 JavaScript,而 JavaScript 为 Firefox 桌面前端提供支持。UniFFI 帮助我们为 Firefox Android 和 iOS 发布了共享组件,但桌面版仍然无法实现。

这种情况在 Firefox 105 中发生了变化,我们添加了通过 UniFFI 生成 JavaScript 绑定的支持,这使我们能够继续推进我们的单组件愿景。这个项目验证了一些从一开始就存在于 UniFFI 中的核心概念,但也要求我们以多种方式扩展 UniFFI。这篇博客文章将介绍我们在过程中遇到的问题以及我们的处理方法。

现有技术

这个项目在 Mozilla 至少之前已经尝试过一次。该团队能够实现部分功能,但有些部分仍然无法实现。我们意识到,之前的尝试所采用的通用方法可能不支持我们在组件中使用的 UniFFI 功能。

这是否意味着之前的工作是失败的?当然不是。该团队留下了一批宝贵的的设计文档、讨论和代码,我们确保认真学习并借鉴。特别地,有一个 ADR 讨论了不同的方法,我们研究了这些方法,以及 有效的 C++/WebIDL 代码,我们将其重新用于我们的项目。

调用 FFI 函数

UniFFI 绑定建立在使用 C ABI 的 FFI 层之上,我们称之为“支架”。然后,用户 API 在支架层之上定义,使用外语。这允许用户 API 支持在 C 中无法直接表达的功能,并且允许生成的 API 感觉自然且符合语言习惯。但是,JavaScript 使情况变得复杂,因为它不支持调用 C 函数。Firefox 中的特权代码可以使用 Mozilla 的 js-ctypes 库,但它已弃用。

之前的项目通过使用 C++ 调用支架函数来解决这个问题,然后利用 Firefox WebIDL 代码生成工具 来创建 JavaScript API。该代码生成工具非常棒,它允许我们使用 WebIDL 和 C++ 粘合代码的组合来定义用户 API。但是,它有限制,不支持所有 UniFFI 功能。

我们的团队决定使用相同的 WebIDL 代码生成工具,但只生成支架层,而不是整个用户 API。然后,我们使用 JavaScript 在其之上定义用户 API,就像其他语言一样。我们很有信心,代码生成工具将不再是限制因素,因为支架层被设计为极简且可以在 C 中表达。

异步函数

UniFFI 接口的线程模型不是非常灵活:所有函数和方法调用都是阻塞的。调用者有责任确保调用不会阻塞错误的线程。通常这意味着在线程池中执行 UniFFI 调用。

Firefox 前端 JavaScript 代码的线程模型同样不灵活:你决不能阻塞主线程。主 JavaScript 线程负责所有 UI 更新,阻塞它意味着浏览器无响应。此外,在 JavaScript 中启动另一个线程的唯一方法是使用 Web Workers,但目前前端代码未使用它们。

为了解决我们遇到的不可阻挡的力量与不可移动的物体的情况,我们简单地颠倒了 UniFFI 模型,使所有调用都成为异步的。这意味着所有函数都返回一个 promise,而不是直接返回它们的值。

“所有函数都是异步的”模型似乎是合理的,至少对于我们打算用 UniFFI 使用的前几个项目而言。但是,并非所有函数都需要是异步的 - 有些函数足够快,不会阻塞。最终,我们计划添加一种方法,让用户自定义哪些函数是阻塞的,哪些函数是异步的。这可能将在一些用于异步 UniFFI 的通用工作进行的同时发生,因为我们发现异步执行是许多使用 UniFFI 的组件遇到的一个问题。

效果如何?

自 Firefox 105 中加入 UniFFI 支持以来,我们已开始慢慢将一些使用 UniFFI 的 Rust 组件添加到 Firefox 中。在 Firefox 108 中,我们添加了 Rust 远程标签同步引擎,使其成为我们三个平台上 Firefox 共享的第一个组件。新的标签引擎使用 UniFFI 在桌面版上生成 JS 绑定,在 Android 版上生成 Kotlin 绑定,在 iOS 版上生成 Swift 绑定。

我们还一直在继续推进我们的移动端共享组件策略。Firefox iOS 在共享组件采用方面一直落后于 Android,但 Firefox iOS 116 版本将使用我们共享的同步管理器组件。这意味着这两个移动浏览器都将使用我们迄今为止编写的全部共享组件。

我们还使用 UniFFI 为 Glean(一个 Mozilla 遥测库)生成绑定,这有点不寻常。Glean 不会生成 JS 绑定;它只生成支架 API,最终会进入为 Firefox Android 提供支持的 GeckoView 库。然后,Firefox Android 可以通过生成的 Kotlin 绑定来使用 Glean,这些绑定链接到 Geckoview 中的支架。

如果你对这个项目或 UniFFI 感兴趣,请加入我们参加 #uniffi Mozilla Matrix 聊天室。

关于 Ben Dean-Kawamura

Ben Dean-Kawamura 的更多文章…