WebAssembly初探

本次分享的文章是基於WebAssembly的探索與研究。最近須要作一個與加密相關的項目,想將後端的加密方案直接放到前端使用,好處是加密方案代碼只用維護一套,且後端方案更貼近系統底層,應該能夠獲得更好的性能。剛好發現 WebAssembly ,它是爲了可移植的目標而設計的,能夠知足需求。javascript

此次研究 WebAssembly的過程當中遇到了各類問題,我均記錄下來,並在後期能夠和你們一塊兒分享,文末放置了參考的文章,你們能夠延伸閱讀。這篇文章是本系列的第一部分,主要是瞭解WebAssembly和WebAssembly的基本使用方法。html

概述

  • WebAssembly的誕生
  • WebAssembly是什麼?
  • MAC安裝Emscripten
  • WebAssembly簡單使用和分析
  • 總結

1、 WebAssembly的誕生

當人們說 WebAssembly 更快的時候,通常來說是與 JavaScript 相比而言的。前端

JavaScript 於 1995 年問世,它的設計初衷並非爲了執行起來快,在前 10 個年頭,它的執行速度也確實不快。緊接着,瀏覽器市場競爭開始激烈起來。被人們廣爲傳播的「性能大戰」在 2008 年打響。許多瀏覽器引入了 Just-in-time 編譯器,也叫 JIT。基於 JIT 的模式,JavaScript 代碼的運行漸漸變快。正是因爲這些 JIT 的引入,使得 JavaScript 的性能達到了一個轉折點,JS 代碼執行速度快了 10 倍。java

在這裏插入圖片描述

隨着性能的提高,JavaScript 能夠應用到之前根本沒有想到過的領域,好比用於後端開發的 Node.js。性能的提高使得 JavaScript 的應用範圍獲得很大的擴展。node

在這裏插入圖片描述

但這也漸漸暴露出了 JavaScript 的問題:python

  • 語法太靈活致使開發大型 Web 項目困難;
  • 性能不能知足一些場景的須要。

針對以上兩點缺陷,近年來出現了一些 JS 的代替語言,例如:git

  • 微軟的 TypeScript 經過爲 JS 加入靜態類型檢查來改進 JS 鬆散的語法,提高代碼健壯性;
  • 谷歌的 Dart 則是爲瀏覽器引入新的虛擬機去直接運行 Dart 程序以提高性能;
  • 火狐的 asm.js 則是取 JS 的子集,JS 引擎針對 asm.js 作性能優化。

以上嘗試各有優缺點,其中:github

  • TypeScript 只是解決了 JS 語法鬆散的問題,最後仍是須要編譯成 JS 去運行,對性能沒有提高;
  • Dart 只能在 Chrome 預覽版中運行,無主流瀏覽器支持,用 Dart 開發的人很少;
  • asm.js 語法太簡單、有很大限制,開發效率低。

三大瀏覽器巨頭分別提出了本身的解決方案,互不兼容,這違背了 Web 的宗旨; 是技術的規範統一讓 Web 走到了今天,所以造成一套新的規範去解決 JS 所面臨的問題迫在眉睫。web

因而 WebAssembly 誕生了,WebAssembly 是一種新的字節碼格式,主流瀏覽器都已經支持 WebAssembly。 和 JS 須要解釋執行不一樣的是,WebAssembly 字節碼和底層機器碼很類似可快速裝載運行,所以性能相對於 JS 解釋執行大大提高。 也就是說 WebAssembly 並非一門編程語言,而是一份字節碼標準,須要用高級編程語言編譯出字節碼放到 WebAssembly 虛擬機中才能運行, 瀏覽器廠商須要作的就是根據 WebAssembly 規範實現虛擬機。shell

2、WebAssembly是什麼?

WebAssembly(縮寫 Wasm)是基於堆棧虛擬機的二進制指令格式。Wasm爲了一個可移植的目標而設計的,可用於編譯C/C+/RUST等高級語言,使客戶端和服務器應用程序可以在Web上部署。

上面這段話是來自官方的定義。

咱們能夠從字面上理解,WebAssembly的名字帶個彙編Assembly,因此咱們從其名字上就能知道其意思是給Web使用的彙編語言,是經過Web執行低級二進制語法。可是WebAssembly並非直接用匯編語言,而是提供了抓換機制(LLVM IR),把高級別的語言(C,C++和Rust)編譯爲WebAssembly,以便有機會在瀏覽器中運行。能夠看出來它實際上是一種運行機制,一種新的字節碼格式(.wasm),而不是新的語言。

在這裏插入圖片描述

3、MAC安裝Emscripten

若是要把一個C/C++程序編譯成一個.wasm文件,是須要編譯工具來完成的。WebAssembly 社區推薦經常使用工具:

  • Emscripten:能把 C、C++代碼轉換成 wasm、asm.js;

  • Binaryen:提供更簡潔的 IR,把 IR 轉換成 wasm,而且提供 wasm 的編譯時優化、wasm 虛擬機,wasm 壓縮等功能。

1. 環境依賴

  • Git
  • CMake
  • brew install cmake
  • Python 2.7.x 或者更高版本,默認安裝過

2. 編譯Emscripten

接下來,您須要經過源碼本身編譯一個Emscripten。運行下列命令來自動化地使用Emscripten SDK。

git clone https://github.com/juj/emsdk.git

cd emsdk

# 編譯源碼
./emsdk install latest

# 激活sdk
./emsdk activate latest

#設置環境變量
source ./emsdk_env.sh

在運行上述命令的時候,可能會遇到以下問題:

  • ./emsdk install latest 報錯:

    likai@likaideMacBook-Pro:~/resource/emsdk$ ./emsdk install latest
    
    Installing SDK 'sdk-releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'..
    Installing tool 'node-12.18.1-64bit'..
    Error: Downloading URL 'https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v12.18.1-darwin-x64.tar.gz': <urlopen error unknown url type: https>
    Warning: Possibly SSL/TLS issue. Update or install Python SSL root certificates (2048-bit or greater) supplied in Python folder or https://pypi.org/project/certifi/ and try again.
    Installation failed!
  • 解決辦法:

    簡單看了emsdk的內容,發現這個命令調用的是emsdk.py文件,因此使用 ./emsdk.py install latest便可解決。

likai@likaideMacBook-Pro:~/resource/emsdk$ ./emsdk.py install latest

    Installing SDK 'sdk-releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'..
    Installing tool 'node-12.18.1-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/node-v12.18.1-darwin-x64.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v12.18.1-darwin-x64.tar.gz, 20873670 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/node-v12.18.1-darwin-x64.tar.gz' to '/Users/likai/hisun/resource/emsdk/node/12.18.1_64bit'
    Done installing tool 'node-12.18.1-64bit'.
    Installing tool 'python-3.7.4-2-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/python-3.7.4-2-macos.tar.gz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/python-3.7.4-2-macos.tar.gz, 25365593 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/python-3.7.4-2-macos.tar.gz' to '/Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit'
    Done installing tool 'python-3.7.4-2-64bit'.
    Installing tool 'releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'..
    Downloading: /Users/likai/hisun/resource/emsdk/zips/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-wasm-binaries.tbz2 from https://storage.googleapis.com/webassembly/emscripten-releases-builds/mac/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f/wasm-binaries.tbz2, 69799761 Bytes
    Unpacking '/Users/likai/hisun/resource/emsdk/zips/7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-wasm-binaries.tbz2' to '/Users/likai/hisun/resource/emsdk/upstream'
    Done installing tool 'releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'.
    Running post-install step: npm ci ...
    Done running: npm ci
    Done installing SDK 'sdk-releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit'.

一樣激活 Emscripten也是使用 ./emsdk.py activate latest

likai@likaideMacBook-Pro:~/resource/emsdk$ ./emsdk.py activate latest

    Setting the following tools as active:
       node-12.18.1-64bit
       python-3.7.4-2-64bit
       releases-upstream-7a7f38ca19da152d4cd6da4776921a0f1e3f3e3f-64bit

    Next steps:
    - To conveniently access emsdk tools from the command line,
      consider adding the following directories to your PATH:
        /Users/likai/hisun/resource/emsdk
        /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin
        /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin
        /Users/likai/hisun/resource/emsdk/upstream/emscripten
    - This can be done for the current shell by running:
        source "/Users/likai/hisun/resource/emsdk/emsdk_env.sh"
    - Configure emsdk in your bash profile by running:
        echo 'source "/Users/likai/hisun/resource/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile

source ./emsdk_env.sh

likai@likaideMacBook-Pro:~/resource/emsdk$ source ./emsdk_env.sh

    Adding directories to PATH:
    PATH += /Users/likai/hisun/resource/emsdk
    PATH += /Users/likai/hisun/resource/emsdk/upstream/emscripten
    PATH += /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin
    PATH += /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin

    Setting environment variables:
    EMSDK = /Users/likai/hisun/resource/emsdk
    EM_CONFIG = /Users/likai/hisun/resource/emsdk/.emscripten
    EM_CACHE = /Users/likai/hisun/resource/emsdk/upstream/emscripten/cache
    EMSDK_NODE = /Users/likai/hisun/resource/emsdk/node/12.18.1_64bit/bin/node
    EMSDK_PYTHON = /Users/likai/hisun/resource/emsdk/python/3.7.4-2_64bit/bin/python3

3. 驗證

emcc -v 不報錯就成功了

likai@likaideMacBook-Pro:~/resource/emsdk$ emcc -v

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 2.0.3
clang version 12.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project a39423084cbbeb59e81002e741190dccf08b5c82)
Target: x86_64-apple-darwin19.4.0
Thread model: posix
InstalledDir: /Users/likai/hisun/resource/emsdk/upstream/bin
shared:INFO: (Emscripten: Running sanity checks)

獲取幫助 emcc --help,內容過多就不展現了。

看下emcc 的版本是2.0.3

likai@likaideMacBook-Pro:~/resource/emsdk$  emcc --version

emcc (Emscripten gcc/clang-like replacement) 2.0.3 (43fcfd2938b72c57373a910ece897b27aa298852)
Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

4、WebAssembly簡單使用和分析

到這裏WebAssembly的編譯工具已經安裝好了,咱們使用兩個官方樣例,看一下WebAssembly是如何使用的,方便後面的學習。

當使用Emscripten來編譯的時候有不少種不一樣的選擇,咱們介紹其中主要的2種:

  • 編譯到 wasm 而且生成一個用來運行咱們代碼的HTML,將全部 wasm 在web環境下運行所須要的 「膠水」 JavaScript代碼都添加進去。

  • 編譯到 wasm,使用JavaScript調用wasm裏邊的方法。

1. 生成 HTML 和 JavaScript

  • 找個目錄建立hello_world.c文件

    #include <stdio.h>
    
    int main(int argc, char ** argv) {
      printf("Hello World\n");
    }
  • 使用剛纔已經配置過的終端,找到hello_world.c文件,執行以下命令

    emcc ./hello_world.c -s WASM=1 -o ./hello_world.html
    • emcc 是Emscripten編譯器行命令

    • hello_world.c 是咱們的輸入文件

    • -s WASM=1 指定咱們想要的wasm輸出形式。若是咱們不指定這個選項,Emscripten默認將只會生成asm.js。(可參考 emcc --help 參數說明)

    • -o ./hello_world.html 指定這個選項將會生成HTML頁面來運行咱們的代碼,而且會生成wasm模塊,以及編譯和實例化wasm模塊所須要的「膠水」js代碼,這樣咱們就能夠直接在web環境中使用了。
    likai@likaideMacBook-Pro:~/resource/emsdk/demo$ emcc ./hello_world.c -s WASM=1 -o ./hello_world.html
    shared:INFO: (Emscripten: Running sanity checks)
    
    likai@likaideMacBook-Pro:~/resource/emsdk/demo$ ls
    hello_world.c    hello_world.html hello_world.js   hello_world.wasm

    執行後會產生三個新文件:

    • hello_world.wasm 二進制的wasm模塊代碼,雖然本地打不開,可是瀏覽器能夠幫忙翻譯。
    • hello_world.js 一個包含了用來在原生C函數和JavaScript/wasm之間轉換的膠水代碼的JavaScript文件
    • hello_world.html 一個用來加載,編譯,實例化你的wasm代碼而且將它輸出在瀏覽器顯示上的一個HTML文件
  • 啓動http服務命令,查看運行結果

    emrun --no_browser --port 8080 ./hello_world.html

    likai@likaideMacBook-Pro:~/resource/emsdk/demo$ emrun --no_browser --port 8080 ./hello_world.html
    
    Web server root directory: /Users/likai/hisun/resource/emsdk/demo
    Now listening at http://0.0.0.0:8080/
    • emrun 這個命令也是emsdk中自帶的直接使用便可。

    在這裏插入圖片描述

    能夠看到原來helloworld.c文件中打印的內容如今了瀏覽器中。我很好奇C代碼中的打印結果是怎麼跑到瀏覽器的控制檯上的。看似很簡單的操做實際上Emscripten作了不少事,點開生成膠水代碼hello_world.js看了下,裏面寫了不少代碼2000多行嘞,加載wasm,處理內存分配、內存釋放、垃圾回收、函數調用,封裝了各類方法。編譯後的js文件我放在了gihub中,點擊查看 hello_world.js
    簡單分析一下膠水代碼的內容,有助於咱們對WebAssembly的理解,對於後面的使用會頗有幫助。

先一塊兒看下.wasm的真容,上面提到了.wasm是個二進制文件,打不開,想要看裏面內容的話推薦反編譯工具wasm2wast,固然瀏覽器也能夠解析,咱們經過瀏覽器簡單看下。 右鍵打開控制檯-->Sources-->hello_world.wasm

在這裏插入圖片描述

果真這個文件看得不太懂,看到了module,我猜這大概是個模塊,我找到了main函數,不知道是否是hello_world.c的main,咱們仍是看膠水代碼吧。

在這裏插入圖片描述

從膠水代碼hello_world.js中能夠看到,載入了WebAssembly彙編模塊(.wasm),原來這個.wasm被膠水代碼加載了一下,核心部分以下:

function instantiateArrayBuffer(receiver) {
    return getBinaryPromise().then(function(binary) {
      return WebAssembly.instantiate(binary, info);
    }).then(receiver, function(reason) {
      err('failed to asynchronously prepare wasm: ' + reason);

      abort(reason);
    });
  }

     // Prefer streaming instantiation if available.
  function instantiateAsync() {
    if (!wasmBinary &&
        typeof WebAssembly.instantiateStreaming === 'function' &&
        !isDataURI(wasmBinaryFile) &&
        // Don't use streaming for file:// delivered objects in a webview, fetch them synchronously.
        !isFileURI(wasmBinaryFile) &&
        typeof fetch === 'function') {
      fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function (response) {
        var result = WebAssembly.instantiateStreaming(response, info);
        return result.then(receiveInstantiatedSource, function(reason) {
            // We expect the most common failure cause to be a bad MIME type for the binary,
            // in which case falling back to ArrayBuffer instantiation should work.
            err('wasm streaming compile failed: ' + reason);
            err('falling back to ArrayBuffer instantiation');
            return instantiateArrayBuffer(receiveInstantiatedSource);
          });
      });
    } else {
      return instantiateArrayBuffer(receiveInstantiatedSource);
    }
  }

主要作了以下幾件事情:

  • 嘗試使用WebAssembly.instantiateStreaming()方法建立wasm模塊的實例;
  • 若是流式建立失敗,則改用WebAssembly.instantiate()方法建立實例;
  • 成功實例化後的返回值交由receiveInstantiatedSource()方法處理。

    receiveInstantiatedSource()代碼

    function receiveInstance(instance, module) {
            var exports = instance.exports;
            Module['asm'] = exports;
            removeRunDependency('wasm-instantiate');
        }
    
        ......
    
        function receiveInstantiatedSource(output) {
            // 'output' is a WebAssemblyInstantiatedSource object which has both the module and instance.
            // receiveInstance() will swap in the exports (to Module.asm) so they can be called
            assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?');
            trueModule = null;
            // TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line.
            // When the regression is fixed, can restore the above USE_PTHREADS-enabled path.
            receiveInstance(output['instance']);
      }

    receiveInstantiatedSource()方法調用了receiveInstance()方法,後者的這條指令:

    Module['asm'] = exports;

將wasm模塊實例的導出對象傳給了Module的子對象asm。假若咱們在上述函數中手動添加打印實例導出對象的代碼。

function receiveInstance(instance, module) {
      ... ...
      Module['asm'] = exports;
      console.log(Module['asm']);  //print instance.exports
      ... ...

在這裏插入圖片描述

因而可知,上述一系列代碼運行後,Module['asm']中保存了WebAssembly實例的導出對象——而導出函數恰是WebAssembly實例供外部調用最主要的入口。

看看我理解的對不,wasm的編譯器把C代碼編譯了.wasm文件,這個文件是個彙編代碼,裏面有C代碼的內容,膠水代碼去加載.wasm文件,經過WebAssembly實例對外提供了C代碼裏面的方法,而後使用javascript調用C代碼。最後給人的感受就是瀏覽器上能運行C語言的程序。

咱們再一塊兒細品下官方原話(翻譯過的):

WebAssembly(縮寫 Wasm)是基於堆棧虛擬機的二進制指令格式。Wasm爲了一個可移植的目標而設計的,可用於編譯C/C+/RUST等高級語言,使客戶端和服務器應用程序可以在Web上部署。
  • Wasm是基於堆棧虛擬機的二進制指令格式,hello_world.wasm本地打開是個二進制指令格式。
  • 可用於編譯C/C+/RUST等高級語言,使用Emscripten編譯hello_world.c文件。
  • 使客戶端和服務器應用程序可以在Web上部署。 確實在瀏覽器上跑起來了。
  • Wasm爲了一個可移植的目標而設計的。要是這麼說的話,我豈不是能夠把加密工具,編譯成wasm,而後經過膠水代碼來調用了麼,下一篇咱們一塊兒搞一下。

2. 編譯到 wasm,使用JavaScript調用wasm裏邊的方法。

這個很好理解,就是在編譯的時候,不生成默認推薦的html,只生成wasm,而後直接調用wasm便可。這就要咱們本身寫膠水代碼,下面看個簡單的例子。步驟以下:

  1. 寫一個test.c文件,裏面是加減乘除計算。
  2. 編譯成.wasm文件
  3. 寫一個html,調用.wasm文件
  • test.c文件
char* toChar (char* str) {
  return str;

}

int add (int x, int y) {
  return x + y;

}

int square (int x) {
  return x * x;

}
  • 編譯成.wasm文件

    emcc ./test.c -Os -s WASM=1 -s SIDE_MODULE=1 -o ./test.wasm

    這個命令好像和上面不同,解釋下:

    • emcc就是Emscripten編譯器,
    • test.c是咱們的輸入文件
    • Os表示此次編譯須要優化(能夠指定優化策略。emcc --help)
    • -s WASM=1表示輸出wasm的文件,由於默認的是輸出asm.js
    • -s SIDE_MODULE=1表示就只要這一個模塊,不要給我其餘亂七八糟的代碼
    • -o test.wasm是咱們的輸出文件。
  • 寫一個html,調用.wasm文件。test.html 這兩個函數是關鍵:
function loadWebAssembly (path, imports = {}) {
        return fetch(path) // 加載文件
               .then(response => response.arrayBuffer()) // 轉成 ArrayBuffer
               .then(buffer => WebAssembly.compile(buffer))
               .then(module => {
                 imports.env = imports.env || {}
                 // 開闢內存空間
                 imports.env.memoryBase = imports.env.memoryBase || 0

                 if (!imports.env.memory) {
                   imports.env.memory = new WebAssembly.Memory({ initial: 256 })
                 }
                 // 建立變量映射表
                 imports.env.tableBase = imports.env.tableBase || 0

                 if (!imports.env.table) {
                   // 在 MVP 版本中 element 只能是 "anyfunc"
                   imports.env.table = new WebAssembly.Table({ initial: 0, element: 'anyfunc' })
                 }
                 // 建立 WebAssembly 實例
                 return new WebAssembly.Instance(module, imports)
               })
     }  

            // 加載wasm文件
    loadWebAssembly('test.wasm')
          .then(instance => {
            //調用c裏面的方法
            const toChar = instance.exports.toChar
            const add = instance.exports.add
            const square = instance.exports.square

            console.log('return:   ', toChar("12352324"))
            console.log('10 + 20 =', add(10, 20))
            console.log('3*3 =', square(3))
            console.log('(2 + 5)*2 =', square(add(2 + 5)))
      })
有了第一個案例的理解,就大概知道這個意思了,建立了一個WebAssembly的實例,返回WebAssembly導出對象,調用了test.c裏面的函數。這裏面有一些膠水代碼語法相關的知識。[MDN Web docs-WebAssembly](./https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly)
  • 運行結果

在這裏插入圖片描述

  • test.wasm

在這裏插入圖片描述

能夠看到優化後的wasm文件,只有這幾個函數了,而且能夠看出包含導出test.c中的函數。

5、總結

咱們今天經過兩個簡單的例子講述了WebAssembly的使用,也進一步理解了WebAssembly是什麼,總體的流程是這樣的:

在這裏插入圖片描述

使用Emscripten編譯C語言源代碼,生成.wasm文件和膠水代碼,經過javascript調用膠水代碼或者.wasm,使C語言的程序在瀏覽器中運行。

以上就是這篇文章要分享的所有內容了,下一篇,基於wasm的加密工具。

文章參考

Webassembly官方網站

MDN Web docs-WebAssembly

中文原文


Netwarps 由國內資深的雲計算和分佈式技術開發團隊組成,該團隊在金融、電力、通訊及互聯網行業有很是豐富的落地經驗。Netwarps 目前在深圳、北京均設立了研發中心,團隊規模30+,其中大部分爲具有十年以上開發經驗的技術人員,分別來自互聯網、金融、雲計算、區塊鏈以及科研機構等專業領域。Netwarps 專一於安全存儲技術產品的研發與應用,主要產品有去中心化文件系統(DFS)、去中心化計算平臺(DCP),致力於提供基於去中心化網絡技術實現的分佈式存儲和分佈式計算平臺,具備高可用、低功耗和低網絡的技術特色,適用於物聯網、工業互聯網等場景。公衆號:Netwarps

相關文章
相關標籤/搜索