WebExtensions 存储的工作原理¶
本文档描述了 storage.sync 部分的 WebExtensions 存储 API 的实现。该实现位于 toolkit/components/extensions/storage 文件夹
理想情况下,您应该已经了解 Rust 和 XPCOM - 请参阅此文档以了解更多详细信息
从非常高的层次来看,该系统看起来像
其中“魔法”实际上是最有趣的部分,也是本文档的主要重点。
注意:下面描述的通用机制也用于 app-services 团队的其他 Rust 组件 - 例如,“dogear” 使用类似的机制,以及同步引擎(但复杂度更高)来管理线程。不幸的是,在撰写本文时,没有代码共享,也不清楚我们如何共享,但这可能会随着更多 Rust 代码的加入而改变。
app-services 组件 位于 github 上。有一些文档描述了 如何更新/引入此(以及所有)外部 Rust 代码,您可能对此感兴趣。
为了设定场景,让我们首先看看公开给 WebExtensions 的部分;那里也有很多移动部件。
WebExtension API¶
WebExtension API 由附加组件团队拥有。此 API 的实现非常复杂,因为它涉及多个进程,但为了本文档的目的,我们可以将 WebExtension 存储 API 的入口点视为 parent/ext-storage.js
此入口点最终使用 ExtensionStorageSync JS 类 中的实现。此类/模块具有诸如从早期基于 Kinto 的后端迁移等复杂性,但重要的是,代码可以将回调 API 适配为基于 Promise 的 API。
API 概述¶
从高层次来看,此 API 非常简单 - 有用于“获取/设置/删除”扩展存储数据的方法。请注意,公开给附加组件的“外部”API 稍微更改了此“内部”API 的参数,因此有一个扩展 ID 参数,并且 JSON 数据已转换为字符串。API 的语义超出了本文档的范围,但已 在 MDN 上记录。
正如您将在这些文档中看到的,API 是基于 Promise 的,但 Rust 实现是完全同步的,并且 Rust 不了解 Javascript Promise - 因此,此系统将基于回调的 API 转换为基于 Promise 的 API。
XPCOM 作为 Rust 的接口¶
XPCOM 是一种旧的 Mozilla 技术,它使用 C++“虚函数表”来实现“接口”,这些接口在 IDL 文件中进行了描述。虽然这传统上用于连接 C++ 和 Javascript,但我们正在利用对 Rust 的现有支持。我们公开的接口在 mozIExtensionStorageArea.idl 中进行了描述
此 IDL 文件中主要的接口是 mozIExtensionStorageArea。此接口定义了功能 - 并且是同步到异步模型中的第一层。例如,此接口定义了以下方法
interface mozIExtensionStorageArea : nsISupports {
...
// Sets one or more key-value pairs specified in `json` for the
// `extensionId`...
void set(in AUTF8String extensionId,
in AUTF8String json,
in mozIExtensionStorageCallback callback);
正如您将注意到的,第 3 个参数是另一个接口 mozIExtensionStorageCallback,也在该 IDL 文件中定义。这是一个小的、通用的接口,定义为
interface mozIExtensionStorageCallback : nsISupports {
// Called when the operation completes. Operations that return a result,
// like `get`, will pass a `UTF8String` variant. Those that don't return
// anything, like `set` or `remove`, will pass a `null` variant.
void handleSuccess(in nsIVariant result);
// Called when the operation fails.
void handleError(in nsresult code, in AUTF8String message);
};
请注意,这会传递所有结果和错误,因此必须能够处理每种结果类型,对于某些 API 来说这可能存在问题 - 但我们非常幸运,此简单的 XPCOM 回调接口能够合理地表示 mozIExtensionStorageArea 接口中每个函数的返回类型。
(还有另一个接口 mozIExtensionStorageListener,它通常也由实际的回调来实现,以通知扩展有关更改的信息,但这超出了本文档的范围。)
请注意此处的线程模型是异步的 - set 调用将立即返回,稍后,在主线程上,我们将使用操作的结果调用回调参数。
因此,在幕后,发生的事情类似于
所以让我们进入桥接中的线程魔法吧!
webext_storage_bridge¶
webext_storage_bridge 是一个 Rust crate,顾名思义,它是此 Javascript/XPCOM 世界与实际的 webext-storage crate 之间的“桥梁”。
lib.rs¶
是入口点 - 它定义了 xpcom“工厂函数” - 一个 extern “C” 函数,由 xpcom 调用以使用现有的 gecko 支持创建实现 mozIExtensionStorageArea 的 Rust 对象。
area.rs¶
此模块定义了接口本身。例如,在该文件中,您将找到
impl StorageSyncArea {
...
xpcom_method!(
set => Set(
ext_id: *const ::nsstring::nsACString,
json: *const ::nsstring::nsACString,
callback: *const mozIExtensionStorageCallback
)
);
/// Sets one or more key-value pairs.
fn set(
&self,
ext_id: &nsACString,
json: &nsACString,
callback: &mozIExtensionStorageCallback,
) -> Result<()> {
self.dispatch(
Punt::Set {
ext_id: str::from_utf8(&*ext_id)?.into(),
value: serde_json::from_str(str::from_utf8(&*json)?)?,
},
callback,
)?;
Ok(())
}
这里值得注意的是
xpcom_method 是一个 Rust 宏,并且是 gecko 中已存在的 xpcom 集成的部分。它声明了 IDL 中描述的 xpcom 虚函数表方法。
set 函数是实现 - 它在主线程上执行字符串转换和 JSON 解析,然后通过提供的回调参数 self.dispatch 和 Punt 执行工作。
dispatch 方法分派到另一个线程,利用现有的树内 moz_task 支持,将 Punt 移到另一个线程,并在完成后进行回调。
Punt¶
Punt 是一个奇特的名字,与“桥梁”有些相关 - 它将事物传递来回。
它是在 punt.rs 中定义的一个相当简单的枚举。它实际上只是对我们公开的 API 的重述,适合跨线程移动。简而言之,Punt 在主线程上创建,然后发送到后台线程,在后台线程中,实际操作通过 PuntTask 运行并返回 PuntResult。
有一些舞蹈在进行,但最终结果是 inner_run() 在后台线程上执行 - 因此对于 Set
Punt::Set { ext_id, value } => {
PuntResult::with_change(&ext_id, self.store()?.get()?.set(&ext_id, value)?)?
}
在这里,self.store() 是围绕来自 app-services 的实际 Rust 实现的包装器,其中涉及各种初始化和互斥锁舞蹈 - 请参阅 store.rs。即,此函数正在调用我们的 Rust 实现并将结果存储在 PuntResult 中
PuntResult 对该文件是私有的,但它是一个简单的结构体,封装了函数的实际结果(以及要发送给观察者的更改集,但这超出了本文档的范围)。
最终,PuntResult 在调用完成后返回到主线程,并安排回调 JS 实现,这反过来又解析了在 ExtensionStorageSync.sys.mjs 中创建的 Promise。