No fun allowed
。如果这还不够,请编辑它以隐藏这个 CSS 类: xeblog-slides-essential
。这样做可能会使演示页面更难理解。低级别的 Go WebAssembly ABI
该演讲是在意大利佛罗伦萨的GoLab 2022上作为远程演讲进行的。它使用对话风格完全编写脚本并提前预先录制。
讲话
十多年来,在浏览器中开发应用程序的主要语言一直是 JavaScript。这个房间里的每个人都在处理或接触世界上与 JavaScript 打交道的东西。最近,万维网联盟发布了一个名为 WebAssembly 的标准,这是使 JavaScript 不再成为开发针对 Web 浏览器的东西的要求的第一步。
2018 年中的 Go 1.11 版本增加了对编译 Go 到 WebAssembly 的支持。它允许你获取一个合理的 Go 程序子集,并在浏览器中与 JavaScript 一起运行它们。
我是 Xe Iaso,今天我将帮助你了解它是如何工作的,以及为它的核心提供动力的可怕的黑客攻击。本次演讲针对的是中级到专家级的听众,如果您对 JavaScript 和 WebAssembly 有一定的了解,尤其是如果您是非常糟糕的想法的粉丝,那么它可能会获得最佳效果。为了确保每个人都在同一页面上,我将根据需要为所有内容提供背景和上下文。
因此,在我们了解 Go 的 WebAssembly 支持工作原理的过程中,加入我通过调用约定、I-triple-E 七个 54 个浮点数以及更多内容的神奇旅程!
正如锡上所说,我是 Xe Iaso。我是 Tailscale 的基础设施大法师,我经常被指责为 Go 和 WebAssembly 方面的专家。我在开发和网站可靠性方面有广泛的背景,但我最近一直在做开发者关系。
所以你知道,这次谈话将包含关于许多主题的意见。这些意见是我自己的,不是我雇主的意见。
WebAssembly 是一个规范,它定义了一堆关于不存在的计算机应该如何工作的语义。它定义了虚拟机、堆栈的工作方式、机器可以运行的指令、编译器应该针对的格式以及其他类似的细节。在实践中,它介于本机代码和脚本语言(很像 Java 类文件)之间,但在高层次上,我们可以这样考虑:
WebAssembly 本身就是一台接收代码并执行它的计算机。本质上,它将运行您想要的任何函数,将结果存储在其线性内存中或从堆栈中返回值;但除此之外,它是一个运行非常快的美化反向抛光符号计算器。
有了它,您可以整天做简单的数学运算,但这本身在现实世界中并没有太大用处。
WebAssembly 如何变得有用的真正魔力来自导入到 WebAssembly-land 的外部函数。这些函数几乎可以做任何你想做的事情,从“使用 JavaScript fetch() 函数发出 HTTP 请求”到“读取和写入本地存储”。
WebAssembly 旨在在消费类硬件上运行得非常快。二进制格式被设计为易于解析,并且 WebAssembly 指令可以轻松地实时编译成机器代码。
总的来说,这可以让你的 WebAssembly 程序做任何你想做的事,访问它需要的任何东西,并且总体上可以像 JavaScript 一样强大。只有一些与性能和两个世界之间的转换有关的警告,就像 Unix 上的系统调用警告一样。在实践中真的很好。
但是,让我们更仔细地看一下“从环境中导入的函数”这个东西。 WebAssembly 规范仅定义了虚拟机、代码如何存储到点 WASM 文件中以及如何从点 WASM 文件中加载,以及所有这些工作的语义。它没有指定针对 WebAssembly 编写的程序可以用来与外界对话的 API。
考虑到当时 WebAssembly 团队的限制,他们在尝试推出最小可行产品时没有尝试将稳定的互操作性 API 也加入其中是非常合理的。那可能需要数年时间。但是,这样做的副作用是,每个人都不得不发明自己的一次性 API 来将两者粘合在一起。
哦,为了让事情变得更有趣,WebAssembly 没有原生字符串类型,就像 C 一样!所有这些都是由空字符终止的连续 ram 块。就像我们的朋友 PDP-11 一样。
WebAssembly 最初旨在用于浏览器,但已经努力为 WebAssembly 程序的 API 标准化以在服务器环境中运行。这允许操作员运行来自用户的任意代码,并利用 WebAssembly 带来的固有隔离功能。 WASI(WebAssembly System Interface 的缩写)是一个独立的标准,它为 WebAssembly 程序提供了一些 Unix-y 调用,但总的来说,我们在这里谈论的是浏览器,而不是 Unix 系统。
Go 的 WebAssembly 支持甚至在 WASI 出现之前就已经完成,所以当时它不是一个可行的选择。 WASI 也不支持像“开放网络套接字”这样的系统调用,这使得编写许多现实世界的应用程序在逻辑上很烦人。据我所知,Go 根本不支持 WASI。
虽然有不止一个 Go 编译器。 TinyGo 是一个构建在 LLVM 之上的 Go 编译器,可以使用 WASI 将 Go 程序的子集编译为 WebAssembly。我想重申,Go WebAssembly 端口主要针对浏览器,而不是 Unix 系统。这两个完全是不同的野兽。为了在本次演讲中保持简单,我将重点介绍 Google 的 Go 编译器如何完成所有这些工作。
因此,考虑到所有这些警告,有理由怀疑“我为什么首先要使用它?这是一个全新的编译器端口,具有全新的平台语义,我必须自己发明。”。
这是一个合理的结论,但我反对以下几点:
有时,你在 JavaScript 中需要但在 Go 中拥有的一个库调用并不存在,你真的不想为它进行 API 调用。 WebAssembly 是唯一得到官方认可的方法。
以前,有一个社区努力将 Go 编译为 JavaScript,称为 GopherJS,但随着 Go 的 WebAssembly 端口变得越来越成熟,这种努力已经失宠。
这样做还可以让您在浏览器和服务器上以相同的语言运行相同的代码,这有助于降低在前端和后端问题之间切换时的认知复杂性。
它也很新很有趣!你们都是程序员,对吧?你和我一样清楚,我们很难抗拒新事物的警笛声。
这里有一些值得注意的地方,你可以使用 Go 的 WebAssembly 端口来完成工作。
您可以将现有的对等 VPN 引擎嵌入到浏览器中,这样您就可以从网页通过 SSH 进入生产环境。
您可以将新的 netip 包嵌入到您的 JavaScript 应用程序中,这样您就可以进行高级子网计算,从而更快地设置网络。
您可以编写功能齐全的 Web 应用程序,而无需编写一点 JavaScript。
另一个使用 Go 的 WebAssembly 端口的地方是作为将游戏“熊的餐厅”移植到 Nintendo Switch 的过程的一部分。团队让它在 WebAssembly 端口中工作,然后让一堆自定义脚本将 WebAssembly 的 blob 重新编译为 C++,然后将输入和输出层包装到 Nintendo Switch 使用的专有 API。
据我所知,《熊的餐厅》是第一款在实际游戏机硬件上以任何方式使用围棋的商业发行游戏。
考虑到所有这些,您会看到编写一个 API 是多么困难,它可以让您在浏览器中使用 JavaScript 做任何您想做的事情,就像您编写原生 JavaScript 代码一样。有很多事情要考虑,因为坦率地说,有很多事情要做。计算机出奇地复杂。
Go 标准库有一个名为syscall/js
的包。这将系统调用 API 定义为一堆 JavaScript 代码(包含在 Go 的每个版本中),有助于弥合 WebAssembly 和 JavaScript 之间的差距。您可以专注于在 Go 中编写代码,并让系统调用层处理其余部分。
这通过给您提供对 JavaScript 对象的引用来工作,然后还给您一组调用来随心所欲地操纵它们。这将让您尽最大努力在 Go 代码中执行 JavaScript 对象。
这些引用是程序外部对象的不透明句柄,就像文件描述符是 Unix 中内核对象的不透明句柄一样。
哦,为了更有趣,所有的对象引用都是 NaN 值。
对真的。浮点逻辑中有多个 NaN(非数字)值。实际上有很多超出你的想象。你知道吗,让我们花点时间了解一下数字在计算机中是如何工作的,这样我们都可以理解这个 hack 是多么的优雅。
作为人类,我们通常处理我们所说的“以 10 为底”或“十进制”的数字。每个数字有十个选项。随着数字在数字上向左移动,这些数字表示越来越大的值。让我们想想四百二十六这个数字:
这个数字被分解成对应不同值的数字。有四个百,两个十和六个一。四百二十六(426)。
但是,这仅涵盖整数。很多时候我们会处理整数的小数部分,例如用硬币精确改变小数点后两位。我们的数字系统也通过添加十分之一、百分之一等列来扩展以处理这个问题。
如果我们考虑数字四百二十六点三五,我们也可以像以前那样分解它。有四个百、两个十和六个一,十分之三和百分之五在小数点后。
这就是我们人类处理数字的方式。我们诅咒思考的沙子的奇怪之处之一是沙子以与人类完全不同的方式处理数字。我们当前的计算机处理完全开启或完全关闭的状态。通过一些转换,您可以使用它来表达与十进制算术相同的所有数学运算,但使用两位数选项而不是十位。我们称之为“base 2”或“binary”数学。
由于时间限制,这张幻灯片从录音中删掉了
我们称其为“二进制”,因为它实际上是两个单词拼在一起。 “Bi”表示两个,“ary”是“airity”一词的缩写,指参数的数量。两个参数,二进制。
每个二进制数字不是以十位递增,而是以两位数递增。第一位是个位,第二位是两位,第三位是四位,第四位是八位,等等。
作为一个厚颜无耻的例子,考虑以 10 为底的数字 255。如图所示,它设置了 8 位。一个用于 1、2、4、8、16、32、64 和 128。您可以将所有这些组件相加,得到总数 255。
数学运算的工作方式与您在二进制中所期望的相同。您只需处理两个而不是十个。
但随后我们又回到了数字中的小数部分的问题。我刚刚描述的系统对整数很有效,但小数部分有点混乱。你可以想象只是在某处拍打一个二进制点并做一些黑客攻击以收工(我想旧计算机这样做是为了节省开发时间),但这是未来,我们有一个标准,称为 I-三E 754。
IEEE-754 是用小数部分或浮点数表示数字的事实标准。它定义了这些数字的二进制形式以供计算机使用。它于 1985 年由电气和电子工程师协会或 I-triple-E 首次定义。该标准旨在通过定义语义来帮助更轻松地实现和使用使用浮点数的代码,以便电气工程师可以在硬件中实现它们。
过去三十年或更长时间制造的每种主要编程语言、CPU 和 GPU 都支持 I-triple-E 754 浮点。 Go、WebAssembly 和 JavaScript 也特别使用它。这意味着您可以将浮点数从 JavaScript 传递到编译成 WebAssembly 的 Go 函数中。
顺便说一句,对于本文的其余部分,我将对浮点数使用 16 位编码,以使我的图表更易于理解。 JavaScript 原生使用 64 位浮点数。想象一下还有更多的位。
关于如何实现这一切的一个很酷的部分是浮点数本质上是科学记数法。您有一个符号位来判断数字是正数还是负数、2 的指数以及您相乘的尾数。这使您可以将像二点一二五这样的数字表示为 2 的 1 次方 1.0625 的科学记数法形式。指数为 1,尾数为 1.0625。
所以考虑到这一切,你可能想知道零点三减去零点二的结果是什么。首先,我们需要将这些转换为浮点数:
我们将遇到的第一个问题是我们无法获得浮点数中零点三和零点二的精确副本。
这是科学计数法,科学计数法为您提供了数字的近似值。近似值加起来,最终结果是零点三减去零点二不是 JavaScript、Go 或大多数其他计算机编程语言中的零点一。你得到零点零九九九等等。
但是,如果您将其四舍五入到小数点后两位,您实际上会得到零点一。所以就是这样。
I-triple-E 754 浮点数中的另一件事是对非数字事物的显式编码,例如无穷大。
您所要做的就是设置所有指数位并且不设置任何尾数位。这会让你获得正无穷大。
如果翻转符号位,则会得到负无穷大。
如果你设置任何其他尾数位,你会得到一个非数字值,也称为 NaN。 Go to JavaScript 互操作性使用 NaN 空间数字来编码对象 ID,就像 Unix 使用数字文件描述符来编码内核对象一样。
使用 64 位浮点数,这给 Go to JavaScript 桥带来了一些有趣的东西,比如 4.5 万亿(10 的 15 次方)可能的对象 ID。
通过对指数位进行简单的按位异或 (xor),您可以将 NaN 空间数提取为 JavaScript 端用来寻址它知道的对象的普通整数。
您想要这样做的原因与几乎每个 JavaScript 引擎的核心中都存在的一种荒谬丑陋的 hack 有关。
它们使用 NaN 值作为对象 ID,因为这样对象 ID 就可以放入机器寄存器中。这意味着您可以将 JavaScript 对象 ID 作为寄存器值传递给函数,然后该函数可以在它确实需要关心的情况下查找其上的内容。如果没有,唯一被复制的是非常小的对象 ID。 NaN 值在大多数 CPU 浮点单元中也有一条快速路径,这比您预期的要快。计算机复制东西的速度非常快,但是当你做很多事情时它就会加起来。
像上面一样,那么下面,嗯?
要带走的主要内容是编码为 NaN 值的数字用作对象 ID。这是一个可怕的包装器,在实践中速度更快,因为 CPU 是惰性的。 NaN 值不是数字,但它可以包含数字。
如果您想了解更多关于此的信息,我真的建议您查看 jan Misali 的视频“浮点的工作原理” 。它更详细地涵盖了所有这些内容,包括如何从头开始推导整个浮点数系统。
数字很奇怪,嗯?
抛开所有这些轻松的想法,让我们专注于更令人兴奋的事情。就像调用约定一样。
当您使用机器语言编写程序时,有时您希望获取常见的代码并重用它们。我们可以将这些代码位称为“函数”。在高层次上,他们需要以某种方式获取参数,以某种方式返回结果,并弄清楚如何返回调用函数的位置,以便程序继续正常工作。
一个著名的例子是任天堂娱乐系统的游戏超级马里奥兄弟。每 21 帧游戏会调用一个函数来检查关卡是否被清除。当该函数被调用时,如果它没有显式返回到调用它的位置,则 NES 将继续执行该函数之后的代码。这可能不会达到游戏开发者的意图。它很可能会使NES崩溃,这不好。
因此,为了解决这个问题,在冗长而无聊的文档中描述了一些语义,这些语义指定了调用函数的约定。这些约定说明了参数和返回值是如何工作的,您应该对 CPU 寄存器和其他中间状态(如堆栈卫生)做出的假设,以及数据如何存储在内存中。
有趣的是,在某些情况下,调用的函数的参数比 CPU 的寄存器多。在这种情况下,剩余的参数将被推送到 CPU 堆栈(或函数假定它应该从中读取的内存中的其他位置)。有时你必须让事情变得更复杂一些才能应对边缘情况。
在 Go 1.17 之前,Go 的大多数目标都有一个基于堆栈的调用约定,仿照贝尔实验室的计划 9。当你调用 Go 函数时,它会将这些参数放入堆栈,为返回参数腾出空间,然后告诉 CPU 跳转到有问题的函数。该函数会将它需要的东西从堆栈中弹出,做它需要做的事情,然后在堆栈上返回任何结果。
这在技术上有点慢,因为堆栈存储在系统内存中,但实际上计算机非常快,所以大部分情况下它都能正常工作。
WebAssembly 内部是一个基于堆栈的虚拟机。这意味着 WebAssembly 函数的调用约定有点类似于反向波兰表示法:
(i32.const 1 ) (i32.const 1 ) (i32.add)
要在 WebAssembly 中添加两个数字,请将它们推送到堆栈并发出 add 指令。 add 指令将这两个数字从堆栈中取出,将它们相加并将结果推回堆栈。 WebAssembly 函数的调用方式相同。将参数推送到堆栈,从堆栈中弹出结果。
关于如何指定 WebAssembly 的有趣部分是堆栈位于WebAssembly线性内存之外。这意味着 WebAssembly 中没有“堆栈指针”,因为堆栈在 WebAssembly 中不存在。函数可以通过向堆栈推入并从中弹出来操纵堆栈,但它们实际上不能移动堆栈。
我很确定他们是这样设计的,以便人们可以编写具有多个线性内存空间的 WebAssembly。有趣的是,这也意味着您可以创建一个线性内存空间为零的 WebAssembly 模块。我不知道实际使用它在野外有什么重要的东西。
在执行诸如调用函数或切换 goroutine 之类的事情时,Go 编译器会告诉堆栈指针移动到新位置。每个 goroutine 都有自己的堆栈,为了切换到该 goroutine,您需要能够移动堆栈的位置。
所以你需要有一个解决方法。有很多方法可以做到这一点,但考虑这一点的一种方法是程序运行时堆栈指针的整体流程。
当一个 goroutine 调用一个函数时,它必须将堆栈指针更改为新位置。堆栈指针是指向内存中某个位置的 CPU 寄存器。当您操作堆栈时,此指针通常会更改。
这意味着您可以将堆栈指针作为参数传递给每个函数,对吗?当您尝试引用堆栈中的内容时,这会有点麻烦,并且可能会导致一些额外的开销,但它会起作用。
所以 WebAssembly 端口就是这样做的。 WebAssembly 中的每个 Go 函数都接收堆栈指针并且不返回任何内容。
当程序启动时,运行时内部有一些例外,但除此之外,每个函数确实只有一个参数:堆栈指针。
一切都通过推入堆栈返回。通过从堆栈中弹出来读取参数。一切都是通过编译器从类型中知道的特定于类型的偏移量完成的。
幻灯片显示https://github.com/Xe/olin/blob/0cf90810960ba4d7d80e20448ec08a71a3510deb/abi/wasmgo/abi.go#L242语法突出显示
// goRuntimeNanotime implements the go runtime function runtime.nanotime. It uses // the Go abi. // // This has the effective type of: // // func (w *WasmGo) goRuntimeNanotime() int64 func ( w *WasmGo) goRuntimeNanotime ( sp int32 ) {now := time. Now (). UnixNano () w. setInt64 (sp+ 8 , int64 (now)) }
因此,如果您想实现内部运行时函数之一,例如 runtime.nanotime,您需要将返回值推到堆栈指针前面 8 个字节。手动实现所有这些在实践中是一个巨大的痛苦,并且需要深入了解 Go 编译器的工作原理。
这段代码来自我早期对名为 Olin 的服务器端 WebAssembly 的尝试,我试图做到这一点:实现 Go 编译器 WebAssembly 对服务器端执行的支持。我没有达到可以运行任意程序的地步,但我已经非常接近了。
这有点疯狂,但我相当肯定这是让 Goroutine 在 WebAssembly 中工作的秘诀。 Goroutine 堆栈像平常一样在内存中,通过更改函数参数中的堆栈指针,它们可以交换。即使没有多线程支持,这也让运行时在浏览器中执行并发代码。它只是不能同时进行两个不相关的计算。
WebAssembly 有一个堆栈,但它与 goroutine 堆栈的工作方式不兼容。 Go 通过将 goroutine 堆栈放在内存中并将堆栈指针作为烫手山芋传递来解决这个问题。
现在我们有了在浏览器中引用对象的能力和运行 Go 函数的能力,接下来会发生什么?我们如何使用它来做一些有用的事情?
我们使用全局对象!在 JavaScript 中有一个名为globalThis
的神奇全局对象。该对象将始终存在于浏览器和服务器端 JavaScript 环境中,这是所有全局对象(如 Date 和 WebSocket)以及 fetch 等函数所在的位置。
当 Go WebAssembly 二进制文件启动时,它设置的第一个对象是对这个全局对象的引用。这允许您的 Go 程序访问浏览器中的 HTML 操作和文件系统(如果您在服务器上运行它)。
从这里你可以做任何你想做的事。您可以像平常一样发出 HTTP 请求,它们会自动发送到 JavaScript 中的 fetch 函数。你可以以任何你想要的方式自由使用任何东西。
想要制作一个从某些输入字段读取并将 HTTP 请求发送到 API 服务器的按钮?你可以这样做!想要连接到 WebSocket 服务器?你可以这样做!这一切都很好!
除非您必须自己为各种 JavaScript 类型编写大量包装器。
一旦完成这些(遗憾的是,可能需要几次尝试才能获得完全正确的东西),您就可以像在 JavaScript 中一样使用它们。这就是 Go 的另一个属性派上用场的地方,它使事情变得更加方便:接口。
Go 最独特的功能之一是接口类型。接口类型让您可以根据它公开的方法来描述类型的“形状”。
type Quacker interface { Quack () }
这意味着您可以创建一个 Quacker 接口,该接口具有一个名为 Quack 的方法,然后是另一个实现该方法的 Duck 类型。鸭子是贵格派。
您还可以制作另一种名为“绵羊”的类型,并将“绵羊”变成“嘎嘎”……假设您可以弄清楚会发出嘎嘎声的绵羊会是什么样子。
因此,当您围绕 WebSockets 制作包装器时,您还可以使包装器成为 io 点读取器,以便您可以从中读取数据,一个 io 点写入器,以便您可以将数据写入其中,以及一个 io 点更接近,以便您可以关闭插座。
然后你可以在你的 Go 代码中使用你的 WebSocket 包装器,你甚至不需要切换大多数类型。将你的 websocket 推到你可以放置读者的地方。让它实现 net dot conn 调用,然后像使用套接字一样使用它。世界是你的牡蛎,它充满了珍珠。
在完全不同的环境中使用完全不同的编译器目标的所有这些粗糙边缘开始逐渐消失。在最坏的情况下,您需要为 WebAssembly 端口编写一些特定于构建标签的代码(因为 Linux 程序不在 JavaScript 解释器中运行),但很多时候您会没事的。
随着时间的推移,编写这些包装器类型也会变得更容易。前几个会有点奇怪,但是一旦你迈出了步伐,它就会变得容易得多。它肯定会教你很多关于 JavaScript 类型如何工作的知识,如果你选择的话,这将使你在 JavaScript 上写得更好。它会解决的。
如果您想调整 Go 程序以使用 WebAssembly 或考虑使用 WebAssembly 制作新程序,这里有一些关于如何使事情发挥最佳效果并应对您的生活决定的建议。
六边形架构技术(也称为端口和适配器)是一种常见模式,您可以通过它们与外部世界通信的方式来描述程序。
程序通过另一端有适配器的端口向外插入。 HTTP 客户端将是一个端口,而使用 JavaScript 获取功能的传输将是一个适配器。想象一下它是如何更普遍地扩展到您的程序所做的许多事情的。
Go 的接口系统必须量身定制,以使六边形架构成为生态系统中的一等公民。您甚至不用考虑就使用接口。
HTTP ResponseWriter 是一个接口,允许您使用相同的处理程序代码处理 HTTP 1.1、2 和 3 连接。将内容写入文件通常会让您使用编写器接口作为接收器。日志记录通常也进入接口。它的接口一路下来!
想想端口在您的应用程序中的位置。那里有哪些适配器?你怎么能添加一个新的?您图书馆的用户如何调整它以满足他们的需求?
在实践中,这通常归结为您应该已经在做的事情,例如“让人们通过已经打开的网络连接”“让人们通过他们自己的预配置 HTTP 客户端”。
如果您让用户提供他们自己的预配置适配器,那么他们可以以任何他们想要的方式使用您的库,并且可以灵活地满足他们的需求。即使他们可能一开始就不应该做那样的事情。
当您需要确定在应用程序中放置端口和适配器的位置时,接口不是一个糟糕的起点。
调试 WebAssembly 有时真的很烦人。很多时候,当您调试 WebAssembly 时,您实际上并没有太多选项来实际进行调试。您的浏览器的开发工具中有一个调试器,但开发人员的体验仍有很多不足之处。
问我怎么知道的。
其实,你知道吗,让我们进入故事时间。这是我第一次在不使用浏览器中的调试器的情况下调试复杂的 WebAssembly 程序的故事。
我之前提到的一个副项目是名为 Olin 的服务器环境上的 WebAssembly。我想创建 Olin 作为 AWS Lambda 或 Google Cloud Functions 之类的主干。我希望人们能够将 WebAssembly 模块上传到控制服务器,然后在事件发生时触发它们在某处运行。
所以我正在经历一场风暴,我设法让一些东西发挥作用。我编写了一个程序,从用户那里获取一个 HTTP 请求,然后返回一个 HTTP 响应以发回。它正在工作。我很兴奋。然后我添加了戳外部 API 的功能,然后出现了问题。我的 WebAssembly 模块惊慌失措,我收到了一条模糊的错误消息。
有用的是,我没有调试日志。我调整了代码以将恐慌处理程序更改为更大声。没有骰子。我必须想出另一种方法来理解发生了什么。
当 WebAssembly 模块崩溃时,它可能表现为运行时环境恐慌。就我而言,当我从 API 将一堆数据读入内存后,它就惊慌失措了。在 WebAssembly 中,线性内存只是一系列连续的字节。我的代码正在处理的数据出了点问题,然后我有了一个想法。
如果我可以检查内存怎么办?
所以我看了一下我正在使用的 WebAssembly VM 的 API。我确实可以作为字节片访问该 WebAssembly 模块的线性内存。我不知道它处于什么状态,但我知道我可以得到它。我也知道我可以使用方便的 Writer 界面将其写入文件。
我添加了一个命令行标志,以便在 WebAssembly 进程完成时无条件地将 WebAssembly 内存转储到文件中。我再次运行程序,它再次崩溃。唷,至少它是一致的。
So now I have this several megabyte long binary file that I needed to investigate. I admit that I haven’t done much binary file parsing, but in the past when I’ve needed to do terrible things I’ve reached for a tool called xxd
.
xxd
is a tool that lets you see the raw bytes of a binary file by showing them both in hexadecimal form and in ascii form (if the character is printable). If you run it on a longer file, it will run over your terminal screen space and you will need to pipe the output to the less command to break the output into “pages”.
This makes it possible to understand the hexadecimal soup that will pour out of the memory dump.
So I started to read the hex dump of the crashed WebAssembly program, and at first it felt like I understood nothing. It was overwhelming at first as my terminal paged through function name after function name (was that for panic handling?) and then I quit out of that and took a moment to look at the code again and think.
The program was just making an HTTP request, this means that there’s going to be an HTTP response somewhere. The HTTP response had JSON in it. JSON has lots of curly braces. If I want to find out what was going on, I need to look for curly braces.
And like that, I was in, I found the JSON in memory and then I took a look at the JSON compared to my code. After a couple double takes I felt flabbergasted. I had the wrong type for a variable in a structure, and I was using the unwrap
function in Rust to parse it. The type was wrong, so it tried to log an error message, panicked, and crashed. Beyond that my panic handler didn’t work, so I had to fix that too.
But, after I fixed everything it worked. And that felt so, so good.
I’m pretty sure that this is a lot easier in browsers now. I’m not sure if Go supports source maps though, those let you see the source code of the programs you’re debugging in the browser inspector. This is most commonly used when you’re trying to debug minified JavaScript code. It would be really convenient if that support was added in the future.
Something else to keep in mind is that you need to keep things cognitively simple. Don’t overcomplicate things. Things are going to be complicated enough because of the weirdness involved in writing your Go code to target a new platform. Don’t be clever.
This is going to spend a lot of “innovation points”, so be sure to make things as easy to understand as much as you can. It makes debugging crashes so much easier.
I’ve spent a lot of time on the state of the world as it is and the hacks we needed to get there, but I’m a lot more excited about what the future could bring. Here’s some things that I can’t wait for.
WebAssembly is still under active development to improve the state of the world. One of the proposals I’ve kept my eye on is the component model proposal. It introduces the concept of “component model types”. They are similar to Go interfaces, but they go a step farther. They define complicated objects and fields on them, not just methods.
You will be able to code generate native bindings for these interface types. You will be able to do DOM manipulation in Go with the same calls as you will in Rust or Zig. You will be able to import things like WebRTC into your Go programs and operate on it like it was written in Go in the first place.
Everything would be taken care of for you. There would be no more drastic wrapper types all over the place. There would be no more NaN-boxed object IDs. You’d just write Go and it’d just work. You would also no longer need to do so much feature detection compared to what you need to do currently.
The worst part about this is that browsers don’t support this yet. There is one server-side runtime that has experimental support for them, but nothing else really does so it’s not overly usable yet.
God I want these though, sooner is better.
One of the weaknesses of WebAssembly is that it’s a single-threaded environment. A WebAssembly program can only really do one calculation at once. There’s a threading extension that will change that. It allows for WebAssembly programs to have multiple threads that execute in parallel.
This will allow your Go programs in browsers to do multiple calculations at once, just like your Go programs on servers.
Chrome and Firefox have experimental support for threads, but Safari (and by extension every iOS device on the planet) does not. As such, the Go WebAssembly port doesn’t take advantage of these yet. It will be really great when we can though!
But, we can’t do that yet. It’ll be better soon. I have faith in the system. It looks slow now, but that’s because people are trying to make sure that they don’t mess it up. That hesitance is surely going to end up being a benefit.
Well, we covered a lot today. We learned a bunch of things:
The Go compiler has a WebAssembly port. You can use it to run your Go code in a browser.
The WebAssembly port can be used to poke the browser and manipulate webpages with the same calls that you use in JavaScript.
We learned the terrible secret of NaN-boxing and why someone would envision, let alone implement, such an accurse-ed thing.
We learned about Go’s calling convention, stack manipulation, and how Go works around platform limitations in WebAssembly.
We learned about Go interfaces and hexagonal architecture to help you fit square pegs into round holes.
And finally I got your hopes up for the future before smashing them down back to Earth by saying that we can’t have those nice things yet.
If you haven’t played with the WebAssembly port yet, I’d suggest trying it out. It’s a totally different way of writing Go than what you do on a regular basis. Web apps are also very easy to share with your friends slash group chat because everyone has a web browser installed. You may just be able to make something useful that people come back to. Try it and see!
These talks take a lot of effort, time and research to turn into the reality you’ve seen here today. I want to especially shout out everyone on this list for helping make this talk shine in some way.
谢谢! You all really help more than you can imagine.
And thank you for watching! I’m going to be available to answer any questions I haven’t answered already. If I miss your question somehow or you really want an answer privately, please email it to [email protected] .
I’ll have a written version of this talk including my slides, a recording of the talk and everything I’ve said today on my blog pretty soon.
If you have questions, please speak up. I love answering them and I am more than happy to take the time to give a detailed answer.
Be well, all.