WebAssembly初探

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

這次研究 WebAssembly的過程中遇到了各種問題,我均記錄下來,并在后期可以和大家一起分享,文末放置了參考的文章,大家可以延伸閱讀。這篇文章是本系列的第一部分,主要是了解WebAssembly和WebAssembly的基本使用方法。

概述

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

一、 WebAssembly的誕生

當人們說 WebAssembly 更快的時候,一般來講是與 JavaScript 相比而言的。

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

在這里插入圖片描述

隨著性能的提升,JavaScript 可以應用到以前根本沒有想到過的領域,比如用于后端開發的 Node.js。性能的提升使得 JavaScript 的應用范圍得到很大的擴展。

在這里插入圖片描述

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

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

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

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

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

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

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

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

二、WebAssembly是什么?

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

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

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

在這里插入圖片描述

三、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.
        

四、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

  • 運行結果

在這里插入圖片描述
  • test.wasm
在這里插入圖片描述

可以看到優化后的wasm文件,只有這幾個函數了,并且可以看出包含導出test.c中的函數。

五、總結

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

在這里插入圖片描述

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

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

文章參考

Webassembly官方網站

MDN Web docs-WebAssembly

中文原文


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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容