WebAssembly 系列(四)WebAssembly 工做原理

做者:Lin Clark <br/>
編譯:鬍子大哈 前端

翻譯原文:http://huziketang.com/blog/posts/detail?postId=58c77641a6d8a07e449fdd24 <br/>
英文原文:Creating and working with WebAssembly modulesreact

轉載請註明出處,保留原文連接以及做者信息webpack


本文是關於 WebAssembly 系列的第四篇文章(本系列共六篇文章)。若是你沒有讀先前文章的話,建議先讀這裏。若是對 WebAssembly 沒概念,建議先讀這裏(中文文章)git

WebAssembly 是除了 JavaScript 之外,另外一種能夠在網頁中運行的編程語言。過去若是你想在瀏覽器中運行代碼來對網頁中各類元素進行控制,只有 JavaScript 這一種選擇。github

因此當人們談論 WebAssembly 的時候,每每會拿 JavaScript 來進行比較。可是它們其實並非「二選一」的關係——並非只能用 WebAssembly 或者 JavaScript。web

實際上,咱們鼓勵開發者將這兩種語言一塊兒使用,即便你不親自實現 WebAssembly 模塊,你也能夠學習它現有的模塊,並它的優點來實現你的功能。npm

WebAssembly 模塊定義的一些功能能夠經過 JavaScript 來調用。因此就像你經過 npm 下載 lodash 模塊並經過 API 使用它同樣,將來你也能夠下載 WebAssembly 模塊而且使用其提供的功能。編程

那麼就讓咱們來看一下如何開發 WebAssembly 模塊,以及如何經過 JavaScript 使用他們。後端

WebAssembly 處於哪一個環節?

在上一篇關於彙編的文章中,我介紹了編譯器是如何從高級語言翻譯到機器碼的。數組

那麼在上圖中,WebAssembly 在什麼位置呢?實際上,你能夠把它當作另外一種「目標彙編語言」。

每一種目標彙編語言(x8六、ARM)都依賴於特定的機器結構。當你想要把你的代碼放到用戶的機器上執行的時候,你並不知道目標機器結構是什麼樣的。

而 WebAssembly 與其餘的彙編語言不同,它不依賴於具體的物理機器。能夠抽象地理解成它是概念機器的機器語言,而不是實際的物理機器的機器語言。

正由於如此,WebAssembly 指令有時也被稱爲虛擬指令。它比 JavaScript 代碼更直接地映射到機器碼,它也表明了「如何能在通用的硬件上更有效地執行代碼」的一種理念。因此它並不直接映射成特定硬件的機器碼。

瀏覽器把 WebAssembly 下載下來,而後先通過 WebAssembly 模塊,再到目標機器的彙編代碼。

編譯到 .wasm 文件

目前對於 WebAssembly 支持狀況最好的編譯器工具鏈是 LLVM。有不少不一樣的前端和後端插件能夠用在 LLVM 上。

提示:不少 WebAssembly 開發者用 C 語言或者 Rust 開發,再編譯成 WebAssembly。其實還有其餘的方式來開發 WebAssembly 模塊。例如利用 TypeScript 開發 WebAssembly 模塊,或者直接用 WebAssembly 文本也能夠。

假設想從 C 語言到 WebAssembly,咱們就須要 clang 前端來把 C 代碼變成 LLVM 中間代碼。當變換成了 LLVM IR 時,說明 LLVM 已經理解了代碼,它會對代碼自動地作一些優化。

爲了從 LLVM IR 生成 WebAssembly,還須要後端編譯器。在 LLVM 的工程中有正在開發中的後端,並且應該很快就開發完成了,如今這個時間節點,暫時還看不到它是如何起做用的。

還有一個易用的工具,叫作 Emscripten。它經過本身的後端先把代碼轉換成本身的中間代碼(叫作 asm.js),而後再轉化成 WebAssembly。實際上它背後也是使用的 LLVM。

Emscripten 還包含了許多額外的工具和庫來包容整個 C/C++ 代碼庫,因此它更像是一個軟件開發者工具包(SDK)而不是編譯器。例如系統開發者須要文件系統以對文件進行讀寫,Emscripten 就有一個 IndexedDB 來模擬文件系統。

不考慮太多的這些工具鏈,只要知道最終生成了 .wasm 文件就能夠了。後面我會介紹 .wasm 文件的結構,在這以前先一塊兒瞭解一下在 JS 中如何使用它。

加載一個 .wasm 模塊到 JavaScript

.wasm 文件是 WebAssembly 模塊,它能夠加載到 JavaScript 中使用,現階段加載的過程稍微有點複雜。

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

若是想深刻了解,能夠在 MDN 文檔中瞭解更多。

咱們一直在致力於把這一過程變得簡單,對工具鏈進行優化。但願可以把它整合到現有的模塊打包工具中,好比 webpack 中,或者整合到加載器中,好比 SystemJS 中。咱們相信加載 WebAssembly 模塊也能夠像加載 JavaScript 同樣簡單。

這裏介紹 WebAssembly 模塊和 JavaScript 模塊的主要區別。當前的 WebAssembly 只能使用數字(整型或者浮點型)做爲參數或者返回值。

對於任何其餘的複雜類型,好比 string,就必須得用 WebAssembly 模塊的內存操做了。若是是常用 JavaScript,對直接操做內存不是很熟悉的話,能夠回想一下 C、C++ 和 Rust 這些語言,它們都是手動操做內存。WebAssembly 的內存操做和這些語言的內存操做很像。

爲了實現這個功能,它使用了 JavaScript 中稱爲 ArrayBuffer 的數據結構。ArrayBuffer 是一個字節數組,因此它的索引(index)就至關於內存地址了。

若是你想在 JavaScript 和 WebAssembly 之間傳遞字符串,能夠利用 ArrayBuffer 將其寫入內存中,這時候 ArrayBuffer 的索引就是整型了,能夠把它傳遞給 WebAssembly 函數。此時,第一個字符的索引就能夠當作指針來使用。

這就好像一個 web 開發者在開發 WebAssembly 模塊時,把這個模塊包裝了一層外衣。這樣其餘使用者在使用這個模塊的時候,就不用關心內存管理的細節。

若是你想了解更多的內存管理,看一下咱們寫的 WebAssembly 的內存操做

.wasm 文件結構

若是你是寫高級語言的開發者,而且經過編譯器編譯成 WebAssembly,那你不用關心 WebAssembly 模塊的結構。可是瞭解它的結構有助於你理解一些基本問題。

若是你對編譯器還不瞭解,建議先讀一下「WebAssembly 系列(三)編譯器如何生成彙編」這篇文章。

這段代碼是即將生成 WebAssembly 的 C 代碼:

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 的各類表示方法。

代碼是如何工做的:基於棧的虛擬機

若是你對具體的操做過程很好奇,那麼這幅圖能夠告訴你指令都作了什麼。

從圖中咱們能夠注意到 操做並無指定哪兩個數字進行加。這是由於 WebAssembly 是採用「基於棧的虛擬機」的機制。即一個操做符所須要的全部值,在操做進行以前都已經存放在堆棧中。

全部的操做符,好比加法,都知道本身須要多少個值。 須要兩個值,因此它從堆棧頂部取兩個值就能夠了。那麼指令就能夠變的更短(單字節),由於指令不須要指定源寄存器和目的寄存器。這也使得 .wasm 文件變得更小,進而使得加載 .wasm 文件更快。

儘管 WebAssembly 使用基於棧的虛擬機,可是並非說在實際的物理機器上它就是這麼生效的。當瀏覽器翻譯 WebAssembly 到機器碼時,瀏覽器會使用寄存器,而 WebAssembly 代碼並不指定用哪些寄存器,這樣作的好處是給瀏覽器最大的自由度,讓其本身來進行寄存器的最佳分配。

WebAssembly 模塊的組成部分

除了上面介紹的,.wasm 文件還有其餘部分,一般把它們叫作部件。一些部件對於模塊來說是必須的,一些是可選的。

必須部分

  1. Type。在模塊中定義的函數的函數聲明和全部引入函數的函數聲明。

  2. Function。給出模塊中每一個函數一個索引。

  3. Code。模塊中每一個函數的實際函數體。

可選部分

  1. Export。使函數、內存、表單(table)、全局變量等對其餘 WebAssembly 或 JavaScript 可見,容許動態連接一些分開編譯的組件,即 .dll 的WebAssembly 版本。

  2. Import。容許從其餘 WebAssembly 或者 JavaScript 中引入指定的函數、內存、表單或者全局變量。

  3. Start。當 WebAssembly 模塊加載進來的時候,能夠自動運行的函數(相似於 main 函數)。

  4. Global。聲明模塊的全局變量。

  5. Memory。定義模塊用到的內存。

  6. Table。使得能夠映射到 WebAssembly 模塊之外的值,如映射到 JavaScript 對象中。這在間接函數調用時頗有用。

  7. Data。初始化內存。

  8. Element。初始化表單(table)。

若是想要了解更多的部件,能夠在「如何使用部件」中深刻了解。

下文預告

如今你已經瞭解了 WebAssembly 模塊的工做原理,下面將會介紹爲何 WebAssembly 運行的更快


我最近正在寫一本《React.js 小書》,對 React.js 感興趣的童鞋,歡迎指點

相關文章
相關標籤/搜索