WebAssembly起步

瞭解WebAssembly原理

WebAssembly是一種能夠在瀏覽器上運行的二進制可執行格式文件。它將成爲瀏覽器進化史上又一次革命。javascript

自從瀏覽器問世以來,javascript就成爲瀏覽器上執行程序的惟一標準,愈來愈多的應用程序經過javascript開發,並運行於瀏覽器上;而隨着瀏覽器上h5程序功能的豐富,也對瀏覽器提出了更多的挑戰。其中一條最爲重要的就是性能問題。javascript是一種弱類型,解釋性的腳本語言。它天生運行速度慢,成爲了不少h5應用的軟肋。雖然2008年google V8引入了即時編譯等技術使js的運行速度提高了一大截,可是一些大型應用程序,好比遊戲,視頻編輯,壓縮,算法等依然不適合運行在瀏覽器上。html

WebAssembly的到來解決了這個問題,並給開發基於瀏覽器的應用程序提供了另外的編程語言選擇。2017年三大瀏覽器同時增長了WebAssembly支持,標誌着WebAssembly已經達到生產實用標準。html5

爲何WebAssembly比javascript快

回答這個問題須要洞悉瀏覽器執行javascript代碼的各個環節。
瀏覽器加載並執行javascript大概可分爲以下幾個環節: 下載,解析,執行和優化,垃圾回收。java

下載

javascript是以純文本格式下載的。相比,webassembly使用二進制格式存儲,結構更精簡,更小。node

解析

javascript下載後,須要js引擎通過tokenize, parse兩個階段轉換成AST(abstract syntax tree),而後再轉換爲瀏覽器須要的中間字節碼。因爲js是比較高級的語言,解析js也相對要作更多的事情。webassembly的格式相似於彙編語言,原本就是中間字節碼,和須要運行的機器碼更相近,須要簡單的轉換工做便可轉化爲CPU能夠直接執行的機器碼。git

下圖是一個真實運行的webassembly(它是文本的,只是爲了方便調試),能夠看出它和彙編是很類似的,更易轉化爲機器碼。github

WebAssembly源碼

執行和優化

在執行階段,js廣泛採用解釋執行策略,至關於每一次執行javascript指令都要經過js引擎中轉給cpu。現代的js引擎同時採用了即時編譯的策略。這須要同時運行一個profiler,關注每一個函數的調用狀況。當profiler發現一個函數調用的比較多的時候,會把這個函數拋給編譯器,爲它生成一個更快的編譯版本。某些狀況下,參數類型會發生變化。這時,須要刪除以前的編譯版本,對新參數類型編譯新的版本。而webassembly因爲類彙編的結構,只需簡單的編譯便可轉換爲可直接運行在cpu上的機器碼,執行更快。web

垃圾回收

javascript運行期間須要同時間歇的運行一個垃圾回收器,掃描堆上的垃圾、釋放內存。垃圾回收器的運行又和js引擎的執行是互斥的,致使js執行間歇性的被垃圾回收器打斷。webassembly不負責垃圾回收,只能編程語言自行解決。因而不一樣的編程語言又有所不一樣。C/C++是手動管理內存(malloc/free, new/delete),rust則是基於生命週期的自動內存管理。全部這些內存管理方法都不須要間歇的全局暫停。所以性能更好。算法

從以上各個角度看WebAssembly確實比javascript性能高。事實上,目前階段WebAssembly執行時間大概等於原生程序執行時間X1.2。編程

WebAssembly的加載與執行

wasm是WebAssembly格式的瀏覽器可執行文件。它是二進制的,可是它並不像桌面win32程序同樣,能夠隨便使用系統資源,調用操做系統api。事實上,全部與外界相關的操做都必須由javascript傳入。好比:要申請一段內存,必須由javascript申請了並傳給他。 瀏覽器上,javascript作不到的,它也作不到;javascript能作到的,它能作的更快。 這個就是它的價值。

目前必需要js啓動WebAssembly的加載和實例化(後面可能會有單獨的加載機制)。

以下函數,使用fetchAPI加載wasm文件,並實例化wasm模塊。

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

fetchAndInstantiate('module.wasm', importObject).then(function(instance) {
  ...
})

importObject即瀏覽器須要向webassembly注入的交互api。

以下,是一個真實運行的importObject包括不少js函數。

importObject示例

注意global.memory就是webassembly程序執行用到的內存,是js申請的一個大的ArrayBuffer。

學會WebAssembly開發

講了這麼多WebAssembly的優勢,接下就講下WebAssembly的開發。

開發WebAssembly並不意味着須要手寫WebAssembly彙編程序。一個開源項目emscripten已經提供了sdk能夠編譯C/C++,並輸出WebAssembly的wasm文件。目前,rust也已經支持編譯到wasm。將來全部支持編譯到LLVM字節碼的編程語言,理論上均可以輸出wasm。

安裝emscripten

下載emscripten sdk後,是個壓縮文件,實際上是sdk包管理器。
須要執行以下命令,完成sdk的安裝。

./emsdk update
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

如今已經有個可用的emcc編譯器了,輸入:

emcc --version

查看編譯器版本。

emsdk安裝後, emscripten文件內是按版本號安裝的sdk內容,裏面有不少C/C++用例,能夠自行研究下。

簡單demo

這個簡單的C程序能夠直接編譯爲wasm。

#include <stdio.h>

int main() {
  printf("hello, world!\n");
  return 0;
}
./emcc hello_world.c
node a.out.js

默認狀況下,emcc只輸出了一個js(asmjs)。asmjs是webassembly的一個早期原型,可提供webassembly在舊版本瀏覽器上的兼容。按以下命令輸出webassembly二進制wasm。

./emcc hello_world.c -s WASM=1 -o index.html

此次編譯輸出了index.html, index.js, index.wasm三個文件。經過一個靜態服務器打開index.html,能夠看到console裏的輸出。

hello world示例

這個index.html是一個調試頁面。生產上加載webassembly通常都須要本身寫index.html,只保留js和wasm文件就夠了。

以上的例子中,printf的標準輸出被定向到了瀏覽器的console裏面。 系統API調用被換成了js實現。 事實上不少libc裏面的函數被emscripten實現成了瀏覽器上的兼容方案,從而更好的和瀏覽器結合。

環境

全部編程語言都要和它的運行環境打交道,不然除了把cpu跑滿,沒什麼實用價值。跑在瀏覽器上的webassembly則是經過和js相互調用發揮它的做用。

Emscripten sdk提供了不少API與js運行環境/瀏覽器交互。定義在其中兩個頭文件中:

  • emscripten.h: 中定義了一些基礎功能相關API,包括調用js,文件讀寫,網絡請求等,這些API在node中也能夠用。
  • html5.h中定義了瀏覽器中與DOM相關的各類操做,包括DOM,事件,設備相關等。

下面,抽出一些關鍵的API講下webassembly是如何與瀏覽器協同工做的。

調用js

EM_ASM宏,讓webassembly能夠直接調用js。

EM_ASM(alert('hai'); alert('bai'));

若是須要從js獲取執行結果,能夠用EM_ASM_INTEM_ASM_DOUBLE兩個版本分別獲取intdouble類型的數值。

int x = EM_ASM_INT({
  return $0 + 42;
}, 100);

若是須要傳遞字符串給js,能夠傳遞一個字符串起始的指針給js。因爲js能夠訪問整個wasm程序的內存區域,js用這個指針就能夠從內存讀出字符串。Module對象上的UTF8ToString(ptr), UTF16ToString(ptr), UTF32ToString(ptr), Pointer_stringify(ptr, length)這幾個函數可得到指針處的字符串。

char* sample = "This is a string";
  EM_ASM_({
      console.log("js got string:", Module.UTF8ToString($0));
  }, sample);

標準輸入輸出

標準輸出咱們以前看過,printf最終被轉到Module.print,默認是console.log實現。
標準錯誤輸出最終會被轉到Module.printErr,默認是console.error實現。
對標準輸入的讀取在瀏覽器上變成了一個prompt框。體驗很差,儘可能不要讀。

顯示

Emscripten支持兩種GUI展現方法。

  • DOM: wasm是能夠調用js的,而js又能夠操做DOM。所以,wasm能夠經過js操做DOM,建立程序的GUI。
  • Webgl Canvas: 除了DOM,emscripten還能夠提供了opengl es的瀏覽器實現。經過操做一個Webgl Canvas,把顯示內容畫在Canvas上。

事件循環

C++ GUI程序通常都有個事件循環,其實就是個死循環,反覆獲取並處理GUI層面上的各類事件。這樣程序不會跑完main函數直接退出。webassembly程序跑在瀏覽器上,而瀏覽器原本就是事件驅動,已經有了一個事件循環。假如不改動直接上瀏覽器,就會卡死瀏覽器的GUI進程。所以webassembly程序須要由瀏覽器控制事件循環。

emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop)函數接受一個函數的指針後,瀏覽器會根據fps按時調用傳入的函數。

#include <stdio.h>
#include <emscripten.h>

int frame = 0;
void main_loop(void) {
  printf("frame: %d\n", frame);
  frame++;
}

int main(void) {
  emscripten_set_main_loop(main_loop, 0, 1);
  return 0;
}

存儲

瀏覽器隔離了程序直接操做存儲的權限,於是webapp是安全的,但不少C代碼都有同步操做文件的API,如open, write, close。爲了兼容,emscripten實現了一個內存文件系統,能夠經過全局對象FS訪問。

下圖,是FS對象下的函數。
FS的函數

另外,emcc還提供了--preload-file參數,在webassembly程序加載的過程當中,預加載文件放到虛擬文件系統中。

wasm中的文件雖然是內存的,可是支持經過indexDB持久化。
以下js,mount一個indexdb的文件夾到/data目錄,而後FS.syncfs把indexdb中的文件同步到內存。

FS.mkdir('/data');
FS.mount(IDBFS, {}, '/data');
FS.syncfs(true, function (err) {
});

接下來,全部,/data目錄下的讀寫,都在內存中的同步讀寫。當程序關閉的時候,須要調用FS.syncfs(false, function(err){})把內存中的文件反方向同步回indexdb。

emsdk提供了一些經常使用的C++庫的webassembly兼容版本。用emcc --show-ports命令顯示。若是要用SDL2,須要給emcc加入選項-s USE_SDL=2,連接SDL2庫。

目前,emcc內置支持這些庫。

$ emcc --show-ports
Available ports:
    zlib (USE_ZLIB=1; zlib license)
    libpng (USE_LIBPNG=1; zlib license)
    SDL2 (USE_SDL=2; zlib license)
    SDL2_image (USE_SDL_IMAGE=2; zlib license)
    ogg (USE_OGG=1; zlib license)
    vorbis (USE_VORBIS=1; zlib license)
    bullet (USE_BULLET=1; zlib license)
    freetype (USE_FREETYPE=1; freetype license)
    SDL2_ttf (USE_SDL_TTF=2; zlib license)
    SDL2_net (zlib license)
    Binaryen (Apache 2.0 license)
    cocos2d

若是所須要的庫沒在列表裏,須要先用emsdk編譯所須要的庫(可能涉及到庫的改動)。再編譯並連接,輸出最終目標。emcc不支持動態連接。

展望

目前,webassembly已經完成MVP最小功能版本開發,有很是注目的性能。能夠碰見,將來將有更多h5 app/遊戲經過webassembly得到更好的體驗。使用C/C++/rust進行webapp開發,混合編程,也會有不少不錯的探索。

將來h5可否經過webassembly撼動原生的大門,讓咱們拭目以待。

相關文章
相關標籤/搜索