WebExtensions 存储的工作原理

本文档描述了 storage.sync 部分的 WebExtensions 存储 API 的实现。该实现位于 toolkit/components/extensions/storage 文件夹

理想情况下,您应该已经了解 Rust 和 XPCOM - 请参阅此文档以了解更多详细信息

从非常高的层次来看,该系统看起来像

graph LR A[扩展 API] A --> B[存储 JS API] B --> C{魔法} C --> D[app-services 组件]

其中“魔法”实际上是最有趣的部分,也是本文档的主要重点。

注意:下面描述的通用机制也用于 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 调用将立即返回,稍后,在主线程上,我们将使用操作的结果调用回调参数。

因此,在幕后,发生的事情类似于

sequenceDiagram Extension->>ExtensionStorageSync: 调用 `set` 并给我一个 Promise ExtensionStorageSync->>xpcom: 调用 `set`,提供新数据和回调 ExtensionStorageSync-->>Extension: 您的 Promise xpcom->>xpcom: “桥接”中的线程魔法 xpcom-->>ExtensionStorageSync: 回调! ExtensionStorageSync-->>Extension: Promise 已解析

所以让我们进入桥接中的线程魔法吧!

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.dispatchPunt 执行工作。

  • 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。

最终结果

sequenceDiagram Extension->>ExtensionStorageSync: 调用 `set` 并给我一个 Promise ExtensionStorageSync->>xpcom - 桥接主线程: 调用 `set`,提供新数据和回调 ExtensionStorageSync-->>Extension: 您的 Promise xpcom - 桥接主线程->>moz_task 工作线程: Punt 此项 moz_task 工作线程->>webext-storage: 将此数据写入数据库 webext-storage->>webext-storage: 完成:结果/错误和观察者 webext-storage-->>moz_task 工作线程: ... moz_task 工作线程-->>xpcom - 桥接主线程: PuntResult xpcom - 桥接主线程-->>ExtensionStorageSync: 回调! ExtensionStorageSync-->>Extension: Promise 已解析