建立和使用 WebAssembly 組件

這是 WebAssembly 系列文章的第四部分。若是你還沒閱讀過前面的文章,咱們建議你從頭開始javascript

WebAssembly 是一種不一樣於 JavaScript 的在 web 頁面上運行程序語言的方式。之前當你想在瀏覽器上運行代碼來實現 web 頁面不一樣部分的交互時,你惟一的選擇就是 JavaScript。前端

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

事實上咱們但願開發者在同一應用中同時使用 WebAssembly 和 JavaScript。即便你不親自寫 WebAssembly 代碼,你也可使用它。react

WebAssembly 組件定義的函數能夠在 JavaScript 中使用。所以,就像如今你能夠從 npm 上下載一個 lodash 這樣的組件而且根據它的 API 調用方法同樣,在將來你一樣能夠下載 WebAssembly 組件。android

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

WebAssembly 處於哪一個環節?

在上一篇關於彙編的文章裏,我談到過編譯器怎麼提取高級程序語言而且把它們翻譯成機器碼。ios

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 對應這張圖片的哪一個部分?git

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

當你經過 web 向用戶的機器上發送要執行的代碼時,你並不知道你的代碼將要在哪一種目標架構上運行。web

因此 WebAssembly 和其餘的彙編有些細微的差異。它是概念機的機器語言,而非真實的物理機。

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

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

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

編譯成 .wasm

LLVM 是當前對 WebAssembly 支持最好的編譯工具鏈。不少先後端編譯工具均可以嵌入 LLVM 中。

注:大部分 WebAssembly 組件開發者用 C 和 Rust 這樣的語言編寫代碼,而後編譯成 WebAssembly,但仍有其餘的方法來建立 WebAssembly 組件。好比,有一個實驗性的工具幫你使用 TypeScript 構建 WebAssembly 組件,或者你能夠直接在 WebAssembly 的文本表示上編碼

好比說咱們想把 C 編譯成 WebAssembly。咱們可使用 clang 編譯器前端把 C 編譯成 LLVM 中介碼。一旦它處於 LLVM 的中間層,LLVM 編譯它,LLVM 就能夠展示一些性能優化。

要把 LLVM IR(中介碼)編譯成 WebAssembly,咱們須要一個後端支持。在 LLVM 項目中有一個這類後端正在開發中。這個後端項目已經接近完成而且應該很快就會定稿。然而,如今使用它還會有很多問題。

目前有一個稍微容易使用的工具叫 Emscripten。他有本身的後端,能夠經過編譯成其餘對象(稱爲 asm.js)而後再轉換成 WebAssembly 的方式來產生 WebAssembly。好像它底層仍舊使用 LLVM,所以你能夠在 Emscripten 中切換這兩種後端。

Diagram of the compiler toolchain

Emscripten 包含了許多附加工具和庫來支持移植整個 C/C++ 代碼庫,所以它更像一個 SDK 而非編譯器。舉個例子,系統開發人員習慣於有一個文件系統用來讀寫,因此 Emscripten 可使用 IndexedDB 模擬一個文件系統。

忽略你已經使用的工具鏈,最後獲得的結果就是一個後綴名爲 .wasm 的文件。下面我將着重解釋 .wasm 文件的結構。首先,咱們先看看怎樣在JS中使用 .wasm 文件。

在 JavaScript 中載入一個 .wasm 組件

這個 .wasm 文件是一個 WebAssembly 組件,它能夠在 JavaScript 中載入。在此情景下,載入過程稍微有些複雜。

functionfetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}複製代碼

你能夠在咱們的文檔中深刻了解這部份內容。

咱們致力於讓這個過程變得更容易。咱們指望改進工具鏈,整合已存在的像 webpack 這樣的模塊打包工具以及相似 SystemJS 的動態加載器。咱們相信載入 WebAssembly 組件能夠像載入 JavaScript 組件同樣簡單。

不過,WebAssembly 組件和 JS 組件有一個顯著的區別。目前,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 函數中。這樣字符串中第一個字符的索引就能夠被當成一個指針使用。

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 內存的文檔。

.wasm 文件結構

若是你使用高級語言來編寫代碼而後把它編譯成 WebAssembly,你沒必要知道 WebAssembly 組件的結構。可是它能夠幫助你理解其基本原理。

若是你以前沒有了解這些基本原理,咱們建議你先閱讀 彙編文章 (part 3 of the series)。

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

int add42(int num) {
  return num + 42;
}複製代碼

你可使用 WASM Explorer 來編譯這個函數。

若是你打開 .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 的幾種表現形式。

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)

代碼如何運行:堆棧機

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

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

你可能注意到了 add 操做並無說明他的值應該從哪裏來。這是由於 WebAssembly 是堆棧機的一個範例。這意味着一個操做所需的全部值在操做執行以前都在棧中排隊。

例如 add 這類的操做指導它們須要多少值。若是 add 須要兩個值,它將從棧頂取出兩個值。這意味着 add 指令能夠很短(單個字節),由於指令不須要指定源或者目的寄存器。這減小了 .wasm 文件的大小,也意味着下載的耗時更短。

即便 WebAssembly 就堆棧機而言是特定的,但那不是其在物理機上的工做方式。當瀏覽器把 WebAssembly 轉化成其運行機器上對應的機器碼時,將會用到寄存器。由於 WebAssembly 代碼不指定寄存器,因此瀏覽器在當前機器上能更靈活的去使用最佳寄存器分配。

組件的 sections

除了 add42 函數自身,.wasm 文件還有其餘部分。那就是 sections。一些 sections 對任何組件都是必需的,而有一些是可選的。

必選項:

  1. 類型(Type)。包括在該組件中定義的函數簽名以及任何引入的函數。
  2. 函數(Function)。給每個在該組件中定義的函數一個索引。
  3. 代碼(Code)。該組件中定義的每個函數的實際函數體。

可選項:

  1. 導出(Export)。使函數,內存,表以及全局變量對其餘 WebAssembly 組件和 JavaScript 可用。這使獨立編譯的組件能夠被動態連接在一塊兒。這就是 WebAssembly 的 .dll 版本。
  2. 導入(Import)。從其餘 WebAssembly 組件或 JavaScript 中導入指定的函數,內存,表以及全局變量。
  3. 啓動(Start)。當 WebAssembly 組件載入時自動運行的函數(基本上相似一個主函數)。
  4. 全局變量(Global)。爲組件聲明全局變量。
  5. 內存(Memory)。定義組件將使用到的內存空間。
  6. 表(Table)。使把值映射到 WebAssembly 組件外部成爲可能,就像 JavaScript 對象那樣。這對於容許簡介函數調用至關有用。
  7. 數據(Data)。初始化導入或本地內存。
  8. 元素(Element)。初始化導入或本地的表。

更多關於 sections 的闡釋,這有一篇深度好文解釋這些 sections 如何運行

接下來

如今你知道怎樣使用 WebAssembly 組件了,讓咱們看看爲何 WebAssembly 這麼快

『系列文章之最終章』

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索