在过去的几天里,我一直在与Firefox的用户聊天,试图将事实与谣言分开,了解2020年8月Mozilla裁员的后果。其中一个反复出现的话题是在迁移到Firefox Quantum的过程中移除了基于XUL的附加组件。我非常惊讶地看到,在这件事发生多年后,一些社区成员仍然对这个选择感到伤害。
然后,正如一些人在reddit上指出的那样,我意识到我们还没有花时间深入解释为什么我们别无选择,只能删除基于XUL的插件。
因此,如果您已经准备好深入了解插件和Gecko的一些内部结构,我想借此机会尝试向您提供更多细节。
在很长一段时间里,Firefox都是由一个非常小的核心组成的,在这个核心之上,一切都以扩展的形式实现。其中许多扩展是用C++编写的,其他扩展是用JavaScript编写的,很多扩展都涉及XUL接口语言和XBL绑定语言。多亏了一种名为XPCOM的技术,C++和JavaScript代码得以连接。每当扩展开发人员想要定制Firefox时,它都是简单而极其强大的,因为可以使用与支持Firefox的构建块完全相同的构建块来定制它。
这就是会话恢复(允许您将Firefox恢复到上次离开的位置,即使在崩溃的情况下)或查找栏在Firefox中首次实现的方式,以及其他功能。这是为火狐和雷鸟提供动力的技术。Songbird(开源iTunes的竞争对手)或Instantbird(聊天客户端)等工具就是这样开发出来的。这也是我很久以前定制Firefox成为电子书阅读器的方式。这就是成千上万的Firefox插件是如何开发出来的。
许多人将这种扩展机制称为“基于XUL的附加组件”,或者有时称为“基于XPCOM的附加组件”,我将在这篇博客文章中同时使用这两个术语,但我经常认为这是“混杂的扩展机制”,原因如下:
很快,插件开发人员意识到,他们所做的任何事情都可能破坏系统中的任何其他东西,包括其他插件和Firefox本身,而他们通常无法阻止这一点;
同样,Firefox开发人员所做的任何事情都可能破坏附加组件,而他们通常无法阻止这一点;
此外,火狐保持与Chrome竞争所需要的一些变化将立即打破大多数附加组件,从长远来看,可能是所有附加组件;
哦,顺便说一句,既然插件可以做任何事情,它们可以很容易地对操作系统做任何事情,从窃取密码到冒充你的银行。
稍后我将更详细地讨论这些问题。就目前而言,我只想说,火狐开发者很长一段时间(至少从2010年以来)就清楚地知道,这种情况是站不住脚的。因此,Mozilla提出了一个名为Firefox Jetpack的后备计划。
Firefox Jetpack是一种非常不同的Firefox扩展方式。那里干净多了。它终于有了权限机制(甚至在Firefox被称为Firefox之前就已经提出了这一点,通常被认为太难实现了)。开箱即用的插件不能破坏彼此,也不能破坏Firefox(我似乎记得通过利用观察器服务有时仍然是可能的,但您必须努力工作),它大量使用异步编程(这对于获得高性能的感觉很棒),而且由于它有一个有限的API,它可以进行测试,这意味着当Firefox开发人员破坏附加组件时,他们立即就知道了,并且可以修复破坏!这是向前迈出的几大步。这是以更有限的API为代价的,但在大多数情况下,这种权衡似乎是值得的。
不幸的是,Jetpack的设计与Firefox所需的一些重大更改之间出现了意想不到的不兼容。我不完全清楚这种不兼容性是什么,但这意味着我们不得不放弃Jetpack,取而代之的是,我们引入了WebExtensions。总体而言,WebExtensions与基于Jetpack的附加组件有着相似的目标,具有类似的受限API,而且额外的好处是它们可以在基于Chromium的浏览器和Firefox上运行。
如果您需要非常高级的API,那么从混杂的扩展机制切换到Jetpack或WebExtensions并不总是可能的,但对于大多数扩展来说,转换都很简单--根据我的个人经验,这甚至是令人愉快的。
Firefox及时为Firefox Quantum引入了WebExtensions,因为这正是杂乱无章的插件模型即将被打破的时候。
在这个阶段,我们已经完成了历史概述。我希望您已经准备好进行一个更技术性的深入研究,因为这就是我将如何向您确切解释在我们从混杂的扩展模型切换到WebExtensions时解决了哪些问题。
XPCOM,即跨平台组件对象模型,可能是Firefox最适合描述为核心的特性(对于深入了解Gecko的人,我将XPConnect和Cycle Collector作为XPCOM的一部分)与我们的JavaScript虚拟机SpiderMonkey一起使用。
XPCOM是一种允许您用两种语言编写代码并让对方调用对方的技术。Firefox的代码充满了C++调用JavaScript,JavaScript调用C++,很久以前,我们有项目混合添加了Python和.Net。这台机器非常复杂,因为语言不共享相同的定义(JavaScript中的64位整数是什么?C++中的JavaScript异常是什么?)。或者相同的内存模型(如何处理包含对C++可能希望从内存中删除的C++对象的引用的JavaScript对象?)。或者相同的并发模型(JavaScript工作者不共享任何东西,而C++线程共享所有东西)。
Gecko本身最初被设计为数以千计的XPCOM组件,每个组件都可以用C++或JavaScript实现,可以单独测试、插入、拔出或动态替换,并且它可以工作。此外,XPCOM体系结构提供了比当时更干净的C++编程,可以在几十个平台上工作,并让我们将用JavaScript编写代码的便利性与C++允许的原始速度结合起来。
要编写XPCOM组件,通常需要定义一个接口,然后用C++或JavaScript(现在是Rust,也许很快会用Wasm)编写实现。需要一些样板文件,但是,嘿,它起作用了。
当早期的Firefox开发人员决定向扩展开放该平台时,XPCOM立即被选为附加组件的基础技术。Firefox只需让插件作者在代码中的任何位置插入,他们就可以拥有强大的功能。
当您开发大型应用程序时,您需要进行更改,以修复错误或添加新功能,或者提高性能。在XPCOM世界中,这意味着更改XPCOM组件。有时将新功能添加到组件。有时要完全移除一个,因为这个设计已经被一个更好的设计所取代。
在基于XPCOM的扩展机制的第一个时代,这通常是被禁止的。如果有外接程序使用的XPCOM组件,则无法以不兼容的方式对其进行更改。这对插件开发人员来说是件好事,但很快就成了Firefox开发人员的噩梦。因为每个单独的更改都必须以向后兼容的方式进行,无论是外部(对于web开发人员)还是内部(对于插件开发人员)。这意味着每个XPCOM组件nsISomething很快都伴随着一个nsISomething2,它是更好的组件-两者都需要一起工作-有一种情况由Firefox开发人员处理起来更加复杂:基于XPCOM的附加组件可以取代任何现有的XPCOM组件。不用说,这是一种非常好的方式来打破Firefox,这让Firefox崩溃调查人员感到困惑。
这意味着开发变得越来越慢,因为我们需要检查每个新特性或每个改进,不仅要对照当前特性,而且还要对照过去/过时的特性,或者只是检查已经过时多年的旧工作方式。有一段时间,这项开发税是可以接受的。毕竟,Firefox的主要竞争对手是Internet Explorer,它有更糟糕的架构问题,而且显然有无限数量的开源贡献者提供了帮助。而且,网络的功能集要小得多,所以它仍然是有可能的。
然而,随着网络的发展,很明显,这些选择使得解决某些问题,特别是性能问题变得不可能。例如,在2008年左右的某个时候,Firefox开发人员意识到该平台有太多的XPCOM组件,这会严重影响性能,因为XPCOM组件会阻止JIT和C++编译器优化代码,并且需要过多的数据转换。这样就开始了deCOMtamation,即在没有XPCOM组件的情况下重写代码的性能关键部分。这意味着打破附加功能。
在基于XPCOM的扩展机制的第二个时代,Firefox开发人员被允许移除或重构XPCOM组件,前提是他们与插件开发人员取得联系,并与他们合作如何使修复其插件成为可能。这是一个畅通无阻的发展项目,但发展税不知何故设法增加到更高的水平,因为这有时意味着在我们甚至可以进行简单的改善之前,与外部开发商进行数周的集思广益。因此,也开始对附加组件开发税征收高额税,因为一些附加组件开发人员需要一次又一次地修改他们的附加组件。在某些情况下,Firefox开发人员和插件开发人员的关系非常好,这有时会导致插件开发人员设计Firefox中使用的API。在其他情况下,插件开发人员厌倦了这种维护负担,于是放弃了,有时会转向新生的Chrome生态系统。
大约在这个时候,Mozilla开始认真关注Chrome。Chrome一开始的设计指南与Firefox截然不同:
当时,Chrome并不在意占用太多内存或系统资源;
Chrome在启动时没有附加API,这意味着Chrome开发人员可以随意重构他们想要的任何东西,而不需要缴纳开发税;
当Chrome引入他们的扩展机制时,他们使用了一个合适的API,这个API通常可以在不考虑后端变化的情况下进行维护;
此外,虽然Chrome最初在几乎所有的基准测试中都比Firefox慢,但它依靠许多让人感觉更快的设计技巧-用户喜欢这一点。
多年来,Mozilla已经清楚地知道,Firefox需要切换到多进程设计。事实上,大约在Chrome1.0发布时,就有多进程Firefox的演示在流传。该项目被称为电解,简称E10S。我们稍后再来讨论这个问题。
当时,Mozilla决定暂停E10S,这肯定会比我们的许多用户使用更多的内存,而专注于一个名为Snappy的新项目(披露:我是Project Snappy的开发人员之一)。Snappy的目的是使用与Chrome相同的设计技巧,让Firefox感觉更快,希望不用重构所有东西。
火狐感觉比Chrome慢的原因是我们几乎所有的事情都是在一个线程上完成的。当Firefox将文件写入磁盘时,这会阻止视觉刷新,因此每秒的帧数会下降。当Firefox收集选项卡列表以保存它们以防崩溃时,这会阻止刷新,结果相同。当Firefox清理cookie时,这会阻止刷新等。
只要有可能,我们就把代码移到另一个线程,而不是在主线程上执行代码;
每当这是不可能的时候,我们就必须将处理分成小块,我们可以以某种方式保证这些小块可以在几毫秒内执行,然后手动交错这些块和主线程的其余执行。
这两个解决方案都帮助我们确保不会丢弃帧,并且当用户单击时我们可以立即响应。这意味着用户界面感觉很快。前者的优势在于它得益于操作系统的帮助,而后者则需要大量的测量和微调。这两种解决方案都很难实现,因为我们突然面临并发性问题,这是出了名的难以调试。这两种解决方案对插件开发人员来说也很困难,因为它们需要将整个功能从同步更改为异步,这通常意味着需要从头开始重新编写整个插件。
对我来说,这是回忆的时间,因为这是伊拉克利、保罗和我(我是不是忘了某个人?)。在Firefox代码库中引入了Promise和现在所知的异步函数。这些实验(绝不是围绕这个主题的唯一实验)作为稍后将这些功能引入网络的试验台。更直接的是,它们使编写异步代码变得容易得多,无论是对于Firefox开发人员还是插件开发人员都是如此。尽管有这些改进(以及其他还没有完全达到标准的改进),但是编写和调试异步代码仍然非常复杂。
因此,又一次,对插件开发人员征收了新的维护税,这一税很快就变得非常复杂。
当我们将代码移出主线程时,情况甚至更糟,因为它们通常被移到C++线程,而C++线程完全独立于JavaScript线程。XPCOM组件到处消失,失去了插件开发人员的可扩展性。
大概就在那个时候,Mozilla开始认真考虑寻找一种新的方法来编写附加组件,这将大大降低Firefox开发人员和附加组件开发人员的税收。当时的解决方案是Jetpack,它非常棒!有一段时间,两种扩展机制并存:更干净的Jetpack和较旧的混杂模型。
Snappy让我们走得相当远,但它永远不能解决Firefox的所有问题。
如上所述,Firefox开发人员很早就知道我们最终需要转向多进程模型。这对于安全和安保来说更好,这使得确保帧保持平稳更新变得更容易,这感觉很自然。到那时,Mozilla开发人员已经对多进程Firefox进行了一段时间的试验,但有两件事阻碍了Mozilla继续推进多进程Firefox(又名电解或E10S):
事实上,拥有多个进程几乎需要重写每个附加组件,而且有些进程根本无法移植。
随着RAM变得更便宜,并且我们优化了内存使用(项目内存收缩),问题1。逐渐不再是一个拦路虎。另一方面,问题2无法解决。
让我们考虑一个需要与页面内容交互的加载项的简单情况。例如,一个旨在增加对比度的附加组件。在E10S之前,这是一些存在于Firefox主窗口中的JavaScript代码,可以直接操作各个页面的DOM。这很容易写。使用E10S时,需要重写此附加组件才能跨进程工作。父进程只能通过交换消息与子进程通信,子进程可以随时停止,包括在处理消息时,由操作系统(在崩溃的情况下)或由最终用户(通过关闭选项卡)停止。可以将此加载项移植到E10S,因为处理网页内容的进程也运行JavaScript,而且E10S团队已经公开了用于加载项发送和接收消息以及在这些内容进程中加载自身的API。
然而,并不是所有的进程都能如此出色地使用附加组件。例如,专门用于保护Firefox免受Flash插件臭名昭著的崩溃的进程没有JavaScript虚拟机。专用于与GPU交互的进程没有JavaScript虚拟机。所有这些都是出于(良好的)性能和安全原因-而向这些进程添加JavaScript虚拟机会使它们变得更加复杂。
然而,对于Mozilla来说,我们别无选择。Mozilla没有提供电解液的每一天都是Chrome在架构、安全性和安全性方面更胜一筹的一天。不幸的是,Mozilla将电解推迟了数年,部分原因是误以为Firefox基准测试已经足够好,在很长一段时间内都无关紧要,部分原因是我们决定在Firefox之前先用FirefoxOS测试电解,但更主要的原因是我们不想失去所有这些附加组件。
最后,Mozilla决定引入WebExtensions,并最终作为Quantum项目的一部分跳跃到E10S。我们失去了多年的发展,再也无法恢复。
性能正在提高。需要重写附加组件。附加电源已经无可挽回地减少了。基于XPCOM的扩展机制基本上被放弃了(我们仍在内部使用它)。
今天,我们生活在一个后量子时代。只要有可能,就会在Rust中实现新功能。我不记得我们是否已经发布了在Wasm中实现的特性,但这肯定在路线图中。
开箱即用,Rust对XPCOM并不友好。Rust有一种不同的与其他语言交互的机制,该机制工作得非常好,但它本身并不会说XPCOM。在Rust中编写或使用XPCOM代码是可能的,并且正在逐步发展到无需付出太多努力就可以工作的阶段,但是对于Firefox中存在的大部分Rust代码来说,这实在是太复杂了,无法做到。因此,即使我们让基于XPCOM的扩展机制可用,Rust中实现的大多数功能对于附加组件也是不可访问的,除非我们显式发布了它们的API-可能是作为WebExtension API。
虽然我没有密切参与壁虎中的Wasm-in-Gecko,但我相信这个故事会有类似的结局。一开始没有XPCOM。后来,逐渐提供了某种XPCOM支持,但只有在明确决定这样做的情况下才会这样做。稍后可能作为WebExtension API公开。
另外,电解计划之后是裂变计划。这是对Firefox的又一次重大重构,通过进一步将Firefox拆分成多个进程,以提高安全性和安全性,并有望提高后期性能,从而朝着电解最初采取的方向迈进了一大步。虽然它不会直接影响XPCOM,但它确实意味着任何使用基于XPCOM的机制的附加组件都需要重新完全重写。
所有这些原因都证实,取消基于XPCOM的插件的选择充其量可以被改变,以延长这项技术的痛苦,但这一结论已成定局。
但是等等,这还不是全部!到目前为止,我们只提到了XPCOM,但是XPCOM只代表了Firefox附加组件背后技术的一半。
XML用户界面语言XUL是Firefox开创的革命之一。这是第一次,一种用于用户界面的声明性语言刚刚起作用。想象一下,放置按钮,用css设置它们的样式,用一点javascript将它们连接起来,添加一些元数据来实现可访问性、本地化、存储首选项…。还有一种叫做XUL的很好的机制,它可以很容易地将组件插入到现有界面中,例如扩展菜单或添加新按钮等,而不必更改其他文档。
在Mozilla存在的大部分时间里,Firefox的用户界面都是这样编写的-当然,我们的用户界面也是这样编写的。
.