WebAssembly或wasm是一种新的,便携式的,大小和加载时间效率高的格式,适合编译到Web上。- WebAssembly设计
一种二进位表示的新语言,但有另外的文字格式可以让你编辑与调试。
编译目标:顾名思义,只要透过特定的编译器,你就能将你自己惯用的语言编译成WebAssembly,然后执行在浏览器上!目前可以透过Emscripten(LLVM to JS compiler)来编译C / C ++的程式。
提供增强JavaScript程式的方法:你可以将性能关键的程式部分用WebAssembly撰写,或是用第2点提及的C / C ++编译成WebAssembly,然后像一般import js module一般,导入你的JavaScript Application。透过WebAssembly,你能够自由控制Memory的存取与释放。
当浏览器能够支持运行WebAssembly的时候,由于二进位格式以及事先编译与优化的关系,势必能够产生比JavaScript运行速度更快,档案大小更小的结果。
语言的安全性WebAssembly当然也很重视,在JavaScript VM中,WebAssembly运行在一个沙箱化的执行环境,迁入web端运行时会强制使用浏览器的同源和权限安全策略。此外,wasm的实作设计中更特别提及他是内存安全的。
Non-Web Embeddings:虽然是为了Web设计,但也希望能在其他环境中运行,因此底层实作并没有要求Web API,让其拥有良好的可移植性,不管是Nodejs,IoT设备都可使用。
WebAssembly目前由W3C Community Group设计开发,成员包含所有主流浏览器的代表。
WebAssembly有许多高级目标,目前版本的主要为MVP(Minimum Viable Product),提供先前asm.js
的多数功能,并以先C / C ++的编译为主。
WebAssembly 主要试图解决现有技术的一些问题:
JavaScript:性能不够理想,以及语言本身的一堆坑(这个大家都懂)
Flash:私有技术(而且漏洞一堆),并且是纯二进制格式
Silverlight:私有技术,并且是纯二进制格式
各种插件(Plug-in):安全性问题,平台兼容问题
JavaScript 的坑我想我不用讲了吧,这里随便拉个人出来都比我讲得好。重点解释一下 WebAssembly 设计过程中考虑到的其它几个方面:
一. 二进制格式
Web 的基础是超文本(Hypertext),即包含超链接(Hyperlink)的文本(字符串)。这个特性使人能读懂,机器也容易分析。因此,和这个理念相符的技术往往在 Web 方向上有着更大的可能性被广泛应用——不说别的,就说后端语言吧,现在烂大街的后端语言哪个处理字符串不方便?要是都像 C 这样 char* 满天飞,现在后端工程师的工资估计得乘个 10。
不过呢,早期这一设计的确限制了 Web 表现力的发展。那时候标准混乱,浏览器们各自为政,基于有特定功能的 tag 的 HTML 的用途极为有限,尤其是当时尚未出现或完善的动态内容(XHR),多媒体内容(canvas, audio, video)以及高性能运算(WebGL,asm.js 等)等场合。
于是那时的 Flash 横空出世。一个插件让 Web 的表现力提升了一大截:不仅自带矢量绘图,动态内容,多媒体内容甚至显卡 3D 加速也获得了支持。在那个 JavaScript 引擎的性能还很弱的年代,Flash 让无数开发者看到了希望。
然而随着 HTML 相关标准的不断完善和 JavaScript 引擎性能的突然提升,Flash 的优点没有那么突出了,而它的一大缺点却暴露了出来:二进制格式。长久以来 Flash 被滥用于提供(动态和静态)内容(我不信你们没看过整站用 Flash 做的网站;而 Flash 在设计的时候也没考虑过操作 DOM 的问题,毕竟人家自带一套用户界面),这样一来搜索引擎和通用的文本分析方案(例如浏览器的搜索功能)对它束手无策。而搜索引擎几乎已经成为 Web 内容提供的中心——它们提供到任何地方的超链接。于是,在 JavaScript 引擎的效率已经相当可观的今天,Flash 不灵了。
当然了,二进制格式有其好处:相对文本格式更轻量,在互联网上传输的成本更低,解释效率也更高(如果设计得当的话)。所以 WebAssembly 最终选择了一个妥协的方案:它要求一个程序段具有两种可互相转换的等价表达:二进制格式和文本格式。这二者可理解为类似机器码和汇编的关系:传输和运行的时候使用二进制格式,展现给人的时候用文本格式。这样就同时保留了二者的优点。当然了,为了安全性,要求以文本格式传输的程序不可被执行。
不过,由于代码混淆和压缩技术的广泛应用,WebAssembly 的这一设计意图最终不容易达到预想中的效果吧。
二. 私有技术和平台兼容性问题
Flash 的诸多漏洞(包括一堆 0day)让人们意识到:让一个公司的私有技术主导 Web 并不是什么好主意。新的硬件平台?对不起,不支持。有 bug?等人家更新吧,你什么也干不了。高发热?对不起,你别无选择。没有权限安装浏览器插件?抱歉,你就别用了。不仅仅是 Flash,Silverlight,ActiveX 插件等也是同样的境地。
如果轮子不好用,那么自己造一个。我们需要开源的标准。
三. 可执行代码的安全性问题
这是黑暗森林法则的一个推论:我们不能信任任何人。网站不应该相信用户的输入是无害的;同样,用户也不应该相信网站提供的内容是无害的,尤其在这些内容会被在本地执行的时候。长期以来,我们给了传统浏览器插件(Plug-in)太多的权力,而事实证明它们中的一部分正在有效地利用用户给他们的所有权力(说你呢,支付宝)。
用户应当有权利掌控他们的设备。
不过呢,WebAssembly 将面临新的挑战:一个全新的体系必将带来更多的安全问题。
四. 性能
WebAssembly 将是一个编译型语言。它的设计目标描述了一个美好的未来:
定义一个可移植,体积紧凑,加载迅捷的二进制格式为编译目标,而此二进制格式文件将可以在各种平台(包括移动设备和物联网设备)上被编译,然后发挥通用的硬件性能以原生应用的速度运行。
五. 远景
如果 WebAssembly 不出现,则 HTML,CSS,JavaScript 必将成为前端界的事实汇编语言:人们不断创造更多的(他们认为更好的)对这三者的高级(high-level)描述形式,并最后以这三者作为“编译目标”。WebAssembly 的出现则提供了一个更好的选择:接近原生的运算效率,开源、兼容性好、平台覆盖广的标准,以及可以借此机会抛弃 JavaScript 的历史遗留问题。何乐而不为呢?
问得好,这就是本篇的重点,WebAssembly的档案格式为wasm
,举一个例子来看,一个用c ++撰写的加法函数:
1
2
3
4
|
int add (int num1,int num2)
{
return
num1 + num2;
}
|
若编译为wasm
会长这个样子(为节省空间我转成十六进制):
1
2
3
4
6
7
|
00 61 73 6d 01 00 00 00 01 87 80 80 80 00 01 60
02 7f 7f 01 7f 03 82 80 80 80 00 01 00 04 84 80
80 80 00 01 70 00 00 05 83 80 80 80 00 01 00 01
06 81 80 80 80 00 00 07 95 80 80 80 00 02 06 6d
65 6d 6f 72 79 02 00 08 5f 5a 33 61 64 64 69 69
00 00 0a 8d 80 80 80 00 01 87 80 80 80 00 00 20
01 20 00 6a 0b
|
当然我们很难去编辑这样的东西,所以有另一种text format
叫做wast
,上述的.wasm转成.wast后:
1
2
3
4
5
6
7
8
9
10
11
12
|
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "add" (func $add))
(func $add (param $0 i32) (param $1 i32) (result i32)
(i32.add
(get_local $1)
(get_local $0)
)
)
)
|
这样就好懂多了,我们一行一行来解释:
line 1
的模块就是WebAssembly中一个可载入,可执行的最小单位程式,在运行时载入后可以产生实例来执行,而这个模块也朝着与ES6模块整合的方向,也就是说以后能透过的方式载入入。
line 2 ~ 3
分别宣告了两个预设的环境变量:memory
与table
,memory是就存储变量的记忆体物件,而table则是WebAssembly用来存放函数引用的地方,在目前MVP的版本中,table的元素类型只能为anyfunc
。
接着line 4 ~ 5
把记忆与add function export出去。之后在JavaScript中,我们可以取得这两个被导出出来的物件与函式。
最后是加法函式的宣告与实作内容,其中get_local
是WebAssembly中取得记忆本地变数的方法。
不知道会不会有人好奇i32是什么?
i32指的就是32位元的整数,在WebAssembly的世界中,是强型态的,必须明确指定变数型态,写习惯JS的要多加注意。
WebAssembly.org中介绍我们使用Emscripten,Emscripten的安装与使用方法大家可以从官网上看到,就不赘述。
安装好后执行emcc add.c -s WASM=1 -o add.html
即可,唯一要注意的是WASM=1
这个标志要设定,否则emcc
预设会跑asm.js.
如果只是想尝鲜一下,可能看到要安装这些东西就会把网页关掉了......
不过不用担心!现在也已经有很方便的在线工具可以使用:
WasmFiddle
WasmFiddle可以帮你把C代码转成Wast与Wasm(可下载),然后同时让你直接利用JS进行操作,缺点是没办法直接更改Wast。
WasmExplorer:
WasmExplorer一样能帮你把C代码编译成Wast与Wasm,并且可以编辑转出来的Wast,缺点是没有JS能直接互动。
先WasmFiddle来进行测试,接着把编好的Wast复制到WasmExplorer进行你想要的编辑,接着再编成wasm并下载下来。
好的,但在那之前,要先提醒大家,除了Chrome 57,Firefox 52预设支援WebAssembly外,Safari需要是紫色版本(Preview版)才能使用,而Edge 15则是要开启JavaScript实验功能。
在还无法使用之前,想要载入wasm必须透过
fetch
API。在Guy bedford的影片范例与mdn的例子中的写法都差不多:
1
2
3
4
五
6
7
8
9
10
11
|
function fetchAndInstantiateWasm (url, imports) {
return fetch(url)
// url could be your .wasm file
.then(
res => {
if (res.ok)
return res.arrayBuffer();
throw
new
Error(
`Unable to fetch Web Assembly file ${url}.`);
})
.then(
bytes => WebAssembly.compile(bytes))
.then(
module => WebAssembly.instantiate(
module, imports || {}))
.then(
instance => instance.exports);
}
|
会基本上实动词}一个wasm-loader
之类的函式,像上面的fetchAndInstantiateWasm
。
内容很简单,取得fetch回来的结果后,将其转为ArrayBuffer
,利用WebAssembly.compile
这个Web API来产生WebAssembly模块,接着透过WebAssembly.instantiate
来产生模块实例,最后的instance.exports就是我们在wasm中导出出来的物件或函数。
除了以外fetch
,WebAssembly.compile
与WebAssembly.instantiate
也都是回传Promise。
这边出现一个相信一般前端开发者也比较少看到的ArrayBuffer。
ArrayBuffer是JavaScript的一种数据类型,用来表示通用的,固定长度的二进制数据缓冲区,属于typed arrays的一部分,而关于typed arrays虽然在WebAssembly中很重要,但是难以在这边详述,mdn的文件写得很清楚,值得阅读。
我们目前只要知道他是一个array-like的物件,让我们能在JavaScript中存取raw binary dat?a,有Int8Array
,Int32Array
与Float32Array
等DataView可以使用即可。(又一个名词... DataView提供getter / setter API来对缓冲中的数据做读取。)
回到主题,如果你刚刚有先点进mdn的例子,可能会发现他怎么没有WebAssembly.compile
这个步骤?
实际上WebAssembly.instantiate
有两种超载实作:
Promise WebAssembly.instantiate(bufferSource, importObject);
Promise WebAssembly.instantiate(module, importObject);
WebAssembly.compile
差别在于,先透过后产生的WebAssembly模块,可以存在indexedDB中缓存,或是在web workers之间传递。
此外,WebAssembly.Instance的第二个参数:importObject
是用来传递JavaScript的参数或函数到WebAssembly程序中使用,后面会有范例。
有了刚刚的fetchAndInstantiateWasm
,取得WebAssembly function很方便:
1
2
3
4
|
fetchAndInstantiateWasm(
'add.wasm', {})
.then(
m => {
console.log(m.add(
5,
10));
// 15
});
|
使用上就是这么简单!
当然可以!就是透过方才所说的第二个参数importObject
。
假设我们想要在刚刚的加法函数内进行JS的console.log
:
add.c
先宣告一个consoleLog
函式,并不需要实作他,因为这会是我们待会要从JavaScript那边import进来的部分:
1
2
3
4
五
6
7
8
|
fetchAndInstantiateWasm(
'./add.wasm'
,{
env
:{
consoleLog
:
num =>
console
.log(num)
}
})
那么(
m =>
{
m.add(
5
,
3
)
// 8的console.log
});
|
在刚刚的fetchAndInstantiateWasm
第二个参数中,我们定义一个env
对象,并传入一个内部console.log的函数。env
是一个特殊的key,在刚刚的add.c当中,我们宣告的void consoleLog (int num)
转换到add.wast时,会他当作这个函式的英文从env
中进口进入的(线2):
1
2
3
4
5
|
(module
(type $FUNCSIG$vi (func (param i32)))
(import "env" "consoleLog" (func $consoleLog (param i32)))
// ...函數內容省略,可參考前面的範例
)
|
当然不是,我们也可以自己定义,但就要去更改wast档案了,其实改过以后会发现逻辑不难懂,有让我回味到大学修组语的感觉...
附加10-20.wast
1
2
3
4
五
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
(module
(type $FUNCSIG$vi (func (param i32)))
(import "env" "consoleLog" (func $consoleLog (param i32)))
++(import "lib" "log" (func $log (param i32)))
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "add" (func $add))
(func $add (param $0 i32) (param $1 i32) (result i32)
(call $consoleLog // 從 env 中載入的 consoleLog
++(i32.add
(tee_local $1
(i32.add
(get_local $1)
(get_local $0)
)
)
++(i32.const 20) // 從 env 載入的 consoleLog 多加 20
)
)
++(call $log // 從我們自己定義的 lib 中載入的 log
++(i32.add
++(get_local $1)
++(i32.const 10) // 從 env 載入的 consoleLog 多加 10
++)
++)
(get_local $1)
)
)
|
前面有加号的就是我们直接在wast中修改的程式码,等同于如下C语言的程式:
add.c
1
2
3
4
5
6
7
8
|
void consoleLog (int num);
int add(int num1, int num2) {
int result = num1 + num2;
consoleLog(result +
20);
log(result +
10);
// 多了這個從 lib 匯入的 log 函數
return result;
}
|
如此一来,我们就能够像下面这般传递lib.log
给我们的wasm使用了!
在jsbin.com上进行WASM测试
前面范例中的wast都将将内存导出出来:(export "memory" (memory $0))
我们可以利用前面提及的JavaScript Typed Array来取内存缓冲区,并利用TextDecoder这个较新的Web API来解码:
1
2
3
4
|
const
memory = wasmModule.memory;
const
strBuf =
new
Uint8Array
(memory.buffer,wasmModule.getStrOffset(),
11
);
const
str =
new
TextDecoder()。decode(strBuf);
console
.log(str);
|
JS Bin在jsbin.com上
可以读取到记忆,当然也能写入:
1
2
3
4
五
6
7
|
function writeString(str,offset)
{
const
strBuf =
new
TextEncoder()。encode(str);
const
outBuf =
new
Uint8Array
(mem.buffer,offset,strBuf.length);
for
(
let
i =
0
; i
outBuf [i] = strBuf [i];
}
}
|
对于Memory的操作部分,Guy Bedford的范例有更多介绍,包含怎么搭配malloc
来动态调整记忆体。
要能够展现JavaScript与WebAssembly的效能差异其实没有那么简单,Guy Bedford在影片中的范例是在萤幕上画出多个圆圈,计算他们之间碰撞的状况来移动,有趣的是,第一次的Demo中,JavaScript的速度比WebAssembly实现碰撞计算的要快得多,然而在重新优化演算法后,才让WebAssembly的效能有大幅进展,比起JavaScript好上不少(同样演算法)
这边放个动态截图给大家看,想自己跑跑看或是看程式码的可以移动Guy Bedford的回购 - Wasm Demo,载下来直接就能打开html执行啰!(要执行这个Demo需要Chrome Canary并在chrome:// flags中启动Experimental Web Platform Flag)
目前wasm在Chrome与firefox都已实作,虽然一定还会有规格上的变更,但了解一下这个势必会影响未来网络开发的东西是有必要的!
本文也只是简单介绍基础的使用方法,实际上还有许多相关的议题,像是Type Arrays与WebAssembly Web API等等,都需要有所了解。甚至是如何将各种程式语言编成wasm也是一门大学问,也有许多我没有提及的工具可以使用(从资料来源中找得到)。