ZhangHanDong

WebAssembly简介

ZhangHanDong · 2017-02-22翻译 · 498阅读 原文链接

WebAssembly, 简称WASM, 是一种以安全有效的方式运行可移植程序的新技术,主要针对Web平台。 与 ASM.js类似, WASM的目标是对高级程序中间表示的适当低级抽象,即,WebAssembly代码旨在由编译器生成而不是由人来写。 W3C 社区组 拥有来自于最大Web浏览器厂商的代表,比如Google, Microsoft, Apple 和 Mozilla ,非常令人期待。

如果你正在阅读本文,很可能你已经对WASM有一定的了解。如果你不了解,那么你可以好好看看 webassembly.org。就在本文发布的时候,WebAssembly 刚刚达成了 浏览器预览里程碑,这意味着WebAssembly版本1很可能和当前草案所描述的一致。本文的细节基于版本mvp-13。

已经有 很多 现存编译器 可以生成WASM,但是本文创建WASM将不会有太多依赖,也不会用高级语言。

看来你的浏览器不支持WebAssembly,但没关系,在本文中,您不需要它。

让我们开始吧。

WebAssembly程序剖析

实际上,称之为“模块(module)”,是因为使用WebAssembly并没有“程序”和“库”之间的区别,只有“模块”,彼此之间可以搭配,亦可通信,每个“模块”都有“main”函数。

首先是模块中用于表示参与编码的WebAssembly版本。接下来是若干段(section),它们都包含了关于模块的信息。模块总是以包含了顺序的标准段开始,并且可选地结束于任何数量的可以包含任何类型数据的自定义段,所有数据都可以被标准的WASM虚拟机忽略。

在WASM版本1中的标准段如下,标记为星号(*)的段都是任何功能模块都必需的:

  1. Type* — 函数签名声明
  2. Import — 导入 声明
  3. Function* — 函数声明
  4. Table — 间接函数表和其他表
  5. Memory — 存储器属性
  6. Global — 全局声明
  7. Export — 导出
  8. Start — 开始函数声明
  9. Element —元素段
  10. Code* — 函数体
  11. Data — 数据段

    type 段包含了整个模块用到的唯一函数签名列表。也包含了任何导入函数的签名。 列表中的位置是type签名在模块中的唯一索引。例如:

(i32 i32 -> i32)  // func_type #0
(i64 -> i64)      // func_type #1
( -> )            // func_type #2

WebAssembly只有四种具体类型:32位整数,64位整数,32位浮点数和64位浮点数,其中整数类型是无符号的(后面会有更多说明)以及浮点数数字符合IEEE 754-2008标准。 任何复杂类型都可以由编译器构建在这些基本类型之上。 本文的其余部分以及 WebAssembly文档将分别使用简称i32,i64,f32和f64来引用这些基本类型。

import 段 通过列出每个函数,值或数据所需的模块名称,字段名称和类型来声明任何外部依赖性:

("dumb-math", "quadruple", (func_type 1))        // func #0
("dumb-math", "pi", (global_type i64 immutable))

由主机系统(例如Web浏览器)来解析这些导入,这是运行时动态链接在WASM中实现的方式,我们将在后面看到,这也用于与非WASM函数交互的外部函数接口。

function段 为之后定义于代码段的每一个函数声明索引,其中列表中的位置是函数的索引,及其值的类型。有效的函数索引始于func_type 导入的数, 这意味着模块中有效的函数列表是通过函数段列表过滤的import段列表

(func_type 1)  // func #1
(func_type 1)  // func #2
(func_type 0)  // func #3

我们随后会通过这些索引来调用这些函数(上面的"func #N",N就是函数的索引)。

table段 定义任意数量的 。表是用于映射不能由WebAssembly表示或不能直接访问的不透明值的机制,例如JavaScript对象或操作系统文件句柄。此功能以边界检查表为代价间接的弥合了底层未受信线性内存和高级不透明句柄或引用。我们不会对表做过多的探究。

memory段 通过定义其初始大小和可选地预期扩展的大小来定义模块的可选存储器。 数据段被用于初始化存储器。

global段 为模块声明任意数量的可变或不可变全局变量,等价于C和C++里的 static 变量。

export 段声明模块中能被主机环境访问的任意部分,不包含专门的启动函数。

("half" (func 1))

上面会把函数1导出为“half”。如果我们看看上面的function段, 可以看到func #1的类型是 func_type #1,所以主机环境看起来是这样:

function half(arg0 :int64) :int64 

除了函数之外,模块也将能table、memory段和global导出到外部环境。

start段 指定模块加载时要调用函数的索引,该机制可以使模块成为可执行程序,或者用来动态初始化模块的全局和memory段。

element段 允许模块初始化从外部导入或在 table段中定义的表的内容。

代码段可能是大多数WebAssembly模块的主体,因为它定义了模块中所有函数的全部代码。 函数体的定义顺序与它们在function段中的相应函数索引相同,但并不包括导入。 我们的“half”函数的body可以这样定义:

get_local 0  // 往栈上push 参数#0  (被除数)
i64.const 2  // 往栈上push int64常量 "2"(除数)
i64.div_u    //无符号除法; 往栈上推送结果
end          // ends 函数, 在栈顶产生一个i64

你可能已经猜到了: WebAssembly是一个堆栈机 ,这意味着我们可以从虚拟栈里push和pop值。大多数WASM操作符代码(简称“操作码”),采用and或or来为栈中添加明确定义的值。

让我们看看运行“half”函数代码的时候 栈上会发生什么 :

关于此图的一些说明:

  1. 该栈是抽象意义的,我们不能对它进行索引、测量或检查,我们只能对它进行push或pop操作。这样就为实现高效的VM打开了方便之门。
  2. 上面的第四步可能永远不是真实VM中的实际状态,因为没有理由实际清空堆栈。该步骤旨在说明堆栈理论上发生了什么。
  3. 结尾的函数隐式返回,调用者将会在栈顶留下可用的值。

这里有很多由WASM 1定义的数字操作符 ,就不在本文列出了,太多了。然而,你可能会欣慰的发现,除了你可能希望的所有基本的操作符之外,还有若干有用的通用数字操作码定义。比如:

  • eqz 有效的测试整数操作数是否为零,当结果为否定并返回0和调用eq的时候可以降低栈抖动。
  • popcnt 计算整数中设置的位数。
  • ceilfloor 在用sqrt计算浮点数平方根的时候执行 ceiling 和 floor 函数

还有一个通用的操作符转换库,用于在两个行为明确定义的类型之间进行转换。

最后同样重要的是数据段 ,用于初始化导入或本地内存:

(data_segment
  0                          // 线性内存索引
  (init_expr (i32.const 4))  // 用于放置数据的字节偏移量
  (data 0x2a 0x0 0x0 0x0))

这会用提供的字节值初始化0号内存的[4-8]字节,如果作为无符号i32读取的话,则是数字“42”。

管理数据和值

我们已经了解WebAssembly使用值堆栈来保存和读取基本的值。还有几种方法可以管理模块中的数据和状态。

  • 堆栈: 机器用来操作那些操作数的地方,并把它们的结果放在栈上。
  • 局部变量(local)全局变量(global) : 可命名,并且可以保存任何与栈相同的基本类型(i32,et al。)
  • 存储器: 随机访问的字节数组,它可以存储任何我们想要的。
  • 元素: 用于外部值的不透明“句柄”(如OS文件句柄)。

局部变量(Local)跟许多高级语言中的变量很相似: 我们可以命名它,用它存储和获取值。 我们使用function-local整数索引来命名它。

参数也是局部变量,局部变量的“名称”索引从第一个函数参数开始为0,并且随着每个附加参数以及函数体中每个列出的局部变量连续递增。局部变量总是初始化为零(所有的位都为0)。

在本文前面的示例中,我们使用操作get_local将第一个函数参数的值推送到了栈上。我们还可以执行相反的操作:从堆栈中弹出一个值并将其存储到局部变量。

i32.const 123  // 演示:把 "123" 推送到栈上
set_local 0    // 从栈上弹出 "123" ,并把它存储到局部变量 #0
// 使用堆栈进行其他操作...
get_local 0    // 把"123"推送到栈顶

局部变量可以在执行其他操作时保存临时值,否则这些操作会替换或移除栈中的值。

全局变量 是带有自己的索引和操作符的模块范围内的局部变量,但是除此之外还共享局部变量的语义。全局索引从任何导入的全局变量开始,并随着在全局段中定义的全局变量递增。我们可以使用get_globalset_global操作加载和存储全局值。

存储器 是最灵活的,因为它允许我们存储任何我们可以想象的数据,但更易使用::

  • 线性存储器
  • 存储器大小是内存页尺寸的偶数倍,即64 KiB
  • 存储器可以由模块导入或声明

“线性”存储器是指没有可用的随机操作符分配器,模块中的代码使用的所有内存地址都表示为从内存段开始的字节偏移。 WASM是一种低级格式,这样很有意义。它取决于更高级别的语言,目标是WASM在这个线性存储器空间之上提供内存管理(如果需要)。

元素(element) 是一些不透明的 “值句柄”,它是指主机环境的一些东西对于WebAssembly是不透明的。例如,当主机环境运行JavaScript,元素可能表示操作系统文件句柄或JavaScript对象。

元素虽然很酷, 但是 存储器充满了更多的乐趣,所以让我们再回头学习下存储器,看看它还能让我们做些什么。

未来版本的WASM可能允许使用多个memory段,但版本1的每个模块只能使用一个,这简化了很多事情,比如存储器操作符:

  • i32.load 对齐偏移(alignment offset)从字节范围[start..start+4)加载32位整数 , 这里的 start是从栈中弹出的整数操作数。
  • i64.store16 对齐偏移 通过将从堆栈弹出的64位整数包装到字节范围[start-start + 2]中来存储16位整数,其中start是也从堆栈弹出的整数操作数。

以下代码在字节范围[4..7]内存储i32“123”:

i32.const 4    // 地址的字节大小
i32.const 123  // 存储值
i32.store 2 0  // 2 = 32位/4字节对齐, 0 = 偏移

可以稍后将值加载到栈上:

i32.const 4
i32.load 2 0   // 该操作执行后栈顶就是 i32 "123"

内存的灵活性不仅在于我们可以加载过去存储的东西(当然你用局部变量也可以做相同的事),而且它在函数的生命周期内是持久的,这意味着我们可以跨函数访问值 ,将相同的数据解释为不同类型,并且以比局部变量、全局变量和堆栈使用更紧凑的方式按字节来存储值。

想象一下,我们想要调用以好基友命名的函数“Lolcat”。将名称编码为UTF-8文本,每个字符只需要1个字节(因此总共6个字节加上一些额外的标记或长度值)。一种简单又呆板的方式是将每个字符推送到栈上并且使被调用者接受一些预定数量的参数,例如, function(c0,c1,c2,c3,c4,c5 i32):void

i32.const 0x4c // 'L'
i32.const 0x6f // 'o'
i32.const 0x6c // 'l'
i32.const 0x63 // 'c'
i32.const 0x61 // 'a'
i32.const 0x74 // 't'
call 1         // 调用函数,该函数需要6个参数

这不仅呆板的可笑,而且它还会占用大量栈空间——每一个“栈位”占用一个字。对于32位的系统(或32位虚拟机),这意味着“Lolcat”字符串中的每一个字母占据4个字节,使总数达到24个字节。另一个要考虑的事情是,栈虽然是抽象的但它是有限制的,如果我们将太多的值推送到堆栈中而不弹出,主机就会恐慌并Crash模块。

这是存储器的任务,假设我们调用的函数是一个只包含字符串存储器地址的参数,比如function (addr i32) :void。 首先,我们用“Lolcat”字符串初始化模块内存的一部分:

(data_segment 0
  (init_expr (i32.const 4))  // 放置数据的字节偏移量
  (data 0x4c 0x6f 0x6c 0x63 0x61 0x74 0x0))

注意在本例中,我们在字符串的末尾放置一个额外的零字节“标记”。 这在C语言中很常见,但通常不鼓励。 如果你正在写一个真正的程序,你可能想用字符串的长度作为前缀,而不是我们例子中的“标记字节”。

我们现在可以使用字符串的地址(4)调用函数:

i32.const 4  // 字节偏移作为参数传递到内存中...
call 1       // 调用接收函数

接收函数将从内存中加载所需的字符串的任何部分。 例如,假设它调用一个导入的“putc”函数(将一个字节放入程序stdout流):

void print_str(i32 addr) {
  i32 byte = 0;
  while (true) {
    byte = i32.load8_u(addr);
    if (byte == 0) { break; }
    putc(byte);
    addr = addr + 1;
    continue;
  }
}

在WASM中,我们声明了在函数中需要一个额外的局部变量(local #1;字节变量),并且现在可以实现等效功能:

block void          // declares a "label" at it's "end"
  loop void         // declares a "label" right here
    // byte = i32.load8_u(addr)
    get_local 0     // push addr
    i32.load8_u 0 0 // push the byte at addr as an i32
    tee_local 1     // store the byte to local 1, but don't pop it

    // if (byte == 0) { break }
    i32.eqz         // (x i32) => i32(x == 0 ? 1 : 0)
    br_if 1         // if the byte was zero, jump to end of "block"

    // putc(byte)
    get_local 1     // push byte
    call 0          // call imported "putc" function with the byte

    // addr = addr + 1
    get_local 0     // push addr
    i32.const 1     // push i32 "1"
    i32.add         // push result from addr + 1
    set_local 0     // store new address to "addr" local

    // continue
    br 0            // jump to "loop" (i.e. continue looping)
  end
end // end of "block"

存储器寻址

你可能已经注意到了,我们为加载和存储操作运算符提供了两个立即值。第一个立即值是一个对齐提示,会按2的幂次方编码为log2(alignment)。这意味着对齐立即值是以下值之一:0 = 8位,1 = 16位,2 = 32位和3 = 64位。

如果存储器访问的有效地址是其要访问的对齐属性值的倍数,则认为存储器访问是对齐的,否则认为是未对齐的。对齐和未对齐的访问具有相同的行为。1

对齐提示可以认为是对执行我们代码的虚拟机的约定,即“有效地址将以N位对齐”,VM将使用该信息来优化代码。 当我们约定某种对齐,但未能遵守该约定,提供一个未对齐的地址时,操作将可能比我们提供了一个有效对齐地址更慢。

因此,作为经验法则,您应该只约定可以保留的内容 ,提示任何操作的最大可能的对齐,但不大于本机对齐(wasm32为32位,wasm64为64位)。

loadstore运算符的第二个立即数是地址偏移量。 这确实有点困扰,所以让来我们一一澄清偏移到底是什么,有效的地址是什么,当我们存储和加载值时内存发生了什么。

这里是线性存储器的起始图示,初始化为零:

有效地址是从存储器起始处测量的字节偏移量,是地址操作数和偏移立即数之和。


有效地址=地址操作数 + 偏移立即数(effective-address = address-operand + offset-immediate )

你可能会问“如果我有一个地址操作数为什么还需要偏移立即数”? 这是一个好问题。正如在前面的例子中所看到的,我们可以为地址操作数提供一个常数值(例如使用i32.const)。 但是,当我们像之前的“print_str”函数中那样开始使用动态地址时,对于编译器来说是非常有用的,为模块函数的所有内存操作添加一个常量偏移量,以便从存储器的一个区域“重定位”到另一个区域。

WASM为四种基本类型提供了丰富的存储器操作集,允许读取一些字节数作为某种数字。 例如,我们可以通过将值截断为16位整数来存储两个字节的64位整数:

i32.const 3      // 地址操作数 = 3
i64.const 1234   // 值
i64.store16 1 3  // 对齐 = 1 = 16-bit, 偏移立即数 = 3
                 // 有效地址 = 3 + 3 = 6

由于我们提示了一个16位对齐,并且有效地址是2的偶数倍(2字节= 16位),所以这是一个优化的(对齐)存储。 我们的内存现在看起来像这样,存储的段被标记为深色背景颜色:

无论主机平台的本地“字节次序(Endianess)”是什么,WebAssembly总是对存储的值使用小端编码。 这意味着16位整数“1234”被编码为0xd4 0x04。

如果我们存储的是一个32位或64位的值,那么由于有效地址“6”位于32位和64位字的中间,存储将不对齐并且效率低下:

i64.store32 2 3  // 对齐 = 2 = 32-bit, 偏移 = 3, 地址 = 6

红色箭头代表存储对齐的约定(32位边界),我们必须增减2来改变我们的地址(即6±2)。

当有效地址处于约定对齐幅度的边界时,load或store是对齐的。 让我们调整偏移立即数以获得32位对齐存储:

i64.store32 2 5  // 对齐 = 2 = 32-bit, 偏移 = 5, 地址 = 8

最佳对齐存储。

值的转换与重新解释

我们可以自由解释任何字节为任何值。 例如,我们可以加载四个字节作为32位浮点数来表示上面存储的16位整数:

i32.const 4   // address-operand = 4
f32.load 2 4  // align = 2 = 32-bit, offset = 4, addr = 8

堆栈的顶部现在包含一个粗略表示数字“1.7292e-42”的f32,这可能不是你预期的。 记住,我们没有将一个数字从一种类型转换为另一种类型,我们只是将用于表示一种类型的原始数据解释为另一种类型。

如果我们的意图是将i32值加载为等效的f32数字,那么我们使用一个数字转换运算符作为目标类型:

i32.const 4       // address-operand = 4
i32.load 2 4      // align = 2 = 32-bit, offset = 4, addr = 8
f32.convert_u_i32

栈顶现在是一个值为“1234.0”的f32。

浮点数总是“有符号”,但整数可以无符号。 “但是,我们在本文一直在使用i32,并没有看到任何符号特定的运算符!”我们目前使用的大多数运算符只需要知道数据的大小,并不在乎该值是否被解释为有符号或无符号。 WebAssembly整数值基本上为未知符号

当然有几个操作需要知道它的操作数是有符号的还是无符号的。 例如,“小于”运算符需要知道它比较的是否为负数。

符号相关的运算符有一个_s'或_u'后缀。 这里有几个:

i32.lt_s    //   signed-i32  <    符号型-i32
i32.lt_u    // unsigned-i32  <  无符号-i32
i32.div_s   //   signed-i32  /    符号型-i32
...

JavaScript API

由于“WebAssembly”在其名称中带有“Web”提示,第一个有用的WebAssembly平台是最流行的Web浏览器的最前沿版本:Chrome,Firefox, SafariEdge。 在撰写本文时,需要通过浏览器的高级设置启用WebAssembly。 在Chrome中,设置位于“flags”(chrome://flags/#enable-webassembly)下。

我不打算对此谈太多,因为已经有大量很好的JavaScript浏览器API的信息了。 这里有一个加载模块的示例,“实例化”它(运行它)并最终从JavaScript调用其导出的函数之一:

注意:您当前查看的Web浏览器不支持WebAssembly。 以下示例将无法在此Web浏览器中工作。

WebAssembly.compile(new Uint8Array(
  00 61 73 6d  0d 00 00 00  01 09 02 60  00 00 60 01
7f 01 7f 03  03 02 00 01  05 04 01 00  80 01 07 07
01 03 66 6f  6f 00 01 08  01 00 0a 3a  02 02 00 0b
35 01 01 7f  20 00 41 04  6c 21 01 03  40 01 01 01
0b 03 7f 41  01 0b 20 01  41 e4 00 6c  41 cd 02 20
01 1b 21 01  41 00 20 01  36 02 00 41  00 21 01 41
00 28 02 00  0f 0b 0b 0e  01 00 41 00  0b 08 00 00
00 00 2c 00  00 00.split(/[\s\r\n]+/g).map(v => parseInt(v, 16))
)).then(mod => {
  let m = new WebAssembly.Instance(mod)
  console.log('foo(1) =>', m.exports.foo(1))
  console.log('foo(2) =>', m.exports.foo(2))
  console.log('foo(3) =>', m.exports.foo(3))
})

如果我们将它粘贴到启用了WebAssembly浏览器的控制台,我们应该看到模块做了一些简单计算的结果:

foo(1) => 400
foo(2) => 800
foo(3) => 1200

希望给你在过渡到WebAssembly驱动的应用程序过程中,对WASM如何与JavaScript进行实用有效的互操作有所提示。 也许你会在React和JavaScript中编写你的UI,但在WASM中编写一个像JPEG编码器或文件格式解析器的东西,让它们一起运行得很好。

wasm工具(wasm-util)

我把一些用于和WASM协同工作的 TypeScript/JavaScript小部分代码集合称为 wasm-util。这些代码没有任何依赖,并且有一定程度的结构化,对于只需要某些功能的任务只需要选择几个源文件就可用了。

  • ast完整的WebAssembly规范 提供了一个完整的TypeScript类型系统,并为WebAssembly模块的所有部分提供了构造函数。
  • emit 为根据AST生成WASM字节码提供帮助程序
  • repr 生成人类可读的AST文本表示
  • lbtext 从AST指令生成线性字节码文本

有关完整的功能列表,请参见 wasm-util readme

我发现自己依赖于一个非常复杂的工具链(llvm,binaryen等源码构建),而我所寻找的是倾向于WebAssembly的工具。 wasm-util的主要组件是ast,它提供了一种方便的方法来构建完整的WASM模块。如果使用TypeScript,则具有完全静态类型检查。

以下示例是构建提供阶乘函数的模块。 让我们以一个类似C语法的函数开始:

int64 factorial(int64 n) {
  return (n == 0) ?
    1
  :
    n * factorial(n-1);
}

等价的WebAssembly代码看起来像这样:

get_local 0    // push parameter #0 on stack.
i64.const 0    // push constant int64 "0" on stack.
i64.eq         // execute "eq" which pops two operands from stack
               //  and pushes int32 "1" or "0" on stack.
if i64         // pops one int32 from stack; if its not "0":
  i64.const 1  //   push constant int64 "0" on stack.
else           // else (if operand was "0"):
  get_local 0  //   push parameter #0 on stack. $1
  get_local 0  //   push parameter #0 on stack.
  i64.const 1  //   push constant int64 "0" on stack.
  i64.sub      //   execute "sub[tract]" which pops two operands
               //    from stack (parameter #0 and constant int64 "1")
               //    and finally pushes the result int64 on stack.
  call 0       //   call function #0 ("factorial") which pops one
               //    int64 from the stack and when it returns an
               //    int64 has been pushed on stack
  i64.mul      //   execute "sub[tract]" which pops two operands
               //    from stack ($1 and result from function call)
               //    and finally pushes the resulting int64 on stack
end            // ends function, returning one int64 result (on stack.)
               // Stack now contains one int64 value that's the result from one of
               // the two branches above.

上面的代码是由lbtext打印的,我们提供了一个用 ast模块构建的AST:

import ... 'wasm-util/ast'
const mod = c.module([

  type_section([
    func_type([i64], i64), // type index = 0
  ]),

  function_section([
    varuint32(0), // function index = 0, using type index 0
  ]),

  export_section([
    // exports "factorial" as function at index 0
    export_entry(str_ascii("factorial"), external_kind.function, varuint32(0)),
  ]),

  code_section([
    // body of function at index 0:
    function_body([ /* additional local variables here */ ], [
      if_(i64, // i64 = result type of if expression
        i64.eq(get_local(i64, 0), i64.const(0)), // condition
        [ // then
          i64.const(1)
        ], [ // else
          i64.mul(
            get_local(i64, 0),
            call(i64, varuint32(0), [ // 0 = function index
              i64.sub(get_local(i64, 0), i64.const(1))
            ]))])])]
  )]
)

我们现在可以通过Emittable 接口生成WASM字节码:

const emitbuf = new BufferedEmitter(new ArrayBuffer(mod.z))
mod.emit(emitbuf)
// the array buffer (emitbuf.buffer) now contains the complete module code

或者打印人类可读的AST文本表示:

import { strRepr } from 'wasm-util/repr'
console.log(strRepr(mod))

将在控制台生成以下内容:

(module 13
  (section type 6 1
    (func_type (i64) i64))
  (section function 2 1 0)
  (section export 13 1
    (export_entry "factorial" external_kind.function 0))
  (section code 25 1
    (function_body 23 0
      (if [i64]
        (i64.eq
          (get_local [0])
          (i64.const [0])
        )
        (then
          (i64.const [1]))
        (else
          (i64.mul
            (get_local [0])
            (call [0]
              (i64.sub
                (get_local [0])
                (i64.const [1])
              )))) end) end)))

这个“factorial”演示的完整版本可以在wasm-util仓库的单元测试中找到。

使用wasm-util你可以毫不费力的开始构建自己的WebAssembly程序来玩玩。 也许你是编译语言的作者,并对WASM作为目标有兴趣,这种方式可能是一个很好的开始。

•••

如果这篇文章还不清楚的话,那么我直接给出我的观点:我相信WebAssembly有一个光明的未来。 Web是一个了不起的概念,但目前因为过去沉重的遗产而举步不前。为了给Web带来真正伟大的体验,模糊是不是‘本地(native)’应用的分界线,我们需要WebAssembly或类似于它的东西。

Figma,我们正在构建当今网络平台上最复杂和性能关键的应用程序之一,我们非常兴奋地从ASM.js 转移到WebAssembly。 你感兴趣吗? 我们正在招聘 :)

WASM作为中间表示( Intermediate Representation)

在我写这篇文章的时候,我逐渐发现WebAssembly可能是各种编译器的一种优秀IR(Intermediate Representation)。

中间表示是源和目标语言之间的程序部分方式的表示。良好的IR是相当独立于源语言和目标语言的IR,可以最大化其在可重定向编译器中的能力。2

  1. WebAssembly确实没有依赖于任何“web”,它的模块和字节码格式是如此底层,它可以用来表示任何其他更高级的程序。
  2. 有几个高质量的虚拟机正在实现,其中主要是Web浏览器,可以有效地执行WASM程序。
  3. WASM代码可以转换为机器代码。
  4. WASM可以被解释和模拟,这使得调试比起只运行本机的程序要轻松很多。我们如何stop the world和交互式检查堆栈?我们如何播放一个解释性记录,确切地再现错误?
相关文章