WebAssembly 常用開發語言和工具鏈

  1. 前言

WebAssembly 作爲一項新興的技術,已經發展至 2.0 版本;相較於 1.0 版本,2.0 版本增加了更全面的指令支持和對大容量內存的友好性;同時,向量指令的加入也提高了 WebAssembly 在複雜場景下的性能表現。目前,WebAssembly 已經廣泛應用於各種 Web 和非 Web 場景,例如 Web 端的視頻渲染、編解碼、算法移植,以及非 Web 端的 Serverless、客戶端跨平臺等領域。

除了廣泛的應用場景,WebAssembly 還具有跨平臺、高效、安全的優點。它可以在各種計算機架構上運行,並且具有接近原生代碼的性能。此外,WebAssembly 的代碼可以在沙箱中運行,不會影響主機環境的穩定性和安全性,從而有助於保護用戶數據和隱私。

隨着 WebAssembly 技術的不斷進步,未來還有許多值得期待的方向。例如 WebAssembly 的多線程支持、AI 模型的推理和訓練,以及在區塊鏈領域的智能合約編寫和執行。這些發展方向有助於進一步提高 WebAssembly 的性能和功能,從而推動 Web 上更多應用場景的發展。

針對 WebAssembly 的巨大發展潛力和廣泛應用前景,社區和公司也在建設 WebAssembly 生態上投入了巨大的精力。多個流行語言已經相繼支持了 WebAssembly 產物,社區開發者們和企業或機構也相繼提供了相當多的 WebAssembly 工具。

本文將介紹當前 WebAssembly 生態中比較流行的開發語言和工具鏈,併爲讀者提供相關應用場景的介紹。值得注意的是,截至 2023 年 2 月,許多語言和工具鏈仍在不斷迭代和發展。

  1. WebAssembly 語言生態介紹

2.1 C/C++

由於 C/C++ 的歷史地位,其擁有龐大的用戶羣體以及完備的工具鏈。在 WebAssembly 生態中,C/C++ 是先行者,也是繞不開的重量級語言。現如今,C/C++ 最常用編譯工具鏈之一的 LLVM 已經把 WebAssembly 添加爲受支持的後端。

此外,社區發展了 WebAssembly System Interface 標準(簡稱 WASI,非 WebAssembly Proposal)。基於這套標準,WebAssembly 的運行環境能夠獲取系統 IO 等能力,不再依賴宿主語言,變成了一個獨立的能夠運行在非 Web 端的產品。自此,WebAssembly 的能力從 Web 端拓展到了非 Web 端,服務端成爲了 WebAssembly 下一個熱門的發展方向。從 C/C++ 的角度來看,大量使用系統調用的基礎庫能夠移植到 WebAssembly,將 WebAssembly 的能力下沉到了原生,甚至是系統層面。

2.2 Rust

Rust 和 Emscripten 如今都由 Mozilla 公司負責,因此 WebAssembly 社區的發展和 Rust 有着密不可分的聯繫。Mozilla、英特爾、RedHat 和 Fastly 公司聯合成立字節碼聯盟 (Bytecode Alliance)[1],其中 Rust 實現的 wasmtime 虛擬機就是最重量級的 WebAssembly 高性能虛擬機之一。

Rust 編譯器藉助 LLVM 的能力生成 WebAssembly 產物,因此 WebAssembly 也是 Rust 的目標語言之一。同時,Rust 兼容 C/C++ 的內存模型,無 GC,因此生成的 WebAssembly 產物體積小巧性能表現良好,與 C/C++ 生成的 WebAssembly 模塊能夠無縫銜接。

Rust 生態同樣支持 WASI 標準。因此非 Web 端應用也成了 Rust WebAssembly 生態發展的熱門方向。在 wasmtime 虛擬機 [2] 的加持下,Rust 在服務端應用同樣大有可爲。

2.3 Go

Go 作爲服務端領域的主流語言,與 WebAssembly 在服務端的應用方向不謀而合。從 Go 1.11 開始,WebAssembly 已經正式被 Go 官方支持,這說明 Go 團隊也認同了 WebAssembly 的發展潛力。同時,社區也存在 Go 實現的大量開源項目,例如 wazero[3] 就是 Go 實現的高性能 WebAssembly 虛擬機。

然而,Go 語言在 WebAssembly 社區也有令人詬病的幾個問題。首先就是 WASI 支持,雖然 Go 在服務端發力,但是由於 Go 的 WebAssembly 不支持 WASI,因而 Go 的 WebAssembly 產物不能脫離 Web 環境,這可能會阻礙 Go 的 WebAssembly 社區在服務端的發展。其次,Go 的 GC 依賴使得 WebAssembly 產物體積嚴重膨脹,難以做到 Web 端所需的輕量化。

總而言之,雖然 Go 能夠作爲 WebAssembly 開發語言使用,但是也有不小的侷限性。

2.4 AssemblyScript

AssemblyScript 使用 TypesScript 中的部分語法並做了部分修改,是一門專爲 WebAssembly 服務的語言。WebAssembly 最初就是爲了前端場景設計的,AssemblyScript 的語法讓前端開發者能夠很自然地寫出可編譯爲 WebAssembly 的程序。因此,在前端社區中,越來越多的開發者選擇 AssemblyScript 開發 WebAssembly 項目。

AssemblyScript 最初就是爲了 Web 端設計的,因此它不支持 WASI 標準。此外,AssemblyScript 生成的 WebAssembly 包含了 GC,產物大小會比 C/C++ 生成的產物大不少。AssemblyScript 底層也借用了 LLVM 的能力,它將類 TypeScript 的語言解析成 AST,生成 LLVM IR,最後生成 WebAssembly 產物。

AssemblyScript 雖然方便了前端程序員編寫 WebAssembly 項目,但是其內置的部分庫,以及生成的 WebAssembly 稍顯臃腫,優化也沒有做到最優。例如,導入導出部分生成的代碼進行了內存拷貝、pow 庫函數生成了大量冗餘代碼,這些都有待優化。

2.5 JavaScript

JavaScript 是前端領域必不可少的語言,大量的 JavaScript 項目運行在各類平臺,瀏覽器、移動端 APP、PC 桌面端都能見到它的身影。WebAssembly 作爲 Web 端的希望之星,設計之初就能夠和 JavaScript 交互使用。爲此,WebAssembly 自有一套 JS-API 標準 [4],用於規範 JavaScript 和 WebAssembly 互操作。

然而,由於 JavaScript 語言特性,它難以生成完備的 WebAssembly 產物。大量 JavaScript 項目無法無縫遷移到 WebAssembly 上。當然,想讓 JavaScript 跑在 WebAssembly 上也不是毫無辦法。生產環境中已經存在將 JavaScript 引擎編譯到 WebAssembly,運行在 WebAssembly 引擎中的例子。這種場景直覺上沒什麼應用前景,但實際上他與 Serverless 和容器化場景不謀而合。在服務端場景中,這種設計讓用戶能夠繼續編寫 JavaScript 代碼,服務端則能夠獲得較少的性能損失,獲得更小的體積和更快的啓動速度,可以說是雙贏。

2.6 其他支持的語言

除了上面提到的語言之外,還有一些主流語言以各種方式支持了 WebAssembly。例如,Lua 社區存在將 Lua 引擎編譯到 WebAssembly 的項目,它讓 Lua 腳本運行在瀏覽器上;C# 社區存在將 C# 編譯到 WebAssembly 的實驗項目,同時微軟官方也有實驗中的 C# to WebAssembly 項目。然而,這些項目大部分都處於實驗性質,或多或少存在一些限制,導致其無法用在生產環境。同時,社區也處於初步發展階段。因此,相關內容不在此做詳細介紹,讀者可參考 awesome-wasm-langs[5] 做進一步的瞭解和學習。

  1. WebAssembly 工具生態

3.1 Emscripten

3.1.1 概述

Emscripten[6] 是 Alon Zakai 創建的項目,源於將 C/C++ 編譯生成的 LLVM IR 翻譯成 JavaScript [7] 的想法。爲此,Alon Zakai 發明了一種 Relooper 算法,這種算法能將任意控制流圖重新轉化爲結構化的控制流圖。

後續,Emscripten 加入了對 WebAssembly 的支持,其功能變爲了將 C/C++ 或是其他基於 LLVM IR 的語言的項目工程編譯到 WebAssembly。任何可移植的 C/C++ 庫都可以被 Emscripten 編譯成 WebAssembly,例如圖形庫、聲音庫等。Emscripten 主要在 Emcc 中使用 Clang + LLVM 將目標代碼編譯成 WebAssembly。同時,Emcc 還會生成包含 API 的 JavaScript 代碼,這些代碼能夠在 Node.js 裏運行,或者能夠被 HTML 包含,在瀏覽器裏面運行。

3.1.2 庫支持

爲了支持 C/C++ 標準庫,Emscripten 在 musl libc 和 LLVM libcxx 庫的基礎上做了定製化,實現了 WASI 標準接口,提供了大部分的 C/C++ 標準庫能力。此外,Emscripten 提供了部分他們適配過的常用庫,包括 socket 庫、html 庫、gl 庫等。這些庫能力的支持讓 Emscripten 能夠將大部分 C/C++ 工程無縫遷移到 WebAssembly 上,大大拓展了 WebAssembly 的生態。

然而,儘管 Emscripten 做了大量的工作,它仍然不能完美地將 C/C++ 工程遷移到 WebAssembly。下面將會簡要介紹部分侷限性:

3.1.3 編譯

Emscripten 底層依然使用的是 clang + wasm-ld 的能力將工程編譯成 WebAssembly 產物。此外,在 LLVM 的基礎上,Emscripten 做了大量移植工作,提供了更多樣的選項。爲了實現這些能力,Emscripten 開發了 emcc 項目。

emcc 基於 clang,提供了大量額外的編譯選項。例如,emcc 能讓用戶選擇是否編譯 WASI 產物,選擇需要導出到外部的函數,選擇是否生成 JavaScript 和 HTML 膠水代碼;並且,emcc 會根據用戶的編譯選項,自動化地選擇依賴的庫,生成額外的編譯和鏈接命令,省去了大量手動配置的時間。更多選項可以查閱 emcc settings 配置說明 [8]。

除了 clang 和 wasm-ld 的優化能力,emcc 還引入了 Closure Compiler[9] 和 Binaryen[10]。前者是一個 JavaScript to JavaScript 優化器,優化 Emscripten 生成的 JavaScript 代碼;後者則是一個獨立的 WebAssembly to WebAssembly 優化器,提供了 LLVM 以外的多個優化 Pass[11]。

3.1.4 調試

Emscripten 能夠生成 JavaScript 膠水代碼和 HTML 代碼。在瀏覽器中打開生成的 html 文件,瀏覽器會加載整個工程。Chrome DevTool 有實驗性質的 DWARF 調試信息支持,Emscripten 生成的 WebAssembly 產物包含 DWARF 信息,Chrome 能夠依據 DWARF 信息,定位並顯示 C/C++ 源碼調試信息。

關於如何調試 Emscripten 生成的 WebAssembly;可以進一步閱讀 WebAssembly 調試原理和方法簡介相關內容。

3.2 Binaryen

Binaryen 的目的是讓編譯到 WebAssembly 的工作變得 簡單,快速,有效

Binaryen 可以理解爲 WebAssembly 編譯器後端 + WebAssembly 編譯工具鏈

3.2.1 Binaryen IR

Binaryen 自己實現了一套 AST 格式的 IR,用來支持編譯器後端的優化等工作,稱之爲 Binaryen IR。Binaryen IR 與 WebAssembly 幾乎等價,是 WebAssembly 的子集。

在 WebAssembly 早期版本中,WebAssembly 以 AST 形式組織,因此,Binaryen IR 自然地被設計成樹狀結構。在標準的後續演進中,WebAssembly 變爲了 Stack Machine,自此 Binaryen IR 與 WebAssembly 分道揚鑣。綜上,Binaryen IR 發展成樹狀有歷史原因,社區也經過了一系列的討論,不在本文中展開說明,有興趣的讀者可以參考文檔 [12][13] 相關內容。

3.2.2 CFG

如前所述,Binaryen 的原生 IR 是 AST,但它也可以接收任意控制流圖中的輸入。Binaryen 可以將代碼 reloop 到結構化的控制流中。這適用於任何 CFG,甚至不可規約(irreducible)的 CFG。

3.2.3 優化 Pipeline

Binaryen 包含了許多優化過程,使 WebAssembly 更小、更快。你可以通過使用 wasm-opt 來運行 Binaryen 優化器,同時它們也可以在使用其他工具時運行,比如 wasm2jswasm-metadce

默認的優化流水線是由 addDefaultFunctionOptimizationPasses 類似的函數設置的。

用戶可以通過設置多種類型的參數選項:調整優化和 shrink 級別;是否忽略 unlikely traps;使用快速數學優化等。詳細配置選項可以通過 wasm-opt --help 進行獲取;對優化 Pass 感興趣讀者,可以閱讀 Binaryen Optimizations[11] 相關內容。

Binaryen 始終啓用 LTO(Link Time Optimization),因爲它通常在最終鏈接的 WebAssembly 上運行。優化器中的高級優化技術包括 SSAification、Flat IR 和 Stack/Poppy IR。此外,Binaryen 還包含各種不做優化的傳遞,例如 JavaScript 的合法化、Asyncify 等。

3.2.4 Tools

3.2.5 Debug

Binaryen 支持 DWARF 格式的調試信息,與 Emscripten 集成時可以接入 Emscripten 中的調試信息。

3.3 wasi-sdk

wasi-sdk[15] 可以看作是魔改過後的 LLVM,添加了 WASI 的支持。使用方式和 clang 編譯 C/C++ 項目基本一致,可以直接使用 clang 命令。wasi-sdk 中,libc 的實現和 Emscripten 相似,都是使用了開源的 musl libc 庫,進行了部分修改以適配 WASI。在 C/C++ 編寫的 WebAssembly 工程中,可以使用 wasi-sdk 替代 LLVM 作爲默認編譯工具鏈。

同樣的 wasi-sdk 也存在和 Emscripten 相似的侷限性,包括多線程、time 能力等都是缺失的。

3.4 TinyGo

TinyGo[16] 是一個輕量級的 Go 編譯器,主要用於嵌入式系統、WebAssembly 和命令行等較爲輕量級的環境中。TinyGo 複用了 Go language tools 以及 LLVM,提供額外的編譯 Go 的方法。

目前在服務端場景上,有部分 Go WebAssembly Serverless 函數服務,背後使用了 TinyGo 做支撐。

3.5 wabt

wabt[17] 是一個工具鏈集合,其中包括 WebAssembly 的反彙編工具、解釋器、編譯器、驗證工具等。下面簡單對幾個常用工具進行介紹。

3.5.1 wasm2wat 和 wat2wasm

wasm2wat 和 wat2wasm 是兩個最常用到的二進制和文本格式互相轉換的工具。wat 是 WebAssembly 文本格式的文件後綴名,wasm 則是二進制格式的後綴名。顧名思義,這兩個工具能將 WebAssembly 的文本格式和二進制格式在完美保留語義的前提下相互翻譯。由於翻譯的準確性,這兩個工具能給程序編寫者提供直接編寫或者修改二進制的能力,這對於進階 WebAssembly 開發者來說非常有用。

3.5.2 wasm2c

wasm2c 將 WebAssembly 二進制文件轉換爲 C 源文件和頭文件。這個功能在部分場合非常有用,例如 Web3 場景或者是安全場景。一部分 Web3 平臺的智能合約採用的是 WebAssembly,例如 eos,它將 C/C++ 編譯到 WebAssembly,運行在 eos 區塊鏈虛擬機平臺上。用戶可以手動下載合約,使用 wasm2c 查看合約代碼。同時,wasm2c 允許用戶將 WebAssembly 二進制產物 relooper 到 C,再編譯成 LLVM IR。安全領域的區塊鏈智能合約靜態動態程序代碼分析大多使用這種方式構造數據集。

3.6 wasm-pack

wasm-pack[18] 工具由 Rust / WebAssembly 工作組開發維護,並且是現在最爲活躍的 WebAssembly 應用開發工具。它支持將代碼打包成 npm 模塊,並且隨附了 Webpack 插件,可以輕鬆地與已有的 JavaScript 應用結合。

3.7 wasm-bindgen

wasm-bindgen[19] 讓 WebAssembly 模塊和 JavaScript 之間能夠進行交互。

wasm-bindgen 允許 JavaScript / WebAssembly 通過字符串、JavaScript 對象、類等進行通信。運用 wasm-bindgen 可以在 Rust 中定義 JavaScript 類或從 JavaScript 獲取一個字符串或返回一個字符串給 JavaScript。目前這個工具主要關注 Rust 生態,但底層原理與語言無關。開發團隊希望隨着時間的推移,這個工具可以穩定並擴展到 C/C ++ 等語言生態中。功能包括:

配合 wasm-pack,用戶可以在 web 上運行 Rust,將其作爲一個更大的應用程序的一部分發布,在 NPM 上發佈 Rust 的 WebAssembly 產物。

  1. 總結

本文介紹的多種編程語言和工具,包括 C/C++、Go、Rust 和 AssemblyScript 等,以及它們所提供的 WebAssembly 編譯和打包功能。其中,C/C++ 和 Rust 語言提供了多種工具,如 Emscripten、wasi-sdk、wasm-pack 和 wasm-bindgen 等,可根據不同的應用場景實現 WebAssembly 的編譯和打包。而 TinyGo 和 AssemblyScript 則分別提供了專爲 Go 的 WebAssembly、嵌入式場景和 TypeScript 的變體語言開發的編譯器。此外,還有兩個通用工具:Binaryen 和 wabt。前者能夠優化 WebAssembly 代碼,後者提供常用的 WebAssembly 工具和格式轉換功能;WebAssembly 相關編程語言和工具總結詳見表 1.

隨着 WebAssembly 越來越被廣泛地關注,各主流編程語言和社區都已將 WebAssembly 視爲更爲重要的組成部分,不僅將其作爲運行環境的編譯目標,甚至將其作爲默認產物提供支持。此外,爲了更好地支持 WebAssembly,社區還專門設計了一些新語言,如 IDEA 研究院張宏波主導設計的新語言 [24]。

WebAssembly 編程語言和工具集是爲實際的應用開發場景服務而存在的;基於本文的語言和工具鏈介紹,WebAssembly 實戰部分將詳細介紹編程語言和工具如何在實際的 WebAssembly 應用場景中使用,感興趣的讀者可以閱讀課程的相關章節。

表 1. WebAssembly 常用開發語言和工具集

  1. 參考文獻

[1]. Bytecode Alliance: https://github.com/bytecodealliance
[2]. wasmtime: https://wasmtime.dev/
[3]. wazero: https://github.com/tetratelabs/wazero
[4]. JS-API Specs: https://webassembly.github.io/spec/js-api/
[5]. awesome-wasm-langs: https://github.com/appcypher/awesome-wasm-langs
[6]. Emscripten: https://emscripten.org/
[7]. Alon Zakai. 2011. Emscripten: an LLVM-to-JavaScript compiler. In Proceedings of the ACM international conference companion on Object oriented programming systems languages and applications companion (OOPSLA '11). Association for Computing Machinery, New York, NY, USA, 301–312. https://doi.org/10.1145/2048147.2048224
[8]. settings: https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
[9]. Closure Compiler: https://github.com/google/closure-compiler
[10]. Binaryen: https://github.com/WebAssembly/binaryen
[11]. binaryen-optimizations: https://github.com/WebAssembly/binaryen#binaryen-optimizations
[12]. Structured code for the stack machine: https://github.com/WebAssembly/design/issues/753
[13]. Future of Binaryen in a stack machine world: https://github.com/WebAssembly/binaryen/issues/663
[14]. binaryen.js: https://www.npmjs.com/package/binaryen
[15]. wasi-sdk: https://github.com/WebAssembly/wasi-sdk
[16]. TinyGo: https://github.com/tinygo-org/tinygo
[17]. wabt: https://github.com/WebAssembly/wabt
[18]. wasm-pack: https://github.com/rustwasm/wasm-pack
[19]. wasm-bindgen: https://github.com/rustwasm/wasm-bindgen
[20]. DOM: https://github.com/rustwasm/wasm-bindgen/tree/main/examples/dom
[21]. console: https://github.com/rustwasm/wasm-bindgen/tree/main/examples/console_log
[22]. performace: https://github.com/rustwasm/wasm-bindgen/tree/main/examples/performance
[23]. paint: https://github.com/rustwasm/wasm-bindgen/tree/main/examples/paint
[24]. 基於 WebAssembly 的程序語言設計: https://idea.edu.cn/dii.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/oe6YvxeFuAB-oaqINIQ8iQ