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:{
consoleLognum => console .log(num)
}
})
那麼(m => {
m.add(53// 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);
forlet 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也是一門大學問,也有許多我沒有提及的工具可以使用(從資料來源中找得到)。

發佈了85 篇原創文章 · 獲贊 59 · 訪問量 31萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章