在上一篇文章中我們探討了WASM在服務端的巨大潛力。這篇文章將從技術角度出發,以將 Rust 程式、C 程式編譯成 WASM 的例項來深入解讀 WebAssembly(Wasm),並探討了 WASM 在區塊鏈、硬體以及面向服務的架構(SOA)的實現。
本文作者: Second State 的研究員、開源核心開發 Tim McCallum。
以下為正文:
本文不僅僅是對 Wasm 的技術探討,還在更廣義的範圍內討論 了Wasm 未來的潛力。
技術示例1:把一個簡單的 Rust 程式編譯成Wasm,並部署到一個獨立的 Wasm 虛擬機器(稱為WAVM)上。
技術示例2:編寫一個 C 程式,然後將其編譯為 Wasm 並部署在 x86_64 硬體(macOS Catalina)上。在這個示例中,我們將使用 Fastly 的本地 WebAssembly 編譯器和稱為 Lucet 的執行時來執行。
本文還將討論:
- Wasm在區塊鏈(全球去中心化計算)中的應用實現
- 硬體實現(可移植binary )
- 面向服務的架構(SOA) 實現
什麼是 Wasm?
WASM 是一種接近機器的、獨立於平臺的、低階的、類似於彙編的語言(Reiser and Bläser,2017)。 Wasm 讓 Web 有了安全、快速、可移植的低階程式碼(Rossberg等,2018)。
Wasm 計算模型基於堆疊機器(譯者注:一種計算模型),指令通過隱式的運算元棧控制值,使用(出棧)引數值併產生或返回(入棧)結果值(webassembly.github.io,2019)。
Wasm 得到了極大的發展
下圖是過去幾年“ WebAssembly”學術論文的數量。
可以看出, 與“ WebAssembly” 相關的學術論文急劇增加,同時包含關鍵詞“ WebAssembly”和“ Blockchain”兩個詞的論文數量也呈上升趨勢。
本文將分別討論瀏覽器內 Wasm 的實現和區塊鏈中的 Wasm 實現。
瀏覽器內 Wasm 實現
WASM 的設計實現了漸進式 Web 開發(Webassembly.org,2019)。 Wasm 在瀏覽器中有許多讓人眼前一亮的實現。
瀏覽器內 Wasm 實現案例之一:線上 Wasm 迷宮遊戲。
在編譯後,這個網頁版遊戲的大小不超過2048位元組!
瀏覽器內 Wasm 實現的案例之二:同樣抓人眼球的 wasm-flate 的壓縮/解壓縮軟體。
Wasm-flate 是當前瀏覽器中速度最快的壓縮和解壓軟體。這種瀏覽器內的 Wasm 執行使 Web 開發者有機會將強大的新功能無縫整合到其 Web 應用程式中。這樣的 Wasm 開發意味著終端使用者不需要安裝第三方系統級應用,也無需在第三方系統級應用之間切換。
瀏覽器中的像 Wasm-flate 這樣的 Wasm 應用程式能否最終取代傳統的系統級競品應用程式,如WinZip?
Wasm 在區塊鏈中的實現
比特幣和以太坊使用基於堆疊的架構,該架構與 WebAssembly 基於堆疊的架構相似。
當然,每個獨特的基於堆疊的虛擬機器都有一些差異。例如,在 Wasm 中找不到類似大家熟知的堆疊專案重複操作的功能,例如比特幣的 OP_DUP 操作碼和以太坊的 DUP1 至 DUP16 操作碼。
以太坊黃皮書中的複製操作。幸好,Wasm 為每個 Wasm 函式提供了固定數量的區域性變數。這些變數將資訊儲存在該特定函式本地的單個索引空間內。更值得關注的是,還有其他方法可以模擬特定堆疊行為。
另一個重要的差異是每次操作可入棧的專案數量。仔細檢視以太坊黃皮書(上圖),能夠注意到兩列標記為 δ 和 α 的列。
標記為 δ 的列表示要從堆疊中刪除的專案數。標記為 α 的下一列代表要放置在堆疊上的其它專案的數量。以太坊虛擬機器(EVM)上的每個操作都可以將許多專案入棧。在上面的示例中,DUP16 能夠將17個專案入棧。
但是,在當前版本的 Wasm 中,一條指令只能將一個結果值入棧(webassembly.github.io,2019)。
還有許多像這樣的細微差別。
毫無疑問,構建能將任何高階區塊鏈智慧合約原始碼轉換為可執行的 Wasm 式程式碼的編譯器,這樣的工作非常複雜且繁重。
但 Second State 的開發者最近構建了一個名為 SOLL 的編譯器(點選此處有視訊demo),這是第一個允許在 Ewasm 測試網上進行以太坊 Solidity 智慧合約的編譯、部署、互動的編譯器。
諸如此類的開拓性工作,標誌著去中心化網路中數字價值和資料的交換,以及裝置之間基於規則的互動開始了。將基於瀏覽器的裝置編織到已經去中心化的區塊鏈架構中,可以使無需許可、抗審查、沒有邊界、安全並基於Web的交易成為主流。
在今年的Devcon5(以太坊開發者大會)上,與以太坊的開發者進行交流後,Second State 也正在考慮構建從以太坊的中間語言Yul 到 LLVM 到 Ewasm 的編譯器。
這項新增的工作可能促成用C ++,Rust,Vyper等語言編寫的智慧合約,得以部署到以太坊的 Wasm 區塊鏈實現中。
很快,大家會意識到,引入新語言(跨編譯器工具鏈的不同部分)會在多語言協作方面帶來無窮的可能性。
這是 Wasm 潛在的巨大益處。
Wasm——更接近硬體
Wasm不僅僅是Web瀏覽器或區塊鏈虛擬機器的位元組碼。這個你們知道嗎?
圖片出處:Raimond Spekking / CC BY-SA 4.0(Wikimedia Commons)Web 跑在不同的瀏覽器、不同型別的裝置、機器體系結構和作業系統上。針對Web的程式碼必須獨立於硬體和平臺,這樣一來,應用程式在不同型別的硬體上執行,可以執行相同的結果(Rossberg等,2018)。
即使是很小的shell、移動裝置、桌上型電腦和 IoT 裝置都能承載 Wasm 的執行環境。Wasm 能夠驅動微晶片,乃至整個資料中心。(Webassembly.org,2019)。
Docker 的聯合創始人所羅門·海克斯(Solomon Hykes)今年早些時候表示,“如果在2008年已經有了 WASM + WASI,我們根本不需要建立 Docker。WASM 就是這麼重要。伺服器端的 Webassembly 是計算的未來。”
可以預見,Wasm 對於全球軟體開發社群中絕大部分開發者都很有吸引力。Wasm 的設計帶來了令人難以置信的速度、靈活性和可移植性,因此,我們顯然能推斷出 Wasm 將在世界上即將到來的每種計算解決方案(無論是在網頁還是非網頁)中都扮演著重要角色。
技術示例1:用 Rust 編寫 Wasm
讓我們深入研究一些程式碼。我們接下來要編寫一個 Rust 程式,對其進行編譯,然後部署在獨立的 Wasm 虛擬機器上。
安裝 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
複製程式碼
新增元件
rustup component add rls rust-analysis rust-src
複製程式碼
建立新的 Rust 專案
cd ~
cargo new --lib add_numbers_via_wavm
cd add_numbers_via_wavm
複製程式碼
編輯 Cargo.toml 檔案;將 lib 部分新增到檔案末尾,如下所示
[lib]
name = "adding_lib"
path = "src/adding.rs"
crate-type =["cdylib"]
複製程式碼
關於“ cdylib”的簡要說明
“如果您打算使用 C 語言(或通過 C FFI 的另一種語言)建立要使用的庫,那麼 Rust 無需在最終目的碼中包含特定於 Rus t的內容。對於這樣的庫,您需要在 Cargo.toml 中使用 cdylib crate 型別”(Doc.rust-lang.org,2019)
接下來我們新增必要的 Wasm 軟體和配置
rustup target add wasm32-wasi
rustup override set nightly
複製程式碼
現在,建立一個名為 〜/ .cargo / config 的新檔案,並將以下構建文字放入此新建立的配置檔案中
[build]
target = "wasm32-wasi"
複製程式碼
注:rust-lang的
wasm32-unknown-wasi
最近被重新命名為wasm32-wasi
。
Rust原始碼
在 /home/ubuntu/add_numbers_via_wavm/src 目錄中建立一個名為 adding.rs (adding.rs/) 的檔案,並填上下面的 Rust 程式碼
#[no_mangle]
pub extern fn main(a: i32, b: i32) {
let z = a + b;
println!("The value of x is: {}", z);
}
複製程式碼
編譯原始碼
這將在
/home/ubuntu/add_numbers_via_wavm/target/wasm32-wasi/release
目錄中建立一個adding_lib.wasm檔案。
cargo build –release
複製程式碼
我們馬上就到執行該 wasm 檔案的步驟了,但是,我們必須先安裝 WebAssembly 虛擬機器。
建立獨立的 Wasm 虛擬機器
通常情況下,我們使用像 wasm-pack 這樣的軟體,可以讓開發者將 Rust 生成的 WebAssembly 與 JavaScript 整合在一起,並且像 wasm-bindgen 這樣的軟體可以促進 wasm 模組和 JavaScrit 之間的高階互動。
但是我們在這裡要做的是完全不同的。
** 我們沒有使用Javascript,Node.js,也沒在瀏覽器中執行任何程式。我們是在獨立的 WebAssembly虛擬機器中執行Rust程式,該虛擬機器專門設計用於非 Web 應用程式。**
安裝名為 WAVM 的虛擬機器
sudo apt-get install gcc
sudo apt-get install clang
wget https://github.com/WAVM/WAVM/releases/download/nightly%2F2019-11-04/wavm-0.0.0-prerelease-linux.deb
sudo apt install ./wavm-0.0.0-prerelease-linux.deb
複製程式碼
在虛擬機器上執行 Wasm
讓我們嘗試使用下面的 WAVM 命令執行我們剛剛編譯的 Rust to Wasm 程式碼
wavm run --abi=wasi --function=main ~/add_numbers_via_wavm/target/wasm32-wasi/release/adding_lib.wasm 2 2
The value of x is: 4
複製程式碼
正如您在上面看到的,我們可以傳入兩個值(2和2),WebAssembly 虛擬機器(WAVM)可以計算總和並通過控制檯將答案返回給我們。 真是激動人心!!!
技術示例2:在x86_64硬體上部署C程式
我們剛剛在獨立虛擬機器中執行了 Rust / Wasm。現在,讓我們使用 Lucet 在 x86_64 硬體(macOS Catalina)上部署 C 程式。 Lucet 不是虛擬機器,而是 WebAssembly 編譯器和執行時,它支援 Wasm 在伺服器端執行。
與 WAVM 一樣,Lucet 還支援 WebAssembly 系統介面(WASI)。WASI 是一種新提議的標準,用於將低階介面安全地公開給檔案系統、網路和其他系統設施。
我們通過從原始碼編譯 Lucet 開始此演示。
然後,我們建立 hello world C 原始碼檔案(如下所示)
#include <stdio.h>
int main(int argc, char* argv[])
{
if (argc > 1) {
printf("Hello from Lucet, %s!\n", argv[1]);
} else {
puts("Hello, world!");
}
return 0;
}
複製程式碼
……位置改為安裝 Lucet 的地方……
cd /opt/lucet/bin
複製程式碼
……將 C 程式碼編譯為 Wasm……
./wasm32-wasi-clang ~/lucet/tpmccallum/hello.c -o ~/lucet/tpmccallum/hello.wasm
複製程式碼
……然後可以看到下面的命令列……
然後,我們將 hello.wasm 檔案傳遞到下一個命令,該命令生成 hello.so
lucetc-wasi ~/lucet/tpmccallum/hello.wasm -o ~/lucet/tpmccallum/hello.so
複製程式碼
上一條命令的輸出如下所示。
為了完成此演示,我們最終執行以下命令,該命令以 Mach-O 64位動態連結的共享庫 x86_64 檔案(在macOS Catalina上)執行該程式。
我們在這裡所做的基本上是在伺服器端執行 Wasm。
其它Wasm編譯器和執行時
如前所述,我們有了越來越多的 Wasm 編譯器和執行時。其中包括英特爾的 Wasm Micro 執行時和 Wasm 虛擬機器。除了這些專案之外,還有 Wasmer 專案,該專案可以使開發者將一切編譯為 WebAssembly,然後在任何作業系統上執行它或將其嵌入其他語言中(Wasmer.io,2019)。例如,您先可以使用 Go、Rust、Python、Ruby、PHP、C、C ++ 和 C#編寫程式碼。之後將程式碼編譯為 Wasm,然後將該 Wasm 程式碼嵌入到上述任何一種語言中。 Wasmer 還致力於建立可在任何平臺上執行的二進位制檔案。
Wasm 的內部工作機制
讓我們簡要介紹一下 Wasm 的內部工作機制,然後列出有助於您建立一個手寫的 Wasm 應用程式的資源。
數字指令
數字指令按值型別劃分。對於每種型別,可以區分幾個子類別:
- 一元運算: 消耗一個運算數併產生一個相應型別的結果
- 二進位制運算: 消耗兩個運算數併產生一個相應型別的結果
- 比較: 消耗各自型別的兩個運算元併產生布林整數結果
- 測試: 消耗相應型別的一個運算元併產生布林整數結果
- 轉換: 消耗一種型別的值併產生另一種型別的結果
使用載入和儲存指令訪問記憶體。所有值以以小端排序讀取和寫入。
變數指令,提供對區域性和全域性變數的訪問。
控制指令包括if,loop和其他對程式碼執行控制有影響的指令。
全球狀態
“儲存”代表可由 WebAssembly 程式操縱的所有全域性狀態。
儲存為抽象機的生命週期分配的每個函式例項、表例項、記憶體例項和全域性例項保留一個單獨的索引位置。通過使用一個地址可以引用/訪問這些單獨的索引位置。
地址是對執行時物件的動態全域性唯一引用(webassembly.github.io,2019)。
值型別對 WebAssembly 程式碼可用於計算的單個值以及變數接受的值進行分類。 i32 和 i64 型別分別將 32 位和 64 位整數分類。整數不是固有地帶符號或無符號的,它們的解釋由單個操作確定(webassembly.github.io,2019)。 f32 和 f64 型別表示浮點值。
如下所示,每個檔案均由一個位元組顯式地編碼。
Wasm程式碼線上編輯器
現在,您已經基本瞭解 Wasm,是時候編寫和部署手寫 Wasm 程式碼了。我們可以使用基於 Web 的線上 WebAssemblyStudio 應用程式來執行此任務。
Wasm有文字檔案格式“ .wat”和二進位制檔案格式“ .wasm”。 WebAssemblyStudio 應用程式(如上圖所示)使我們能夠建立各種源格式的 Wasm 應用程式。包括 Wasm 的上述文字格式 .wat。
這是一個簡單功能,在編輯器內以 Wasm 文字編寫。
那麼這個功能可以做什麼呢?
- 如上所示,第1行定義了模組。有效的模組可以少至文字“(模組)”
- 第2行定義了稱為“ add”的功能。此函式採用兩個 i32 引數,“ firstValue”和“ secondValue”。它返回一個 i32 變數
- 該程式碼的第3行將 firstValue 的值入棧
- 第4行將 secondValue 的值入棧
- 第5行從堆疊中彈出兩個當前項,然後計算這兩個值的總和並將該總和值入棧
- 當函式顯式宣告一個返回值時,在這種情況下(結果為 i32),始終將堆疊中剩餘的最後一項指定為函式承諾返回的值。
您可能想知道如何根據堆疊中專案的完美數量來考慮每個單個操作的引數需求以及整個函式的返回承諾。
有了對功能簽名進行分類的“功能型別”,可以提前計算操作和專案之間的關係。更具體地說,函式型別定義每個單獨的操作“出棧”的專案數量以及該操作然後“壓入”堆疊的專案數量。此資訊允許執行顯式程式碼驗證。
下面是新增操作的說明(彈出兩 個i32 值併入棧一個 i32 值)。
i32.add
------------------------
[pops off] [pushes]
[i32 i32] -> [i32]
複製程式碼
手動將 WAT 轉換為 Wasm
雖然像 WebAssemblyStudio 這樣的線上產品可以為我們處理所有編譯和轉譯。我們還可以在命令列中執行任務,例如建立 Wasm輸出。
tpmccallum$ ./wat2wasm adding_numbers.wat -o adding_numbers.wasm
複製程式碼
上文提及的命令列 C 到 Wasm 的示例,Wasm 二進位制格式的輸出對人眼而言是難以辨認的。這些可執行二進位制檔案並非設計成由作業系統(即 vi 或 emacs)本地檢視。 值得慶幸的是,我們可以依靠預先構建的 Wasm 軟體庫來轉換 Wasm 程式碼。
Wabt 發音為“ wabbit”,是非常好用的 Wasm工具庫。 Wabt 可以執行以下任務,包括但不限於將 Wasm文字轉換為 Wasm 二進位制(wat2wasm),將二進位制轉換回文字(wasm2wat),計算指令的操作碼使用量(wasm_opcodecnt),將Wasm轉換為C(wasm2c)等。
以下面的命令為例。
tpmccallum $ ./wat2wasm adding_numbers.wat -v
複製程式碼
有關此基於彙編的程式碼的完整輸出,請參見附錄A.1。
結果
回到我們的手寫 Wasm 演示。如果在 WebAssemblyStudio 中單擊“生成並執行”按鈕,我們將看到該函式新增了“ firstValue”和“ secondValue”,並且現在它返回了這些值的總和“ 2”。
結論
Wasm還處在一個很早期的發展階段。儘管如此,許多流行的程式語言的原始碼,例如C,C ++,Rust,Go 和 C#,已經可以編譯為可用於生產的 Wasm 程式碼。
這種前所未有的可移植性,對於促成開發者採用 Wasm,並進行協作,意義重大。
我們知道現在已經有了許多非常令人印象深刻的瀏覽器內 Wasm 應用程式。,越來越多的 Wasm 編譯器和執行時允許Wasm 在 Web 瀏覽器之外,在更接近硬體的地方執行。
Wasm 式的編譯器不可避免地會越來越接近硬體。這使得我們能夠開發出許許多多高效、易於移植和易於訪問且互相獨立的去中心化功能。
Wasm 具有下一代服務導向架構(SOA)的所有功能。它可以針對特定結果,也可以獨立存在,支援抽象化,並且可以輕鬆共享和使用其它底層功能單位。
這是一個振奮人心的合作領域。許多專案的繁重開發工作正在進行,所有這些艱苦的工作,會讓大家看到偉大的成就。
參考文獻
-
Doc.rust-lang.org. (2019). cdylib crates for C interoperability — The Edition Guide. [線上資源] 連結: doc.rust-lang.org/edition-gui… [訪問於2019年11月6日].
-
GitHub/appcypher. (2019). Awesome WebAssembly Languages. [線上資源] 連結: github.com/appcypher/a… [訪問於2019年10月27日].
-
Reiser, M. and Bläser, L., 2017, October. Accelerate JavaScript applications by cross-compiling to WebAssembly. In Proceedings of the 9th ACM SIGPLAN International Workshop on Virtual Machines and Intermediate Languages (pp. 10–17). ACM.
-
Rossberg, A., Titzer, B., Haas, A., Schuff, D., Gohman, D., Wagner, L., Zakai, A., Bastien, J. and Holman, M. (2018). Bringing the web up to speed with WebAssembly. Communications of the ACM, 61(12), pp.107–115.
-
Wasmer.io. (2019). Wasmer — The Universal WebAssembly Runtime. [線上資源] 連結: wasmer.io/ [訪問於2019年11月4日].
-
webassembly.github.io. (2019). WebAssembly Specification. [線上資源] 連結: webassembly.github.io/spec/core/ [訪問於2019年10月23日].
-
Webassembly.org. (2019). Non-Web Embeddings — WebAssembly. [線上資源] 連結: webassembly.org/docs/non-we… [訪問於2019年10月28日].
附錄
20191128更新
現在,任意指令序列可以使用和生成任意數量的堆疊值。 而不是像以前那樣只推送一個產生的堆疊值。 多值的提議當前的實現,在此已經解釋清楚了,目前處在 Wasm 標準化程式的第三階段。 附錄A.1
tpmccallum$ ./wat2wasm adding_numbers.wat -v
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01 ; section code
0000009: 00 ; section size (guess)
000000a: 01 ; num types
; type 0
000000b: 60 ; func
000000c: 03 ; num params
000000d: 7f ; i32
000000e: 7f ; i32
000000f: 7f ; i32
0000010: 01 ; num results
0000011: 7f ; i32
0000009: 08 ; FIXUP section size
; section "Function" (3)
0000012: 03 ; section code
0000013: 00 ; section size (guess)
0000014: 01 ; num functions
0000015: 00 ; function 0 signature index
0000013: 02 ; FIXUP section size
; section "Export" (7)
0000016: 07 ; section code
0000017: 00 ; section size (guess)
0000018: 01 ; num exports
0000019: 03 ; string length
000001a: 6164 64 add ; export name
000001d: 00 ; export kind
000001e: 00 ; export func index
0000017: 07 ; FIXUP section size
; section "Code" (10)
000001f: 0a ; section code
0000020: 00 ; section size (guess)
0000021: 01 ; num functions
; function body 0
0000022: 00 ; func body size (guess)
0000023: 00 ; local decl count
0000024: 20 ; local.get
0000025: 00 ; local index
0000026: 20 ; local.get
0000027: 01 ; local index
0000028: 6a ; i32.add
0000029: 0b ; end
0000022: 07 ; FIXUP func body size
0000020: 09 ; FIXUP section size
複製程式碼