WebAssembly的基础使用方法

什么是WebAssembly(wasm)?

WebAssembly或wasm是一种新的,便携式的,大小和加载时间效率高的格式,适合编译到Web上。WebAssembly设计

  1. 一种二进位表示的新语言,但有另外的文字格式可以让你编辑与调试。

  2. 编译目标:顾名思义,只要透过特定的编译器,你就能将你自己惯用的语言编译成WebAssembly,然后执行在浏览器上!目前可以透过Emscripten(LLVM to JS compiler)来编译C / C ++的程式。

  3. 提供增强JavaScript程式的方法:你可以将性能关键的程式部分用WebAssembly撰写,或是用第2点提及的C / C ++编译成WebAssembly,然后像一般import js module一般,导入你的JavaScript Application。透过WebAssembly,你能够自由控制Memory的存取与释放。

  4. 当浏览器能够支持运行WebAssembly的时候,由于二进位格式以及事先编译与优化的关系,势必能够产生比JavaScript运行速度更快,档案大小更小的结果。

  5. 语言的安全性WebAssembly当然也很重视,在JavaScript VM中,WebAssembly运行在一个沙箱化的执行环境,迁入web端运行时会强制使用浏览器的同源和权限安全策略。此外,wasm的实作设计中更特别提及他是内存安全的。

  6. 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 等)等场合。<br><br>于是那时的 Flash 横空出世。一个插件让 Web 的表现力提升了一大截:不仅自带矢量绘图,动态内容,多媒体内容甚至显卡 3D 加速也获得了支持。在那个 JavaScript 引擎的性能还很弱的年代,Flash 让无数开发者看到了希望。<br><br>然而随着 HTML 相关标准的不断完善和 JavaScript 引擎性能的突然提升,Flash 的优点没有那么突出了,而它的一大缺点却暴露了出来:二进制格式。长久以来 Flash 被滥用于提供(动态和静态)内容(我不信你们没看过整站用 Flash 做的网站;而 Flash 在设计的时候也没考虑过操作 DOM 的问题,毕竟人家自带一套用户界面),这样一来搜索引擎和通用的文本分析方案(例如浏览器的搜索功能)对它束手无策。而搜索引擎几乎已经成为 Web 内容提供的中心——它们提供到任何地方的超链接。于是,在 JavaScript 引擎的效率已经相当可观的今天,Flash 不灵了。

当然了,二进制格式有其好处:相对文本格式更轻量,在互联网上传输的成本更低,解释效率也更高(如果设计得当的话)。所以 WebAssembly 最终选择了一个妥协的方案:它要求一个程序段具有两种可互相转换的等价表达:二进制格式和文本格式。这二者可理解为类似机器码和汇编的关系:传输和运行的时候使用二进制格式,展现给人的时候用文本格式。这样就同时保留了二者的优点。当然了,为了安全性,要求以文本格式传输的程序不可被执行。

不过,由于代码混淆和压缩技术的广泛应用,WebAssembly 的这一设计意图最终不容易达到预想中的效果吧。

二. 私有技术和平台兼容性问题

Flash 的诸多漏洞(包括一堆 0day)让人们意识到:让一个公司的私有技术主导 Web 并不是什么好主意。新的硬件平台?对不起,不支持。有 bug?等人家更新吧,你什么也干不了。高发热?对不起,你别无选择。没有权限安装浏览器插件?抱歉,你就别用了。不仅仅是 Flash,Silverlight,ActiveX 插件等也是同样的境地。

如果轮子不好用,那么自己造一个。我们需要开源的标准。

三. 可执行代码的安全性问题

这是黑暗森林法则的一个推论:我们不能信任任何人。网站不应该相信用户的输入是无害的;同样,用户也不应该相信网站提供的内容是无害的,尤其在这些内容会被在本地执行的时候。长期以来,我们给了传统浏览器插件(Plug-in)太多的权力,而事实证明它们中的一部分正在有效地利用用户给他们的所有权力(说你呢,支付宝)。

用户应当有权利掌控他们的设备。

不过呢,WebAssembly 将面临新的挑战:一个全新的体系必将带来更多的安全问题。

四. 性能

WebAssembly 将是一个编译型语言。它的<a href="https://link.zhihu.com/?target=https://github.com/WebAssembly/design/blob/master/HighLevelGoals.md" class=" wrap external" target="_blank" rel="nofollow noreferrer">设计目标</a>描述了一个美好的未来:

定义一个可移植,体积紧凑,加载迅捷的二进制格式为编译目标,而此二进制格式文件将可以在各种平台(包括移动设备和物联网设备)上被编译,然后发挥通用的硬件性能以原生应用的速度运行。

五. 远景

如果 WebAssembly 不出现,则 HTML,CSS,JavaScript 必将成为前端界的事实汇编语言:人们不断创造更多的(他们认为更好的)对这三者的高级(high-level)描述形式,并最后以这三者作为“编译目标”。WebAssembly 的出现则提供了一个更好的选择:接近原生的运算效率,开源、兼容性好、平台覆盖广的标准,以及可以借此机会抛弃 JavaScript 的历史遗留问题。何乐而不为呢?

等等,第一点就有问题了,你说他是二进位表示的语言,那该怎么写?!text format又是长什么样子?

问得好,这就是本篇的重点,WebAssembly的档案格式为wasm,举一个例子来看,一个用c ++撰写的加法函数:

add.c
 
     
1
2
3
4
 
     
#include <math.h>
int add int num1,int num2) {
return num1 + num2;
}

若编译为wasm会长这个样子(为节省空间我转成十六进制):

add.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后:

add.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模块整合的方向,也就是说以后能透过<script src="abc.wasm" type="module" />的方式载入入。

line 2 ~ 3分别宣告了两个预设的环境变量:memorytable,memory是就存储变量的记忆体物件,而table则是WebAssembly用来存放函数引用的地方,在目前MVP的版本中,table的元素类型只能为anyfunc

接着line 4 ~ 5把记忆与add function export出去。之后在JavaScript中,我们可以取得这两个被导出出来的物件与函式。

最后是加法函式的宣告与实作内容,其中get_local是WebAssembly中取得记忆本地变数的方法。

不知道会不会有人好奇i32是什么?

i32指的就是32位元的整数,在WebAssembly的世界中,是强型态的,必须明确指定变数型态,写习惯JS的要多加注意。

那到底怎么将C / C ++编译成wasm或wast呢?

WebAssembly.org中介绍我们使用Emscripten,Emscripten的安装与使用方法大家可以从官网上看到,就不赘述。

安装好后执行emcc add.c -s WASM=1 -o add.html即可,唯一要注意的是WASM=1这个标志要设定,否则emcc预设会跑asm.js.

如果只是想尝鲜一下,可能看到要安装这些东西就会把网页关掉了......

不过不用担心!现在也已经有很方便的在线工具可以使用:

WasmFiddle

WasmFiddle

WasmFiddle可以帮你把C代码转成Wast与Wasm(可下载),然后同时让你直接利用JS进行操作,缺点是没办法直接更改Wast。

WasmExplorer

wasmExplorer

WasmExplorer一样能帮你把C代码编译成Wast与Wasm,并且可以编辑转出来的Wast,缺点是没有JS能直接互动。

所以搭配操作的流程...

先WasmFiddle来进行测试,接着把编好的Wast复制到WasmExplorer进行你想要的编辑,接着再编成wasm并下载下来。

知道怎么编译wasm后,该说说JavaScript了吧

好的,但在那之前,要先提醒大家,除了Chrome 57,Firefox 52预设支援WebAssembly外,Safari需要是紫色版本(Preview版)才能使用,而Edge 15则是要开启JavaScript实验功能。

载入wasm到Web端

<script src="abc.wasm" type="module" />还无法使用之前,想要载入wasm必须透过fetchAPI。在Guy bedford的影片范例mdn的例子中的写法都差不多:

WASM-loader.js
 
     
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中导出出来的物件或函数。

除了以外fetchWebAssembly.compileWebAssembly.instantiate也都是回传Promise。

这边出现一个相信一般前端开发者也比较少看到的ArrayBuffer

ArrayBuffer是JavaScript的一种数据类型,用来表示通用的,固定长度的二进制数据缓冲区,属于typed arrays的一部分,而关于typed arrays虽然在WebAssembly中很重要,但是难以在这边详述,mdn的文件写得很清楚,值得阅读。

我们目前只要知道他是一个array-like的物件,让我们能在JavaScript中存取raw binary dat?a,有Int8ArrayInt32ArrayFloat32ArrayDataView可以使用即可。(又一个名词... DataView提供getter / setter API来对缓冲中的数据做读取。)

回到主题,如果你刚刚有先点进mdn的例子,可能会发现他怎么没有WebAssembly.compile这个步骤?

实际上WebAssembly.instantiate有两种超载实作:

  • Promise<ResultObject> WebAssembly.instantiate(bufferSource, importObject);
  • Promise<WebAssembly.Instance> WebAssembly.instantiate(module, importObject);

WebAssembly.compile差别在于,先透过后产生的WebAssembly模块,可以存在indexedDB中缓存,或是在web workers之间传递。

此外,WebAssembly.Instance的第二个参数:importObject是用来传递JavaScript的参数或函数到WebAssembly程序中使用,后面会有范例。

在JavaScript中使用WebAssembly实作的函数

有了刚刚的fetchAndInstantiateWasm,取得WebAssembly function很方便:

 
     
1
2
3
4
 
     
 
     
fetchAndInstantiateWasm( 'add.wasm', {})
.then( m => {
console.log(m.add( 5, 10)); // 15
});

使用上就是这么简单!

那能不能在WebAssembly中使用JavaScript写的函数呢?

当然可以!就是透过方才所说的第二个参数importObject

假设我们想要在刚刚的加法函数内进行JS的console.log

add.c

 
 
#include <math.h>
void consoleLog (int num);
int add(int num1, int num2) {
int result = num1 + num2;
consoleLog(result);
return result;
}

先宣告一个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):

add.wast
 
     
1
2
3
4
5
 
     
 
      
 
       
(module
(type $FUNCSIG$vi (func (param i32)))
(import "env" "consoleLog" (func $consoleLog (param i32)))
// ...函數內容省略,可參考前面的範例
)

难道只能从env载入吗?

当然不是,我们也可以自己定义,但就要去更改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
 
     
 
      
#include <math.h>
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测试

现在我知道如何在JS与WebAssembly中互相使用函式了,但前面好像有提到他还能让你操作Memory ?!

前面范例中的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 writeStringstr,offset {
const strBuf = new TextEncoder()。encode(str);
const outBuf = new Uint8Array (mem.buffer,offset,strBuf.length);
for let i = 0 ; i <strBuf.length; i ++){
outBuf [i] = strBuf [i];
}
}

对于Memory的操作部分,Guy Bedford的范例有更多介绍,包含怎么搭配malloc来动态调整记忆体。

WebAssembly对于效能的展现似乎到目前为止都没有触及耶?

要能够展现JavaScript与WebAssembly的效能差异其实没有那么简单,Guy Bedford在影片中的范例是在萤幕上画出多个圆圈,计算他们之间碰撞的状况来移动,有趣的是,第一次的Demo中,JavaScript的速度比WebAssembly实现碰撞计算的要快得多,然而在重新优化演算法后,才让WebAssembly的效能有大幅进展,比起JavaScript好上不少(同样演算法)

这边放个动态截图给大家看,想自己跑跑看或是看程式码的可以移动Guy Bedford的回购 - Wasm Demo,载下来直接就能打开html执行啰!(要执行这个Demo需要Chrome Canary并在chrome:// flags中启动Experimental Web Platform Flag)

Wasm VS JS

结论

目前wasm在Chrome与firefox都已实作,虽然一定还会有规格上的变更,但了解一下这个势必会影响未来网络开发的东西是有必要的!

本文也只是简单介绍基础的使用方法,实际上还有许多相关的议题,像是Type ArraysWebAssembly Web API等等,都需要有所了解。甚至是如何将各种程式语言编成wasm也是一门大学问,也有许多我没有提及的工具可以使用(从资料来源中找得到)。

猜你喜欢

转载自blog.csdn.net/sinat_34070003/article/details/80291178