翻译:Marty Kalin翻译:疯狂的技术宅javascript
原文:https://opensource.com/articl...html
未经容许严禁转载前端
有这样一种技术,能够把用高级语言编写的非 Web 程序转换成为 Web 准备的二进制模块,而无需对 Web 程序的源代码进行任何更改便可完成这种转换。浏览器能够有效地下载新翻译的模块并在沙箱中执行。执行的 Web 模块能够与其余 Web 技术无缝地交互 - 特别是 JavaScript(JS)。欢迎来到WebAssembly。java
对于名称中带有 assembly 的语言,WebAssembly 是低级的。可是这种低级角色鼓励优化:浏览器虚拟机的即时(JIT)编译器能够将可移植的 WebAssembly 代码转换为快速的、特定于平台的机器代码。所以,WebAssembly 模块成为适用于计算绑定任务(例如数字运算)的可执行文件。node
有不少高级语言都能编译成 WebAssembly,并且这个名单正在增加,但最初的候选是C、C ++ 和 Rust。咱们将这三种称为系统语言,由于它们用于系统编程和高性能应用编程。系统语言都具备两个特性,这使它们适合被编译为 WebAssembly。下一节将详细介绍设置完整的代码示例(使用 C 和 TypeScript)以及来自 WebAssembly 本身的文本格式语言的示例。git
这三种系统语言须要显式数据类型,例如 int 和 double,用于变量声明和从函数返回的值。例如如下代码段说明了 C 中的 64 位加法:程序员
long n1 = random(); long n2 = random(); long sum = n1 + n2;
库函数 random 声明以 long 为返回类型:github
long random(); /* returns a long */
在编译过程当中,C 源被翻译成汇编语言,而后再将其翻译成机器代码。在英特尔汇编语言(AT&T flavor)中,上面的最后一个 C 语句的功能相似如下内容(## 为汇编语言的注释符号):web
addq %rax, %rdx ## %rax = %rax + %rdx (64-bit addition)
%rax 和 %rdx 是 64 位寄存器,addq 指令意味着 add quadwords,其中 quadword 是 64 位大小,这是 C 语言中 long 类型的标准大小。汇编语言强调可执行机器代码涉及类型,经过指令和参数的混合给出类型(若是有的话)。在这种状况下,add 指令是 addq(64 位加法),而不是例如 addl 这样的指令,它增长了 C 语言典型的 int 的 32 位值。使用的寄存器字长是完整的 64 位( %rax 和%rdx )而不是其 32 位的(例如,%eax 是 %rax 的低 32 位,%edx 是 %rdx 的低 32 位)。面试
汇编语言的效果很好,由于操做数被存储在 CPU 寄存器中,而合理的 C 编译器(即便是默认的优化级别)也会生成与此处所示相同的汇编代码。
这三种系统语言强调显式类型,是编译成 WebAssembly 的理想选择,由于这种语言也有明确的数据类型:i32 表示 32 位的整数值,f64 表示 64 位的浮点值,依此类推。
显式数据类型也鼓励优化函数调用。具备显式数据类型的函数具备 signature,它用于指定参数的数据类型以及从函数返回的值(若是有)。下面是名为$add 的 WebAssembly 函数的签名,该函数使用下面讨论的 WebAssembly 文本格式语言编写。该函数把两个 32 位的整数做为参数并返回一个 64 位的整数:
(func $add (param $lhs i32) (param $rhs i32) (result i64))
浏览器的 JIT 编译器应该具备 32 位的整数参数,并把返回的 64 位值存储在适当大小的寄存器中。
谈到高性能 Web 代码,WebAssembly 并非惟一的选择。例如,asm.js 是一种 JS 方言,与 WebAssembly 同样,能够接近原生速度。 asm.js 方言容许优化,由于代码模仿上述三种语言中的显式数据类型。这是 C 和 am.js 的例子。 C中的示例函数是:
int f(int n) { /** C **/ return n + 1; }
参数 n 和返回值都以 int 显式输入。asm.js 的等效函数是:
function f(n) { /** asm.js **/ n = n | 0; return (n + 1) | 0; }
一般,JS 没有显式数据类型,但 JS 中的按位或运算符可以产生一个整数值。这就解释了看上去毫无心义的按位或运算符:
n = n | 0; /* bitwise-OR of n and zero */
n 和 0 之间的按位或运算获得 n,但这里的目的是表示 n 保持整数值。 return 语句重复了这个优化技巧。
在 JS 方言中,TypeScript 在显式数据类型方面脱颖而出,这使得这种语言对于编译成 WebAssembly 颇有吸引力。 (下面的代码示例说明了这一点。)
三种系统语言都具备的第二个特性是它们在没有垃圾收集器(GC)的状况下执行。对于动态分配的内存,Rust 编译器会自动分配和释放代码;在其余两种系统语言中,动态分配内存的程序员负责显式释放内存。系统语言避免了自动化 GC 的开销和复杂性。
WebAssembly 的概述能够总结以下。几乎全部关于 WebAssembly 语言的文章都提到把近乎原生的速度做为语言的主要目标之一。 原生速度是指已编译的系统语言的速度,所以这三种语言也是最初被指定为编译成 WebAssembly 的候选者的缘由。
WebAssembly 语言并不是为了取代 JS,而是为了经过在计算绑定任务上提供更好的性能来补充 JS。WebAssembly 在下载方面也有优点。浏览器将 JS 模块做为文本提取,这正是 WebAssembly 可以解决的低效率问题之一。WebAssembly 中的模块是紧凑的二进制格式,可加快下载速度。
一样使人感兴趣的是 JS 和 WebAssembly 如何协同工做。 JS 旨在读入文档对象模型(DOM),即网页的树形表示。相比之下,WebAssembly 没有为 DOM 提供任何内置功能,可是 WebAssembly 能够导出 JS 根据须要调用的函数。这种关注点分离意味着清晰的分工:
DOM<----->JS<----->WebAssembly
不管用什么方言,JS 都应该管理 DOM,但 JS 也能够用经过 WebAssembly 模块提供的通用功能。代码示例有助于说明,本文中的代码案例能够在个人网站上找到(http://condor.depaul.edu/mkalin)。
生产级代码案例将使 WebAssembly 代码执行繁重的计算绑定任务,例如生成大型加密密钥对,或进行加密和解密。
考虑函数 hstone(对于hailstone),它以正整数做为参数。该函数定义以下:
3N + 1 if N is odd hstone(N) = N/2 if N is even
例如,hstone(12) 返回 6,而 hstone(11) 返回 34。若是 N 是奇数,则 3N + 1 是偶数;但若是 N 是偶数,则 N/2 能够是偶数(例如,4/2 = 2)或奇数(例如,6/2 = 3)。
hstone 函数能够经过将返回值做为下一个参数传递来进行迭代。结果是一个 hailstone 序列,例如这个序列,以 24 做为原始参数开始,返回值 12 做为下一个参数,依此类推:
24,12,6,3,10,5,16,8,4,2,1,4,2,1,...
序列收敛到 4,2,1 的序列无限重复须要 10 次调用:(3 x 1)+ 1 是 4,它除以 2 得 2,再除以 2 得 1。 Plus 杂志提供了为何把这些序列的称作 hailstone 的解释。
请注意,两个幂很快收敛,只须要 N 除以 2 获得 1;例如,32 = 25的收敛长度为5,64 = 26的收敛长度为6。这里感兴趣的是从初始参数到第一个出现的序列长度。我在 C 和 TypeScript 中的代码例子计算了冰雹序列的长度。
Collatz 猜测是一个冰雹序列会收敛到 1,不管初始值 N> 0 刚好是什么。没有人找到 Collatz 猜测的反例,也没有人找到证据将猜测提高到一个定理。这个猜测很简单,就像用程序测试同样,是数学中一个极具挑战性的问题。
下面的 hstoneCL 程序是一个非 Web 应用,可使用常规 C 语言编译器(例如,GNU 或 Clang)进行编译。程序生成一个随机整数值 N> 0 八次,并计算从 N 开始的冰雹序列的长度。两个程序员定义的函数,main 和 hstone 是有意义的。该应用程序稍后会被编译为 WebAssembly。
示例1. C 中的 hstone 函数
#include <stdio.h> #include <stdlib.h> #include <time.h> int hstone(int n) { int len = 0; while (1) { if (1 == n) break; /* halt on 1 */ if (0 == (n & 1)) n = n / 2; /* if n is even */ else n = (3 * n) + 1; /* if n is odd */ len++; /* increment counter */ } return len; } #define HowMany 8 int main() { srand(time(NULL)); /* seed random number generator */ int i; puts(" Num Steps to 1"); for (i = 0; i < HowMany; i++) { int num = rand() % 100 + 1; /* + 1 to avoid zero */ printf("%4i %7i\n", num, hstone(num)); } return 0; }
代码能够在任何类 Unix 系统上从命令行编译和运行(% 是命令行提示符):
% gcc -o hstoneCL hstoneCL.c ## compile into executable hstoneCL % ./hstoneCL ## execute
如下是例子运行的输出:
Num Steps to 1 88 17 1 0 20 7 41 109 80 9 84 9 94 105 34 13
系统语言(包括 C)须要专门的工具链才能将源代码转换为 WebAssembly 模块。对于 C/C++ 语言,Emscripten 是一个开创性且仍然普遍使用的选项,创建在众所周知的 LLVM (低级虚拟机)编译器基础结构之上。我在 C 语言中的示例使用 Emscripten,你能够[使用本指南进行安装(https://github.com/emscripten...)。
hstoneCL 程序能够经过使用 Emscription 编译代码进行 Web 化,而无需任何更改。Emscription工具链还与 JS glue(在asm.js中)一块儿建立一个HTML页面,该页面介于 DOM 和计算 hstone 函数的 WebAssembly 模块之间。如下是步骤:
% emcc hstoneCL.c -o hstone.html ## generates hstone.js and hstone.wasm as well
文件 hstoneCL.c 中包含上面显示的源代码,-o 输出标志用于指定 HTML 文件的名称。任何名称均可以,但生成的 JS 代码和 WebAssembly 二进制文件具备相同的名称(在本例中,分别为 hstone.js 和 hstone.wasm)。较旧版本的 Emscription(在13以前)可能须要将标志 -s WASM = 1 包含在编译命令中。
% emrun --no_browser --port 9876 . ## . is current working directory, any port number you like
要禁止显示警告消息,能够包含标志 --no_emrun_detect。此命令用于启动 Web 服务器,该服务器承载当前工做目录中的全部资源;特别是 hstone.html、hstone.js 和 hstone.webasm。
这个截图显示了我用 Firefox 运行的示例输出。
图1. web 化 hstone 程序
结果很是显著,由于完整的编译过程只须要一个命令,并且不须要对原始 C 程序进行任何更改。
Emscription工具链很好地将 C 程序编译成 WebAssembly 模块并生成所需的 JS 胶水,但这些是机器生成的典型代码。例如,生成的 asm.js 文件大小几乎为 100 KB。 JS 代码处理多个场景,而且不使用最新的 WebAssembly API。 webified hstone 程序的简化版本将使你更容易关注 WebAssembly 模块(位于 hstone.wasm 文件中)如何与 JS 胶水(位于 hstone.js 文件中)进行交互。
还有另外一个问题:WebAssembly 代码不须要镜像 C 等源程序中的功能边界。例如,C 程序 hstoneCL 有两个用户定义的函数,main 和 hstone。生成的 WebAssembly 模块导出名为 _ main 的函数,但不导出名为 _ hstone 的函数。 (值得注意的是,函数 main 是 C 程序中的入口点。)C 语言 hstone 函数的主体可能在某些未导出的函数中,或者只是包含在 _ main 中。导出的 WebAssembly 函数正是 JS glue 能够经过名称调用的函数。可是应在 WebAssembly 代码中按名称导出哪些源语言函数。
示例2. 修订后的 hstone 程序
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <emscripten/emscripten.h> int EMSCRIPTEN_KEEPALIVE hstone(int n) { int len = 0; while (1) { if (1 == n) break; /* halt on 1 */ if (0 == (n & 1)) n = n / 2; /* if n is even */ else n = (3 * n) + 1; /* if n is odd */ len++; /* increment counter */ } return len; }
如上所示,修改后的 hstoneWA 程序没有 main 函数,它再也不须要,由于该程序不是做为独立程序运行,而是仅做为具备单个导出函数的 WebAssembly 模块运行。指令 EMSCRIPTEN_KEEPALIVE(在头文件 emscripten.h 中定义)指示编译器在 WebAssembly 模块中导出 _ hstone 函数。命名约定很简单:诸如 hstone 之类的 C 函数保留其名称 —— 但在 WebAssembly 中使用单个下划线做为其第一个字符(在本例中为 _ hstone)。 WebAssembly中的其余编译器遵循不一样的命名约定。
要确认此方法是否有效,能够简化编译步骤,仅生成 WebAssembly 模块和 JS 粘合剂而不是 HTML:
% emcc hstoneWA.c -o hstone2.js ## we'll provide our own HTML file
HTML文件如今能够简化为这个手写的文件:
<!doctype html> <html> <head> <meta charset="utf-8"/> <script src="hstone2.js"></script> </head> <body/> </html>
HTML 文档加载 JS 文件,后者又获取并加载 WebAssembly 二进制文件 hstone2.wasm。顺便说一下,新的 WASM 文件大小只是原始例子的一半。
程序代码能够像之前同样编译,而后使用内置的Web服务器启动:
% emrun --no_browser --port 7777 . ## new port number for emphasis
在浏览器(在本例中为 Chrome)中请求修改后的 HTML 文档后,能够用浏览器的 Web 控制台确认 hstone 函数已导出为 _ hstone。如下是我在 Web 控制台中的会话段,## 为注释符号:
> _hstone(27) ## invoke _hstone by name < 111 ## output > _hstone(7) ## again < 16 ## output
EMSCRIPTEN_KEEPALIVE 指令是使 Emscripten 编译器生成 WebAssembly 模块的简单方法,该模块将全部感兴趣的函数导出到 JS 编程器一样产生的 JS 粘合剂。一个自定义的 HTML 文档,不管手写的 JS 是否合适,均可以调用从 WebAssembly 模块导出的函数。为了这个干净的方法,向 Emscripten 致敬。
下一个代码示例是 TypeScript,它是具备显式数据类型的 JS。该设置须要 Node.js 及其 npm 包管理器。如下 npm 命令安装 AssemblyScript,它是 TypeScript 代码的 WebAssembly 编译器:
% npm install -g assemblyscript ## install the AssemblyScript compiler
TypeScript 程序 hstone.ts 由单个函数组成,一样名为 hstone。如今数据类型如 i32(32位整数)紧跟参数和局部变量名称(在本例中分别为 n 和 len):
export function hstone(n: i32): i32 { // will be exported in WebAssembly let len: i32 = 0; while (true) { if (1 == n) break; // halt on 1 if (0 == (n & 1)) n = n / 2; // if n is even else n = (3 * n) + 1; // if n is odd len++; // increment counter } return len; }
函数 hstone 接受一个 i32 类型的参数,并返回相同类型的值。函数的主体与 C 语言示例中的主体基本相同。代码能够编译成 WebAssembly,以下所示:
% asc hstone.ts -o hstone.wasm ## compile a TypeScript file into WebAssembly
WASM 文件 hstone.wasm 的大小仅为14 KB。
要突出显示如何加载 WebAssembly 模块的详细信息,下面的手写 HTML 文件(个人网站上找到(http://condor.depaul.edu/mkalin)中的 index.html)包含如下脚本:获取并加载 WebAssembly 模块 hstone.wasm 而后实例化此模块,以即可以在浏览器控制台中调用导出的 hstone 函数进行确认。
示例 3. TypeScript 代码的 HTML页面
<!doctype html> <html> <head> <meta charset="utf-8"/> <script> fetch('hstone.wasm').then(response => <!-- Line 1 --> response.arrayBuffer() <!-- Line 2 --> ).then(bytes => <!-- Line 3 --> WebAssembly.instantiate(bytes, {imports: {}}) <!-- Line 4 --> ).then(results => { <!-- Line 5 --> window.hstone = results.instance.exports.hstone; <!-- Line 6 --> }); </script> </head> <body/> </html>
上面的 HTML 页面中的脚本元素能够逐行说明。第 1 行中的 fetch 调用使用 Fetch 模块从托管 HTML 页面的 Web 服务器获取 WebAssembly 模块。当 HTTP 响应到达时,WebAssembly 模块将把它作做为一个字节序列,它存储在脚本第 2 行的 arrayBuffer 中。这些字节构成了 WebAssembly 模块,它是从 TypeScript 编译的代码。文件。该模块没有导入,如第 4 行末尾所示。
在第 4 行的开头实例化 WebAssembly 模块。 WebAssembly 模块相似于非静态类,其中包含面向对象语言(如Java)中的非静态成员。该模块包含变量、函数和各类支持组件;可是与非静态类同样,模块必须实例化为可用,在本例中是在 Web 控制台中,但更常见的是在相应的 JS 粘合代码中。
脚本的第 6 行以相同的名称导出原始的 TypeScript 函数 hstone。此 WebAssembly 功能如今可用于任何 JS 粘合代码,由于在浏览器控制台中的另外一个会话将确认。
WebAssembly 具备更简洁的 API,用于获取和实例化模块。新 API 将上面的脚本简化为 fetch 和 instantiate 操做。这里展现的较长版本具备展现细节的好处,特别是将 WebAssembly 模块表示为字节数组,将其实例化为具备导出函数的对象。
计划是让网页以与 JS ES2015 模块相同的方式加载 WebAssembly 模块:
<script type='module'>...</script>
而后,JS 将获取、编译并以其余方式处理 WebAssembly 模块,就像是加载另外一个 JS 模块同样。
WebAssembly 二进制文件能够转换为 文本格式的等价物。二进制文件一般驻留在具备 WASM 扩展名的文件中,而其人类可读的文本副本驻留在具备 WAT 扩展名的文件中。 WABT 是一套用于处理 WebAssembly 的工具,其中包括用于转换为 WASM 和 WAT 格式的工具。转换工具包括 wasm2wat,wasm2c 和 wat2wasm 等。
文本格式语言采用 Lisp 推广的 S 表达式(S for symbolic)语法。 S 表达式(简称 sexpr)表示把树做为具备任意多个子列表的列表。例如这段 sexpr 出如今 TypeScript 示例的 WAT 文件末尾附近:
(export "hstone" (func $hstone)) ## export function $hstone by the name "hstone"
树表示是:
export ## root | +----+----+ | | "hstone" func ## left and right children | $hstone ## single child
在文本格式中,WebAssembly 模块是一个 sexpr,其第一项是模块,它是树的根。下面是一个定义和导出单个函数的模块的简单例子,该函数不带参数但返回常量 9876:
(module (func (result i32) (i32.const 9876) ) (export "simpleFunc" (func 0)) // 0 is the unnamed function's index )
该函数的定义没有名称(即做为 lambda),并经过引用其索引 0 导出,索引 0 是模块中第一个嵌套的 sexpr 的索引。导出名称以字符串形式给出;在当前状况下其名称为“simpleFunc”。
文本格式的函数具备标准模式,能够以下所示:
(func <signature> <local vars> <body>)
签名指定参数(若是有)和返回值(若是有)。例如,这是一个未命名函数的签名,它接受两个 32 位整数参数,返回一个 64 位整数值:
(func (param i32) (param i32) (result i64)...)
名称能够赋予函数、参数和局部变量。名称以美圆符号开头:
(func $foo (param $a1 i32) (param $a2 f32) (local $n1 f64)...)
WebAssembly 函数的主体反映了该语言的底层栈机器体系结构。栈存储用于暂存器。考虑一个函数的示例,该函数将其整数参数加倍并返回:
(func $doubleit (param $p i32) (result i32) get_local $p get_local $p i32.add)
每一个 get_local 操做均可以处理局部变量和参数,将 32 位整数参数压入栈。而后 i32.add 操做从栈中弹出前两个(当前惟一的)值以执行添加。最后 add 操做的和是栈上的惟一值,从而成为 $doubleit 函数的返回的值。
当 WebAssembly 代码转换为机器代码时,WebAssembly 栈做为暂存器应尽量由通用寄存器替换。这是 JIT 编译器的工做,它将 WebAssembly 虚拟栈机器代码转换为实际机器代码。
Web 程序员不太可能以文本格式编写 WebAssembly,由于从某些高级语言编译是一个很是有吸引力的选择。相比之下,编译器编的做者可能会发如今这种细粒度级别上工做是有效的。
WebAssembly 的目标是实现近乎原生的速度。但随着 JS 的 JIT 编译器不断改进,而且随着很是适合优化的方言(例如,TypeScript)的出现和发展,JS 也可能实现接近原生的速度。这是否意味着 WebAssembly 是在浪费精力?我想不是。
WebAssembly 解决了计算中的另外一个传统目标:有意义的代码重用。正如本文中的例子所示,使用适当语言(如 C 或 TypeScript)的代码能够轻松转换为 WebAssembly 模块,该模块能够很好地与 JS 代码一块儿使用 —— 这是链接 Web 中所使用的一系列技术的粘合剂。所以 WebAssembly 是重用遗留代码和扩展新代码使用的一种诱人方式。例如最初做为桌面应用的用于图像处理的高性能程序在 Web 应用中也多是有用的。而后 WebAssembly 成为重用的有吸引力的途径。 (对于计算限制的新 Web 模块,WebAssembly 是一个合理的选择。)个人预感是 WebAssembly 将在重用和性能方面茁壮成长。