什麼是WebAssembly(wasm)?
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 等)等場合。<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 ++撰寫的加法函數:
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模塊整合的方向,也就是說以後能透過<script src="abc.wasm" type="module" />
的方式載入入。
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的要多加註意。
那到底怎麼將C / C ++編譯成wasm或wast呢?
WebAssembly.org中介紹我們使用Emscripten,Emscripten的安裝與使用方法大家可以從官網上看到,就不贅述。
安裝好後執行emcc add.c -s WASM=1 -o add.html
即可,唯一要注意的是WASM=1
這個標誌要設定,否則emcc
預設會跑asm.js.
如果只是想嚐鮮一下,可能看到要安裝這些東西就會把網頁關掉了......
不過不用擔心!現在也已經有很方便的在線工具可以使用:
WasmFiddle可以幫你把C代碼轉成Wast與Wasm(可下載),然後同時讓你直接利用JS進行操作,缺點是沒辦法直接更改Wast。
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必須透過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<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
先宣告一個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))) // ...函數內容省略,可參考前面的範例 ) |
難道只能從env載入嗎?
當然不是,我們也可以自己定義,但就要去更改wast檔案了,其實改過以後會發現邏輯不難懂,有讓我回味到大學修組語的感覺...
附加10-20.wast1 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.c1 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使用了!
現在我知道如何在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); |
可以讀取到記憶,當然也能寫入:
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 <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在Chrome與firefox都已實作,雖然一定還會有規格上的變更,但瞭解一下這個勢必會影響未來網絡開發的東西是有必要的!
本文也只是簡單介紹基礎的使用方法,實際上還有許多相關的議題,像是Type Arrays與WebAssembly Web API等等,都需要有所瞭解。甚至是如何將各種程式語言編成wasm也是一門大學問,也有許多我沒有提及的工具可以使用(從資料來源中找得到)。