起源
WebAssembly 起源于 Mozilla 員工的一個業余專案,2010年,在 Mozilla 從事 Android Firefox 開發的 Alon Zakai,為了把他以前開發的游戲引擎移植到瀏覽器上運行,利用業余時間開發了一款名叫 Emscripten 的編譯器,可以把 C++ 代碼通過 LLVM IR 編譯成 JavaScript 代碼,
到了 2011 年底,Emscripten 甚至能夠成功編譯 Python 和 Doom 等大型 C++ 專案,Mozilla 此時覺得這個專案很有前途,于是成立團隊并邀請 Alon 全職開發這個專案,2013 年 Alon 和其他成員一起提出了 asm.js 規范,asm.js 是 JavaScript 語言的一個嚴格子集,試圖通過“減少動態特性”和”添加型別提示“的方式幫助瀏覽器提升 JavaScript 優化空間,相較于完整的 JavaScript 語言,裁剪后的 asm.js 更靠近底層,更適合作為編譯器目標語言,
asm.js 只提供兩種資料型別:32位帶符號整數,64位帶符號浮點數,其他資料型別比如字串、布林值或者物件,asm.js 一概不提供,它們都是以數值的形式存在,保存在記憶體中,通過 TypedArray 呼叫,型別的宣告也有固定寫法:變數 | 0 表示整數,+變數 表示浮點數,例如下面一段代碼:
function MyAsmModule() {
"use asm"; // 告訴瀏覽器這是個 asm.js 模塊
function add(x, y) {
x = x | 0; // 變數 | 0 表示整數
y = y | 0;
return (x + y) | 0;
}
return { add: add };
}
支持 asm.js 的引擎提前識別出了型別,可以進行激進的 JIT(即時編譯)優化,甚至是 AOT(事先編譯)編譯,大幅提升性能,不支持 asm.js 按普通 JavaScript 代碼執行也不會影響運行結果,
但是 asm.js 的缺點也很明顯,那就是“底層”得不夠徹底,例如代碼仍然是文本格式;代碼撰寫仍然受 JavaScript 語法限制;瀏覽器仍然需要完成決議腳本、解釋執行、收集性能指標、JIT 編譯等一系列步驟,如果采用像 Java 類檔案那樣的二進制格式,不僅能縮小檔案體積,減少網路傳輸時間和決議時間,還能選用更接近機器的位元組碼,這樣 AOT/JIT 編譯器實作起來會更輕松,效果也更好,
與此同時,Google 的 Chrome 團隊也在試圖解決 JavaScript 性能問題,但方向有所不同,Chrome 給出的解決方案是 NaCl(Google Native Client)和 PNaCl(Portable NaCl),通過 NaCl/PNaC1,Chrome 瀏覽器可以在沙箱環境中直接執行本地代碼,
asm.js 和 NaCl/PNaC1 技術各有優缺點,二者可以取長補短,Mozilla 和 Google也看到了這一點,所以從 2013 年開始,兩個團隊就經常交流和合作,后來他們決定結合兩個專案的長處,合作開發一種基于位元組碼的技術,到了 2015 年,“WebAssembly” 確定為正式名稱并對外公開,W3C 成立了 WASM 社區小組(成員包括Chrome、Edge、Firefox 和 WebKit),致力于推動 WASM 技術的發展,
2016 年 Rust 1.14發布,開始支持 WASM,
2017 年 Google 決定放棄 PNaCl 技術;四大瀏覽器 Chrome、Edge、Safari、Firefox 更新版本開始支持 WASM,
2018 年 Go 1.11 發布,開始支持 WASM,
2019 年 Emscripten 更新為默認使用 LLVM 編譯為 WASM 代碼,停止對 asm.js 的支持;WebAssembly 成為萬維網聯盟(W3C)的推薦標準,與 HTML,CSS 和 JavaScript 一起成為 Web 的第四種語言,
簡介
官方給出的定義:WebAssembly / WASM 是基于堆疊式虛擬機的二進制指令集,可以作為編程語言的編譯目標,能夠部署在 Web 客戶端和服務端的應用中,
WebAssembly 具有如下特性:
- 是一種底層類匯編語言,能夠在所有當代桌面瀏覽器及很多移動瀏覽器上以接近本地的速度運行,
- 檔案設計得很緊湊,因此可以快速傳輸和下載,這些檔案的設計方式也使得它們可以快速決議和初始化,
- 被設計為編譯目標,讓 C++、Rust 和其他語言撰寫的代碼現在可以在 Web 上運行,
也就是說 WebAssembly 可以使得以各種語言撰寫的代碼都可以以接近原生的速度在瀏覽器中運行,
WebAssembly 也被設計為與 JavaScript 共存并協同作業,相對于 JavaScript(包括 asm.js)解決了如下幾個問題:
- 性能提升,由于 WebAssembly是一種底層類匯編語言,代碼是靜態型別,瀏覽器執行時可以直接將其編譯成機器碼去大幅提高性能;并且由于 WebAssembly 是位元組碼形式,檔案體積也很小,便于網路快速傳輸,瀏覽器廠商甚至引入了“流編譯”技術,讓檔案可以邊下載邊編譯,下載完畢即可進行初始化,
- 融合不同語言,之前想在 Web 上執行其他語言,只能把其他語言轉成 JavaScript 語言,但這個程序并不容易,而且會帶來執行性能上的大幅降低;而 WebAssembly 從設計之初就定位為編譯目標語言,讓其他語言可以輕松轉成 WebAssembly 語言代碼,不僅不用擔心性能(雖然仍會有一定損失),也讓代碼復用變得簡單,
- 加強代碼安全,對 JavaScript 代碼進行保護通常只能使用混淆來大幅降低代碼可讀性,但是在一些工具的幫助下只要多花費一些時間仍然可讀,但是轉譯而來的 WASM 代碼則完全不具有可讀性,即使通過 wasm2c 等工具進行反編譯,依然比分析 JS 代碼要難度大很多(當然并不會達到完全的代碼安全,但增加逆向難度會使其風險大大降低),
不過 WebAssembly 并不是純瀏覽器平臺的技術,猶如 JavaScript 與 Node.js,如今它也有自己的 Runtime,在瀏覽器之外的云原生、區塊鏈、安全等系統應用領域都有諸多應用,
編譯
C / C++ 通過 Emscripten 編譯:
emcc hello.c -o hello.wasm
Rust 通過 Cargo 編譯:
cargo build --target wasm32-example --release
還可以進一步壓縮體積:
wasm-gc target/wasm32-example/release/hello.wasm
Golang 內置編譯:
GOARCH=wasm GOOS=js go build -o hello.wasm main.go
運行
在 JavaScript 運行
為了在 JavaScript 中運行 WebAssembly,在編譯/實體化之前,你首先需要把模塊放入記憶體,比如通過 XMLHttpRequest 或 Fetch,模塊將會被初始化為帶型別陣列,
使用 Fetch 的例子:
fetch('module.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
result.instance.exports
});
上述方式是先創建一個包含你的 WebAssembly 模塊二進制代碼的 ArrayBuffer,然后使用 WebAssembly.instantiate() 編譯它,
你也可以使用 WebAssembly.instantiateStreaming(),該方法直接從原始位元組碼中直接獲取,編譯和實體化模塊,無需轉換為 ArrayBuffer:
WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject)
.then(result => {
result.instance.exports
});
WebAssembly 計劃未來會支持 <script type='module'> 和 ES6 的 import 陳述句這種形式直接加載運行,
在瀏覽器之外運行
Wasm 社區提供了很多 Runtime 容器,讓 WASM 可以在瀏覽器之外的系統上執行,并且運行環境是沙箱化的,
目前比較流行的 Runtime:
- wasmtime:既可以作為一個CLI,也可以被嵌入到其他應用系統中,如 IoT 或者云原生
- WebAssembly Micro Runtime:更偏向于芯片場景的虛擬機,如它的名字所示,體積非常小,起步速度只要 100 微秒,記憶體耗費最低只需 100KB
- wasmer:特點是支持在更多的編程語言運行 WASM 實體,并有自己的包管理平臺 Wapm
- WasmEdge:之前名為 SSVM,對云原生、邊緣和去中心化應用有針對性優化
底層概念
模塊
WebAssembly 程式的主要單元稱為模塊(Module),這個術語既用來表示代碼的二進制版本,也表示瀏覽器中的編譯后版本,
一個大型 WebAssembly 應用往往由多個子模塊組成,每個模塊都擁有自己的獨立資料資源,因此子模塊無法篡改其他模塊的資料;另外每個模塊所能使用的權限由最上層的呼叫者指定,因此第三方子模塊無法在上層模塊不感知的情況下越權呼叫,這種權限管理類似于 Android 開發需要預先宣告所有依賴的權限一樣,
當其他高級語言編譯成 WebAssembly 后,會成為了一個模塊二進制檔案,檔案名是以 .wasm 后綴結尾,檔案內容開頭是 8 位元組的用于描述的模塊頭:
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0d00 0000 ; WASM_BINARY_VERSION
前4 位元組被稱為“魔數(Magic Number)”,對應 \0asm 字串,用來識別這是一個 Wasm 模塊;后 4 位元組是當前模塊所使用的 WASM 標準版本號,
段
在模塊頭之后就是模塊的主體內容,這些內容被分門別類放在不同的段(Section),Wasm 把特定功能或者有相關聯的代碼放進一個特定的段中,有些段是任何的模塊都必需的,有些段是可選的,
段可能會包含多個專案,Wasm 規范一共定義了 12 種段,并給每種段分配了 ID,除了自定義段以外,其他所有的段都最多只能出現一次,且必須按照段 ID 遞增的順序出現,
下面是各個段的說明,其中粗體是必需存在的段:
| ID | 段 | 說明 |
|---|---|---|
| 0 | 自定義段(Custom) | 主要用于存盤除錯資訊等資料 |
| 1 | 型別段(Type) | 存盤匯入函式、模塊內部函式的函式引數串列 |
| 2 | 匯入段(Import) | 用于存盤匯入函式的函式名稱、函式引數索引 |
| 3 | 函式段(Function) | 用于存盤函式索引值 |
| 4 | 表格段(Table) | 用于存盤物件參考,通過表格段可以實作函式指標的功能(call_indirect 指令),可以從外部宿主匯入,同時也可以匯出到外部宿主環境 |
| 5 | 記憶體段(Memory) | 用于存盤程式的運行時動態資料,可以從外部宿主匯入,同時也可以匯出到外部宿主環境 |
| 6 | 全域段(Global) | 用于存盤全部變數值 |
| 7 | 匯出段(Export) | 用于存盤匯出函式的函式名稱、函式引數索引 |
| 8 | 開始段(Start) | 用于指定模塊初始化時的函式索引值 |
| 9 | 元素段(Elem) | 表格段并沒有顯式地初始化,元素段用于存盤函式的索引值 |
| 10 | 代碼段(Code) | 用于存盤函式的指令代碼 |
| 11 | 資料段(Data) | 用于存盤初始化記憶體的靜態資料 |
資料型別
WASM 在二進制編碼里的資料型別如下:
- 無符號整數,支持三種非負整數型別:uint8、uint16、uint32,后面的數字表示占用了多少個bit
- 可變長無符號整數,支持三種可變長非負整數型別:varuint1、varuint7、varuint32,所謂可變長的意思是會根據具體資料大小決定使用多少bit,后面的數字表示最大可占用多少個bit
- 可變長有符號整數,同上,這里允許負數的出現,支持varint7、varint32、varint64 三種型別
- 浮點數,同 JavaScript,采用 IEEE-754 方案,單精度為32位
對于語言本身,提供以下數值型別:
- i32: 32-bit 整型
- i64: 64-bit 整型
- f32: 32-bit 浮點型
- f64: 64-bit 浮點型
每個引數和區域變數都必須是以上四種值型別之一 ,函式簽名由 0 或多個引數的型別序列及 0 或多個回傳值的型別序列組成,(在最小可行版本中,一個函式最多可以有一個回傳型別),需要注意的是,值型別 i32 和 i64 不是固有有符號或無符號的, 這些型別的解釋取決于某個具體的運算子,
布林值用無符號 32 位整數表示,0 為 false,非 0 值為 true,所有其他值型別(如字串)需要在模塊的線性記憶體空間中表示,
WAT
WASM 二進制檔案是不可讀的,WAT (WebAssembly Text Format) 是另外一種輸出格式,是使用 “S- 運算式” 的文本格式,可以近似理解為與二進制等價的匯編語言,

部分瀏覽器的開發者工具支持將 WASM 轉換成 WAT 查看,便于在線除錯,社區提供了 wasm2wat 和 wat2wasm 等成熟的工具將二者進行轉換,可以在 WABT (WebAssembly Binary Toolkit) 工具集中找到,所以也是可以直接撰寫 WAT 再轉換成 WASM,
WASI
WebAssembly 雖然是為了 Web 而生,但并不意味著它只能也不打算只在瀏覽器上運行,開發人員想將它推向了瀏覽器之外,而這需要提供一套與作業系統互動的介面,
由于 WebAssembly 是基于概念機器的匯編語言,而不是物理機器,因此,WebAssembly提供了一種快速,可擴展,安全的方式來在所有計算機上運行相同的代碼,同時為了在所有不同的作業系統上運行,WebAssembly 需要一個概念機器的系統介面,而不是任何單個作業系統,于是開發人員定義了一種與不同作業系統通信統一標準,名為 WASI (WebAssembly System Interface),它是為 WASM 專門設計一套引擎無關(engine-indepent)、面向非 Web 系統(non-Web system-oriented)的 API 標準,
WASI 的設計遵循兩大原則:
- 可移植性,能夠編譯可移植的二進制檔案,編譯一次就能在不同的計算機上運行,讓用戶分發代碼更容易,例如,Node 的原生模塊如果是用 WebAssembly 撰寫的,那么當用戶安裝帶有原生模塊的應用時就不需要運行
node-gyp了,開發人員也無需配置并分發幾十個二進制檔案了, - 安全性,當一行代碼請求作業系統執行某些輸入或輸出時,作業系統需要確定該代碼所請求的操作是否安全,WebAssembly 采用了沙箱機制,代碼不能直接與作業系統互動,宿主機(可能是瀏覽器,也可能是 WASM 運行時)需要將相關函式放入代碼可以使用的沙箱中,宿主機可以逐一限制每個程式可以做什么,雖然擁有沙箱機制并不會使系統本身變安全(宿主機仍然可以將所有能力都放入到沙箱中),不過它至少讓宿主機能夠選擇創建更安全的系統,
基于上述兩項關鍵原則,WASI 被設計為一組模塊化的標準介面,其中最基礎的核心模塊為 wasi-core,其它的比如 sensors、crypto、processes、multimedia 等子集合都是以單獨的子模塊的形式組織,

wasi-core 包含所有程式都需要的基本介面,它會覆寫與 POSIX 近乎相同的領域,包括諸如檔案、網路連接、時鐘以及亂數等相關系統呼叫的 WASI 抽象函式介面,
WASI 在 WASM 位元組碼與虛擬機之間,增加了一層“系統呼叫抽象層”,比如對于在 C/C++ 原始碼中使用的 fopen 函式,當我們將這部分源代碼與專為 WASI 實作的 C 標準庫 wasi-libc 進行編譯時,原始碼中對 fopen 的函式呼叫程序,其內部會間接通過呼叫名為 __wasi_path_open 的函式來實作,這個 __wasi_path_open 函式,便是對實際系統呼叫的一個抽象,
WASI 主要作業是定義 Import 介面標準,提供通用 Import 介面在不同系統上的具體實作(與不同作業系統上實作libc模式類似), 基于 WASI 的設計思路,針對不同的領域我們還可以提供更上層的WADSI(WebAssembly Domain Specific Interface),將領域通用的介面作為 Import 介面提供,從而使得開發者可以直接使用,
安全性
WebAssembly 的安全性來源之一是,它是第一個共享 JavaScript VM 的語言,而 JavaScript VM 在運行時是沙箱化的,同時也經歷了多年的檢驗和安全測驗,這確保了其安全性,WebAssembly 模塊的可訪問范圍不超過 JavaScript 的訪問范圍,同時也會遵守相同的安全性規則,包括同源策略(same-origin policy)這樣的增強規則,
與桌面應用程式不同,WebAssembly 模塊對設備記憶體沒有直接訪問權限,而是運行時環境在初始化程序中向模塊傳遞一個 ArrayBuffer ,模塊將這個 ArrayBuffer 當作線性記憶體來使用,WebAssembly 框架執行檢查以確保代碼不會對這個陣列進行越界操作,
對于像函式指標這樣存盤在 Table 段中的專案,WebAssembly 模塊也不能直接訪問,代碼會用索引值向WebAssembly框架提出訪問某個專案的請求,然后框架訪問記憶體,并代表代碼執行這個專案,
在 C++ 中,執行堆疊與線性記憶體一起位于記憶體中,雖然 C++ 代碼不應該修改執行堆疊,但是它可以使用指標實作修改,WebAssembly的執行堆疊與線性記憶體是分離的,代碼無法訪問,
應用案例
谷歌地球
谷歌地球在 2017 年發布是 9.0 版本中,采用的是 NaCl 技術開發,所以當時只能在 Chrome 上運行,2020 年谷歌使用 C++ 通過 WebAssembly 重寫了該專案,從此可以在 Firefox 和 Edge 上運行,

AutoCAD
AutoCAD 是一款由將近 40 年歷史的知名桌面端設計軟體,被廣泛地用于土木建筑、裝飾裝潢、工業制圖等多個領域中,2014 年 AutoCAD 發布 Web 版,是通過 Google Web Toolkit(一個 Google 開發的可以使用 Java 語言開發 Web 應用的工具集)的幫助下開發,將 Android 端的 Java 代碼轉譯成 JS 代碼,但由于生成的 JS 代碼十分龐大,導致瀏覽器上運行效率很低,2015 年又通過 asm.js 將原有的 C++ 代碼中的主要功能直接進行編譯移植到到 Web 平臺,性能有了很大的提告,2018 年 3 月,基于 WASM 構建的 AutoCAD Web 也成功誕生,

Figma
Figma 是一個基于瀏覽器的協作式 UI 設計工具,核心的互動界面是在一個 Canvas 內承載,這個 Canvas 的互動是通過 WASM 控制的,基于瀏覽器讓它可以輕松跨平臺運行,而 WebAssembly 帶來了高性能,讓它即使在 Web 平臺依然在速度上完勝那些基于原生 OS 開發的同類應用,

結語
可以看出 WebAssembly 并不是用來完全取代 JavaScript,而是作為 Web 技術的補充,在性能和代碼復用等方面彌補 JavaScript 的局限,正如 WASM 官方的口號:“所有可以用 WebAssembly 實作的終將會用 WebAssembly 實作”,WebAssembly 的最終目標是用任何語言編譯而來并可以高效運行在任何平臺,最重要的是它背靠 Google、Mozilla、Edge 等主流開發機構的支持,相信在未來一定還會有更長足的發展,
參考資料
- WebAssembly原理與核心技術
- WebAssembly實戰
- 標準化中的 WASI:在 web 之外運行 WebAssembly 的系統介面
- 創建并使用 WebAssembly 模塊
- WebAssembly | MDN
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/433306.html
標籤:JavaScript
上一篇:分析HTTP請求以降低HTTP走私攻擊HTTP資料接收不同步攻擊的風險
下一篇:three.js的使用
