编程语言分为两类:具有垃圾收集功能的编程语言和需要手动管理内存的编程语言。前者有很多,例如 Kotlin、PHP 或 Java。后者的示例包括 C、C++ 或 Rust。一般来说,更高级别的编程语言更有可能将垃圾回收作为标准功能。这篇博文重点介绍了此类具有垃圾回收功能的编程语言,以及如何将它们编译为 WebAssembly (Wasm)。但首先,什么是垃圾收集(通常称为 GC)?
Browser Support
垃圾回收
简单来说,垃圾回收是指尝试回收程序已分配但不再引用的内存。此类内存称为垃圾。实现垃圾收集的策略有很多。其中一种是引用计数,其目的是统计内存中对象的引用数量。当不再有对某个对象的引用时,该对象可以标记为不再使用,从而准备好进行垃圾回收。PHP 的垃圾收集器使用引用计数,而使用 Xdebug 扩展程序的 xdebug_debug_zval()
函数可让您了解其内部运作方式。请考虑以下 PHP 程序。
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
该程序会将强制转换为字符串的随机数分配给一个名为 a
的新变量。然后,它会创建两个新变量 b
和 c
,并为其分配 a
的值。之后,它会将 b
重新分配给数字 42
,然后取消设置 c
。最后,它将 a
的值设置为 null
。使用 xdebug_debug_zval()
注释程序的每个步骤,您就可以看到垃圾收集器的引用计数器在工作。
<?php
$a= (string) rand();
$c = $b = $a;
xdebug_debug_zval('a');
$b = 42;
xdebug_debug_zval('a');
unset($c);
xdebug_debug_zval('a');
$a = null;
xdebug_debug_zval('a');
?>
上述示例将输出以下日志,您可以看到变量 a
的值在每个步骤后引用次数是如何减少的,这对于给定的代码序列来说是有意义的。(当然,您的随机数会有所不同。)
a:
(refcount=3, is_ref=0)string '419796578' (length=9)
a:
(refcount=2, is_ref=0)string '419796578' (length=9)
a:
(refcount=1, is_ref=0)string '419796578' (length=9)
a:
(refcount=0, is_ref=0)null
垃圾回收还存在其他挑战,例如检测循环,但对于本文而言,对引用计数有基本的了解就足够了。
编程语言以其他编程语言实现
这可能有点像盗梦空间,但编程语言是用其他编程语言实现的。例如,PHP 运行时主要在 C 中实现。您可以在 GitHub 上查看 PHP 源代码。PHP 的垃圾收集代码主要位于 zend_gc.c
文件中。大多数开发者都会通过操作系统的软件包管理器安装 PHP。不过,开发者也可以从源代码构建 PHP。例如,在 Linux 环境中,步骤 ./buildconf && ./configure && make
会为 Linux 运行时构建 PHP。但这同时也意味着,PHP 运行时可以针对其他运行时进行编译,例如,您可能已经猜到,Wasm。
将语言移植到 Wasm 运行时的传统方法
无论 PHP 在哪个平台上运行,PHP 脚本都会被编译为相同的字节码,并由 Zend Engine 运行。Zend Engine 是 PHP 脚本语言的编译器和运行时环境。它由 Zend 虚拟机 (VM) 组成,而 Zend 虚拟机又由 Zend 编译器和 Zend 执行器组成。以其他高级语言(如 C)实现的语言(如 PHP)通常具有针对特定架构(如 Intel 或 ARM)的优化,并且每种架构都需要不同的后端。在此背景下,Wasm 代表一种新的架构。如果虚拟机具有特定于架构的代码(例如即时 [JIT] 或预先 [AOT] 编译),那么开发者还需要为新架构实现 JIT/AOT 后端。这种方法非常合理,因为通常情况下,代码库的主要部分只需针对每种新架构重新编译即可。
鉴于 Wasm 的低级特性,很自然地会尝试在其中采用相同的方法:将主虚拟机代码及其解析器、库支持、垃圾收集和优化器重新编译为 Wasm,并根据需要为 Wasm 实现 JIT 或 AOT 后端。自 Wasm MVP 以来,这已成为可能,并且在许多情况下,这种方法在实践中效果良好。事实上,编译为 Wasm 的 PHP 为 WordPress Playground 提供支持。如需详细了解该项目,请参阅文章使用 WordPress Playground 和 WebAssembly 构建浏览器内 WordPress 体验。
不过,PHP Wasm 在浏览器中运行,并以宿主语言 JavaScript 为上下文。在 Chrome 中,JavaScript 和 Wasm 在 V8 中运行,V8 是 Google 的开源 JavaScript 引擎,可实现 ECMA-262 中指定的 ECMAScript。此外,V8 已经有垃圾收集器。这意味着,使用编译为 Wasm 的 PHP 等语言的开发者最终会向已经有垃圾回收器的浏览器提供移植语言 (PHP) 的垃圾回收器实现,这非常浪费资源。这就是 WasmGC 的用武之地。
旧方法(让 Wasm 模块在 Wasm 的线性内存之上构建自己的垃圾收集器)的另一个问题是,Wasm 自身的垃圾收集器与编译为 Wasm 的语言的顶层垃圾收集器之间没有互动,这往往会导致内存泄漏和低效的收集尝试等问题。让 Wasm 模块重用现有的内置 GC 可避免这些问题。
使用 WasmGC 将编程语言移植到新的运行时
WasmGC 是 WebAssembly 社区组的一项提案。当前的 Wasm MVP 实现只能处理线性内存中的数字(即整数和浮点数),而随着引用类型提案的发布,Wasm 还可以保存外部引用。WasmGC 现在添加了结构和数组堆类型,这意味着支持非线性内存分配。每个 WasmGC 对象都具有固定的类型和结构,这使得虚拟机可以轻松生成高效的代码来访问其字段,而不会像 JavaScript 等动态语言那样存在反优化的风险。此提案通过结构体和数组堆类型为 WebAssembly 添加了对高级别托管语言的高效支持,从而使以 Wasm 为目标的语言编译器能够与宿主虚拟机中的垃圾回收器集成。简单来说,这意味着使用 WasmGC 时,将编程语言移植到 Wasm 意味着编程语言的垃圾回收器不再需要成为移植的一部分,而是可以使用现有的垃圾回收器。
为了验证此改进的实际效果,Chrome 的 Wasm 团队已从 C、Rust 和 Java 编译了 Fannkuch 基准(在工作时分配数据结构)的版本。C 和 Rust 二进制文件的大小可能介于 6.1 K 到 9.6 K 之间,具体取决于各种编译器标志,而 Java 版本的大小仅为 2.3 K,小得多!C 和 Rust 不包含垃圾收集器,但它们仍然捆绑了 malloc/free
来管理内存,而 Java 在这里之所以更小,是因为它根本不需要捆绑任何内存管理代码。这只是一个具体的示例,但它表明 WasmGC 二进制文件有可能非常小,而这甚至是在针对大小进行任何重大优化之前。
了解 WasmGC 移植的编程语言的实际应用
Kotlin Wasm
得益于 WasmGC,Kotlin 是首批移植到 Wasm 的编程语言之一,以 Kotlin/Wasm 的形式呈现。以下清单中显示了由 Kotlin 团队提供的源代码演示。
import kotlinx.browser.document
import kotlinx.dom.appendText
import org.w3c.dom.HTMLDivElement
fun main() {
(document.getElementById("warning") as HTMLDivElement).style.display = "none"
document.body?.appendText("Hello, ${greet()}!")
}
fun greet() = "world"
现在,您可能想知道这样做的意义何在,因为上面的 Kotlin 代码基本上是由转换为 Kotlin 的 JavaScript OM API 组成。与 Compose Multiplatform 结合使用时,它的优势会更加明显,开发者可以基于他们可能已经为 Android Kotlin 应用创建的界面进行构建。您可以查看 Kotlin/Wasm 图片查看器,初步了解这方面的探索,该查看器同样由 Kotlin 团队提供。
Dart 和 Flutter
Google 的 Dart 和 Flutter 团队也在准备对 WasmGC 的支持。Dart 到 Wasm 的编译工作已接近完成,团队正在努力开发工具支持,以便交付编译为 WebAssembly 的 Flutter Web 应用。您可以在 Flutter 文档中了解该工作的当前状态。以下演示是 Flutter WasmGC 预览版。
详细了解 WasmGC
这篇博文只是浅尝辄止,主要简要介绍了 WasmGC。如需详细了解此功能,请访问以下链接:
致谢
本文由 Matthias Liedtke、Adam Klein、Joshua Bell、Alon Zakai、Jakob Kummerow、Clemens Backes、Emanuel Ziegler 和 Rachel Andrew 审阅。