Rust & WebAssembly 初探
https:// lynvv.xyz/2018/07/02/Ru st_WebAssembly/
一個非常快速的概覽,文章中不涉及深度細節,深度的細節可以在代碼中找到: crypto-wasm , node-crypto
0x00
隨著2017年底,四大瀏覽器廠商全部完成對WebAssembly的初步實現,以及Webpack implementing first-class support for WebAssembly的消息公佈,越來越多的團隊在實現需求的時候將WebAssembly作為備選技術之一考慮,那麼在如今的環境(Node 8.11.3 LTS,目前階段沒有嘗試瀏覽器),和相關工具鏈( wasm-pack )下WebAssembly的使用體驗、性能到底是什麼樣的狀況呢?
0x01前置背景介紹
如果你已經對WebAssembly 已經有所了解甚至已經上手試過了,那你可以放心的跳過這一節。這一小節主要介紹一些WebAssembly 相關的非常基礎的概念
WebAssembly 簡要來說有以下三個特點:
- 二進制格式,不同於JavaScript 代碼的文本格式
- 標準化,與JavaScript 一樣,實現了WebAssembly 標準的引擎都可以運行WebAssembly,不管是在服務器端還是瀏覽器端
- 快速,WebAssembly 可以充分發揮硬件的能力,以後你甚至可以在WebAssembly 中使用SIMD 或直接與GPU 交互
而WebAssembly 誕生自瀏覽器環境,自然需要與JavaScript 交互,在目前支持WebAssembly 的環境中使用WebAssembly 大概需要以下步驟:
- 加載
因為我們需要使用WebAssembly.instantiate 來實例化一個WebAssembly 模塊,而這個方法只接受ArrayBuffer 作為第一個參數,所以只能將.wasm 文件加載為ArrayBuffer 才能將它實例化。
你可以使用fetch 加載
fetch ( url ). then ( response => response . arrayBuffer () ). then ( bytes => { // your bytes here })
或者在NodeJS 中使用fs.readFile 加載:
const bytes = fs.readFileSync('hello.wasm')
- 實例化
WebAssembly.instantiate(bytes,imporObject)
這是一個binding 的過程。 WebAssembly.instantiate 方法會將importObject 對像傳遞給wasm ,這樣在wasm 內就可以訪問到imporObject 上的屬性和方法,而WebAssembly.instantiate(bytes, imporObject) 的返回值則會將wasm 內暴露的方法交給JavaScript調用。
constimportObject={imports:{foo:arg=>console.log(arg)// 可以在 wasm 内调用
}}WebAssembly.instantiate(bytes,importObject).then(results=>{results.instance.exports.exported_func()// exports 对象上有所有 wasm 暴露的东西
})
- 調用
無參數或者參數類型為數字的時候,可以直接調用wasm 模塊內的方法,但是一旦涉及到其它複雜類型的參數或返回值的時候,就需要直接對內存進行操作才能正常的調用和獲取返回值了。
摘自WebAssembly系列(四)WebAssembly工作原理
如果你想在JavaScript和WebAssembly之間傳遞字符串,可以利用ArrayBuffer將其寫入內存中,這時候ArrayBuffer的索引就是整型了,可以把它傳遞給WebAssembly函數。此時,第一個字符的索引就可以當做指針來使用。

目前來看這是一個非常冗長和令人費解的過程,你可以在Using_the_JavaScript_API找到更多詳細的相關描述
0x02工具鏈,Emscripten與wasm-pack
提到WebAssembly 就不得不說一下Emscripten。
WebAssembly最早是由Asm.js發展而來,Emscripten與asm.js同時誕生,最初版本的asm.js就是靠Emscripten生成。
asm.js 高性能的秘密在於引擎會直接將符合asm.js 規範的代碼編譯為彙編執行,而不是像普通的JavaScript 代碼那樣先運行在虛擬機上再由引擎逐步優化。
早期的Emscripten使用llvm將靜態語言變成LLVM IR ,然後再將LLVM IR變成asm.js。而如今它還可以將LLVM IR 編譯成wasm,目前來看它是C/C++ 到asm.js/wasm 最重要的工具。
今年稍早的時候,Rust團隊公佈了Rust 2018 Roadmap ,裡面將WebAssembly的戰略地位放在了與Network services、 Command-line apps 、 Embedded devices平級的地位上,並成立了專門的小組Focus在WebAssembly的生態建設上。
隨後,rustwasm 團隊便發布了wasm-pack,這是一個能快速將Rust 代碼編譯到WebAssembly 並發佈到npm 的工具。對比原始的Emscripten 工具鏈,它有三個方面的功能讓我覺得非常強大方便:
- 無需寫巨長的編譯命令/編譯腳本,一鍵編譯
wasm-pack init
或者wasm-pack init -t nodejs
即可得到需要的wasm代碼與相應的js bindings。相較而言,這是C++的代碼使用Emscripten編譯到wasm的腳本(代碼來自fdconf 2018 : webassembly在全民直播的應用):
#!/usr/bin/env bash set -x rm -rf ./build mkdir -p ./build em++ --std=c++11 -s WASM=1 -Os --memory-init-file 0 --closure=1 -s NO_FILESYSTEM=1 -s DISABLE_EXCEPTION_CATCHING=0 -s ELIMINATE_DUPLICATE_FUNCTIONS=1 -s LEGACY_VM_SUPPORT=1 --llvm-lto 1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" -s EXPORTED_FUNCTIONS="['_sum']" ./sum.cpp -o ./build/index.html
- 不需要編寫巨長無比的js wrapper
下面是一段用C++ 編寫,使用Emscripten 編譯到wasm 之後從JavaScript 端的調用過程:
function_arrayToHeap(typedArray){constnumBytes=typedArray.length*typedArray.BYTES_PER_ELEMENT;constptr=Module._malloc(numBytes);constheapBytes=newUint8Array(Module.HEAPU8.buffer,ptr,numBytes);heapBytes.set(newUint8Array(typedArray.buffer));returnheapBytes;}function_freeArray(heapBytes){Module._free(heapBytes.byteOffset);}Module.sum=function(intArray){constheapBytes=_arrayToHeap(intArray);constret=Module.ccall("sum","number",["number","number"],[heapBytes.byteOffset,intArray.length],);_feeArray(heapBytes);returnret;}
對比來看,wasm pack 生成的包可以像調用普通JavaScript 庫一樣調用:

這是wasm-pack生成出來的文件,pkg目錄是wasm-pack自動生成的文件,裡面的內容髮佈為npm package之後,使用方可以直接require('crypto-wasm')
來使用。
- 自動生成.d.ts,對前端工具鏈非常友好
wasm pack 可以將這樣的代碼:
#[wasm_bindgen]pubfnsha256(input: &str)-> String{letmuthasher=Sha256::new();hasher.input_str(input);hasher.result_str()}
生成為:
exportfunctionsha256(arg0: string):string;
無需任何額外的配置
不過在這些優點的同時,目前wasm pack 還存在一些缺陷:
- 生成的wasm 體積太大準確來說這並不是wasm pack 工具的問題,而是Rust 語言的問題。目前一個空項目打包出來的體積大概是
250kb
左右,當然這對於NodeJS來說就不是什麼大問題了。 - 無法增量編譯,編譯速度極慢空項目2min 左右才能編譯好,而且重複編譯時間不會減少。
- 還有一些Bug
不是指生成的代碼有Bug,而是打包過程中的一些問題。比如生成的package.json 文件中的files 字段少加了文件導致發佈到npm 之後無法直接安裝使用。
0x03性能
目前這是一個非常不嚴謹的Benchmark,結果僅供參考,如果你想修改Benchmark的過程或者進行Profile,可以在crypto-wasm的bench目錄下進行修改調試。
這次Benchmark對比了4種代碼的md5
性能,它們分別是:
- NodeJS 原生crypto
- rust-crypto 庫的wasm 版本
- crypto-js
- rust-crypto Node native binding 版本
在輸入字符串為hello world!
時:
➜ node bench/md5.js md5#native x 637,185 ops/sec ±1.99% ( 79 runs sampled ) md5#wasm x 194,583 ops/sec ±17.49% ( 63 runs sampled ) md5#js x 86,228 ops/sec ±11.65% ( 66 runs sampled ) md5#binding x 1,172,619 ops/sec ±4.45% ( 85 runs sampled ) Fastest is md5#binding
在輸入字符串長度為200000
時:
➜ node bench/md5.js md5#native x 1,496 ops/sec ±2.24% ( 91 runs sampled ) md5#wasm x 809 ops/sec ±1.46% ( 91 runs sampled ) md5#js x 60.91 ops/sec ±1.68% ( 63 runs sampled ) md5#binding x 785 ops/sec ±0.75% ( 93 runs sampled ) Fastest is md5#native
由於實現的算法存在差異,所以需要更多維度的Benchmark 才能獲得wasm 在v8 下性能的準確訊息。
但就現實場景而言,可以看到如果一些庫在Node 下沒有Native 實現,並且有相關的Rust 實現,並且恰好這個Rust 實現沒有用到系統級的API ,那麼使用wasm pack 打包一個wasm 版本並在Node 下使用還是一個性能收益非常可觀並且成本並不算太高的選擇。
相較於native binding 而言,wasm 在CI 上的優勢是巨大的,wasm 可以在任意環境任意支持的Node 版本下編譯就可以全平台使用了。而native binding 如果安裝時build 成本太高需要使用prebuilt 發布的話,CI 簡直就是噩夢,需要所有平台所有支持的Node 版本下prebuilt,然後讓安裝的一方下載編譯好的binary 。
EOF
由於時間有限,所以目前只玩耍到這一步。
後面會帶來wasm 在瀏覽器中使用的體驗和多方面的對比(如果還有下一篇。