本文是圖說 WebAssembly 系列文章的第四篇。若是您還未閱讀以前的文章,建議您從第一篇入手。前端
WebAssembly 是一種使得除 JavaScript 之外的編程語言也能運行在網頁上的技術。
在過去,當咱們須要經過編程來控制網頁內容時,咱們的選擇只有 JavaScript 。webpack
因此當你們都說 WebAssembly 運行速度很快時,其實它的比較對象就是指 JavaScript 。
不過這並不意味着你只能使用 JavaScript 和 WebAssembly 中的一種。
反而,更推薦的作法是同時使用它們。即使是你不寫 WebAssembly ,你也是能夠從它身上得到好處的。git
WebAssembly 模塊定義了能夠被 JavaScript 調用的函數。
就像咱們如今能夠直接從 npm 下載 lodash 模塊並調用其接口同樣,將來咱們也能夠下載 WebAssembly 模塊並使用它。github
因此,今天咱們來看看如何建立 WebAssembly 模塊,以及如何使用 JavaScript 調用它。web
在上一篇文章中,咱們介紹了編譯器如何把高級語言編譯爲機器碼。npm
在上圖中,WebAssembly 對應哪一個角色呢?編程
聰明的你可能已經想到,它只不過是另外一種目標彙編語言而已。
從某種意義上來講,這種想法是對的,只不過圖中的 x8六、ARM 等其實對應的是一種特定的計算機架構。segmentfault
對於開發者來講,他所開發的代碼是但願可以運行在互聯網上全部用戶機器上的,可是他其實並不知道運行這些代碼的機器屬於哪一種架構。後端
因此 WebAssembly 跟彙編相比仍是有略微不一樣之處。
它面向的是一種概念上機器的機器語言,而不是一種真實存在的物理機器。數組
這也就致使了 WebAssembly 指令是一種虛擬指令。
與 JavaScript 源碼相比,虛擬指令跟機器碼的映射來得更爲直接。
它們表示一種能夠在廣泛流行機器上高效使用的指令集合。但同時它們也不會直接映射到特定的機器碼。
瀏覽器會下載 WebAssembly,而後把它變成目標機器的彙編。
目前對 WebAssembly 支持最多的編譯器工具鏈稱爲 LLVM 。有不少不一樣的編譯器前端和後端都在使用 LLVM 。
注意: 大多數的 WebAssembly 模塊開發者都會使用 C 和 Rust 這樣的語言,而後編譯爲 WebAssembly,可是也有其餘方式建立 WebAssembly 模塊。好比,有一個 實驗工具能夠把 TypeScript 編譯爲 WebAssembly 模塊,更有甚者,
能夠 直接手寫 WebAssembly 。
這裏,假如咱們想把 C 編譯爲 WebAssembly 。
咱們可使用 C 語言編譯器前端把 C 代碼編譯爲 LLVM 中間代碼。一旦變成 LLVM 的中間代碼,LLVM 就能夠理解並分析代碼,而後作一些優化。
爲了把 LLVM 中間代碼變成 WebAssembly,咱們還須要一個編譯器後端。恰好,LLVM 項目中確實有一個正在開發編譯器後端,將來它應該是大部分人的共同選擇,並且應該很快就要完成了。不過,如今用它的話仍是至關棘手。
不過不用灰心,還有另外一個工具稱爲 Emscripten,目前用起來會更加簡單點。
它擁有本身編譯器後端,能夠把中間代碼編譯爲 asm.js ,進而轉化爲 WebAssembly 。
不過它也支持 LLVM,所以咱們也能夠在 Emscripten 和其餘後端之間相互切換。
Emscripten 還包含了不少其餘工具和庫,容許開發者移植整個 C/C++ 代碼,所以與其說它是編譯器,其實它更像是軟件開發套件(SDK)。
無論用什麼工具鏈,最終的結果都是獲得一個 .wasm
文件。後面咱們會介紹 .wasm
文件的結構,不過首先讓咱們來看看如何在 JavaScript 中使用它。
.wasm
文件就是 WebAssembly 模塊,它能夠直接使用 JavaScript 加載。
截止到目前,這種加載方式略微複雜。
function fetchAndInstantiate(url, importObject) { return fetch(url).then(res => res.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, importObject)) .then(results => results.instance); }
想深刻的話,能夠參考這個 MDN 文檔
咱們正在努力把這個過程變得更加簡單。咱們也但願可以把工具鏈變得更加友好,但願可以直接集成到諸如 webpack 或者 SystemJS 等打包器中。相信將來 WebAssembly 模塊能夠跟加載 JavaScript 模塊同樣簡單好用。
不過,WebAssembly 模塊和 JavaScript 模塊之間有一個主要的不一樣之處。
當前,WebAssembly 模塊中的函數只能使用數字做爲參數或者返回值。
對於其餘任何更復雜的數據類型,如字符串,咱們必須直接操做 WebAssembly 模塊的內存。
若是你大部分的時間都在使用 JavaScript,那麼你可能對直接操做內容不太熟悉。
像 C、C++ 和 Rust 這些高性能的語言,它們都必須手動管理內存。
WebAssembly 模塊的內存就模擬了這些語言的堆內存。
爲了可以操做內存,咱們須要使用 JavaScript 中的 ArrayBuffer
。
它是字節數組,因此它的索引當作內存地址來使用。
若是想要在 JavaScript 和 WebAssembly 之間傳遞字符串,那麼必須先把字符串轉爲等效的字符碼,而後寫入 ArrayBuffer
。因爲數組索引是整數,因此索引能夠傳遞給 WebAssembly 函數。這樣,索引就變成了指向字符串首個字符的指針了。
不過大部分狀況下,WebAssembly 模塊開發者都會把模塊作友好地封裝。此時,模塊的使用者可能就不必知道其內部是如何管理內存的了。
若是你對內存管理感興趣,能夠查看 MDN 文檔
若是你編程使用的是高級語言而後編譯爲 WebAssembly,那其實你不必瞭解 WebAssembly 模塊的結構,不過它能夠幫你理解基礎信息。
下面是一個 C 函數,咱們將把它編譯爲 WebAssembly 。
int add42(int num) { return num + 42; }
你可使用 WasmExplorer來編譯這個函數。
打開編譯好的 .wasm
文件後,咱們可能會看到相似如下的內容:
00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60 01 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 96 80 80 80 00 02 06 6D 65 6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69 00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20 00 41 2A 6A 0B
這是模塊的「二進制」表示。之因此給「二進制」加上引號,是由於二進制表示一般是顯示爲十六進制的,可是能夠很簡單的轉爲二進制,或者人類可讀的格式。
舉例來講,下面是 num + 42
的模樣:
你可能會對上圖中的內容感到疑惑,下面咱們把這些指令的做用標註出來。
你可能已經注意到 add
操做並無說要相加的兩個數從哪裏來。這是由於 WebAssembly 是一種稱爲堆棧機器。這意味着,操做碼在操做以前,它所需的操做數已經在堆棧的隊列當中了。
像 add
這樣的操做碼自己就知道它須要多少個操做數。由於 add
須要兩個操做數,因此它會從堆棧的頂部取出兩個值來做爲操做數。
這樣種設計中,add
指令能夠變得很短,只佔用一字節,由於它並不須要指定源和目標寄存器地址。這樣就減少了 .wasm
文件的大小,從而更利於網絡傳輸。
儘管 WebAssembly 是根據堆棧機器來設計的,可是這並非它在真實物理機器上工做的方式。
當瀏覽器把 WebAssembly 編譯爲機器碼時,它仍然會用到寄存器。不過,因爲 WebAssembly 代碼並不指定寄存器,因此瀏覽器可以更自由的爲其指定最高效的寄存器。
除了 add42
函數自己,.wasm
也還包含了其餘內容。這些內容稱爲段(Section)。有些段是任何模塊都必須有的,有些則是可選的。
必選的有:
可選的有:
更多的資料可參考 MDN 文檔
通過本文,相信你已經知道該如何使用 WebAssembly 模塊了。下一篇文章咱們將探索它爲什麼如此快。