天天看點

建立和使用 WebAssembly 元件

<b>本文講的是建立和使用 WebAssembly 元件,</b>

WebAssembly 是一種不同于 JavaScript 的在 web 頁面上運作程式語言的方式。以前當你想在浏覽器上運作代碼來實作 web 頁面不同部分的互動時,你唯一的選擇就是 JavaScript。

是以當人們談論 WebAssembly 運作迅速時,合理的比較對象就是 JavaScript。但這并不意味着你必須在 WebAssembly 和 JavaScript 二者中選擇一個使用。

事實上我們希望開發者在同一應用中同時使用 WebAssembly 和 JavaScript。即使你不親自寫 WebAssembly 代碼,你也可以使用它。

WebAssembly 元件定義的函數可以在 JavaScript 中使用。是以,就像現在你可以從 npm 上下載下傳一個 lodash 這樣的元件并且根據它的 API 調用方法一樣,在未來你同樣可以下載下傳 WebAssembly 元件。

那麼讓我們看看如何建立 WebAssembly 元件,以及如何在 JavaScript 中使用這些元件吧。

建立和使用 WebAssembly 元件

Diagram showing an intermediate representation between high level languages and assembly languages, with arrows going from high level programming languages to intermediate representation, and then from intermediate representation to assembly language

WebAssembly 對應這張圖檔的哪個部分?

你可能認為它隻不過是又一個目标彙編語言。某種程度上是對的,不同之處在于那些語言(x86,ARM)中每個都對應一個特定的機器架構。

當你通過 web 向使用者的機器上發送要執行的代碼時,你并不知道你的代碼将要在哪種目标架構上運作。

是以 WebAssembly 和其他的彙編有些細微的差别。它是概念機的機器語言,而非真實的實體機。

正因如此,WebAssembly 指令有時也被稱為虛拟指令。它們比 JavaScript 源碼有更直接的機器碼映射。它們代表一類可以在常見的流行硬體上高效執行的指令集合。但是它們并不直接映射某一具體硬體的特定機器碼。

建立和使用 WebAssembly 元件

Same diagram as above with WebAssembly inserted between the intermediate representation and assembly

浏覽器下載下傳 WebAssembly 後,它就能從 WebAssembly 轉成目标機器的彙編碼。

LLVM 是目前對 WebAssembly 支援最好的編譯工具鍊。很多前後端編譯工具都可以嵌入 LLVM 中。

比如說我們想把 C 編譯成 WebAssembly。我們可以使用 clang 編譯器前端把 C 編譯成 LLVM 中介碼。一旦它處于 LLVM 的中間層,LLVM 編譯它,LLVM 就可以展現一些性能優化。

目前有一個稍微容易使用的工具叫 Emscripten。他有自己的後端,可以通過編譯成其他對象(稱為 asm.js)然後再轉換成 WebAssembly 的方式來産生 WebAssembly。好像它底層仍舊使用 LLVM,是以你可以在 Emscripten 中切換這兩種後端。

建立和使用 WebAssembly 元件

Diagram of the compiler toolchain

Emscripten 包含了許多附加工具和庫來支援移植整個 C/C++ 代碼庫,是以它更像一個 SDK 而非編譯器。舉個例子,系統開發人員習慣于有一個檔案系統用來讀寫,是以 Emscripten 可以使用 IndexedDB 模拟一個檔案系統。

忽略你已經使用的工具鍊,最後得到的結果就是一個字尾名為 .wasm 的檔案。下面我将着重解釋 .wasm 檔案的結構。首先,我們先看看怎樣在JS中使用 .wasm 檔案。

這個 .wasm 檔案是一個 WebAssembly 元件,它可以在 JavaScript 中載入。在此情景下,載入過程稍微有些複雜。

我們緻力于讓這個過程變得更容易。我們期望改進工具鍊,整合已存在的像 webpack 這樣的子產品打包工具以及類似 SystemJS 的動态加載器。我們相信載入 WebAssembly 元件可以像載入 JavaScript 元件一樣簡單。

不過,WebAssembly 元件和 JS 元件有一個顯著的差別。目前,WebAssembly 函數隻能使用數字(整型或浮點型數字)作為參數和傳回值。

建立和使用 WebAssembly 元件

Diagram showing a JS function calling a C function and passing in an integer, which returns an integer in response

對于更加複雜的資料類型,如字元串,你必須使用 WebAssembly 元件存儲器。

像 C,C++,和 Rust 這些更高性能的語言傾向于手動管理記憶體。如果你大部分時間都在使用 JavaScript,也許對直接通路存儲器的操作不熟悉。WebAssembly 元件存儲器模拟了你在這些語言中會看到的堆。

為了實作這個功能,它使用了 JavaScript 中的類型化數組(ArrayBuffer)。類型化數組是存放位元組的數組。數組的索引就是對應的存儲器位址。

如果想要在 JavaScript 和 WebAssembly 中傳遞字元串,你需要把這些字元轉換成他們的字元碼常量。然後把這些寫入存儲器陣列。既然索引是整數,那麼單個索引值就可以傳入 WebAssembly 函數中。這樣字元串中第一個字元的索引就可以被當成一個指針使用。

建立和使用 WebAssembly 元件

Diagram showing a JS function calling a C function with an integer that represents a pointer into memory, and then the C function writing into memory

幾乎所有想要開發供 web 開發者使用的 WebAssembly 元件的開發者,都會為元件建立一個包裝器。這樣以來,你作為元件的消費者并不需要了解記憶體管理。

如果你使用進階語言來編寫代碼然後把它編譯成 WebAssembly,你不必知道 WebAssembly 元件的結構。但是它可以幫助你了解其基本原理。

下面是一個 C 函數,我們将把它轉成 WebAssembly:

如果你打開 .wasm 檔案(假設你的編輯器支援顯示),你将看到類似這樣的内容:

這是元件的“二進制”表示法。我把二進制加上引号是因為它通常顯示的是十六進制符号,但這很容易轉換成二進制符号,或者人類可讀的格式。

舉個例子,下圖是 <code>num + 42</code> 的幾種表現形式。

建立和使用 WebAssembly 元件

Table showing hexadecimal representation of 3 instructions (20 00 41 2A 6A), their binary representation, and then the text representation (get_local 0, i32.const 42, i32.add)

如果你想知道的話,下圖是執行的一些指令說明。

建立和使用 WebAssembly 元件

Diagram showing that get_local 0 gets value of first param and pushes it on the stack, i32.const 42 pushes a constant value on the stack, and i32.add adds the top two values from the stack and pushes the result

你可能注意到了 <code>add</code> 操作并沒有說明他的值應該從哪裡來。這是因為 WebAssembly 是堆棧機的一個範例。這意味着一個操作所需的所有值在操作執行之前都在棧中排隊。

例如 <code>add</code> 這類的操作指導它們需要多少值。如果 <code>add</code> 需要兩個值,它将從棧頂取出兩個值。這意味着 <code>add</code> 指令可以很短(單個位元組),因為指令不需要指定源或者目的寄存器。這減少了 .wasm 檔案的大小,也意味着下載下傳的耗時更短。

即使 WebAssembly 就堆棧機而言是特定的,但那不是其在實體機上的工作方式。當浏覽器把 WebAssembly 轉化成其運作機器上對應的機器碼時,将會用到寄存器。因為 WebAssembly 代碼不指定寄存器,是以浏覽器在目前機器上能更靈活的去使用最佳寄存器配置設定。

除了 <code>add42</code> 函數自身,.wasm 檔案還有其他部分。那就是 sections。一些 sections 對任何元件都是必需的,而有一些是可選的。

必選項:

類型(Type)。包括在該元件中定義的函數簽名以及任何引入的函數。

函數(Function)。給每一個在該元件中定義的函數一個索引。

代碼(Code)。該元件中定義的每一個函數的實際函數體。

可選項:

導出(Export)。使函數,記憶體,表以及全局變量對其他 WebAssembly 元件和 JavaScript 可用。這使獨立編譯的元件可以被動态連結在一起。這就是 WebAssembly 的 .dll 版本。

導入(Import)。從其他 WebAssembly 元件或 JavaScript 中導入指定的函數,記憶體,表以及全局變量。

啟動(Start)。當 WebAssembly 元件載入時自動運作的函數(基本上類似一個主函數)。

全局變量(Global)。為元件聲明全局變量。

記憶體(Memory)。定義元件将使用到的記憶體空間。

表(Table)。使把值映射到 WebAssembly 元件外部成為可能,就像 JavaScript 對象那樣。這對于允許簡介函數調用相當有用。

資料(Data)。初始化導入或本地記憶體。

元素(Element)。初始化導入或本地的表。

<b></b>

<b>原文釋出時間為:2017年3月21日</b>

<b>本文來自雲栖社群合作夥伴掘金,了解相關資訊可以關注掘金網站。</b>

繼續閱讀