翻譯:Marty Kalin翻譯:瘋狂的技術宅
原文:https://opensource.com/articl...
未經允許嚴禁轉載
有這樣一種技術,可以把用高級語言編寫的非 Web 程序轉換成爲 Web 準備的二進制模塊,而無需對 Web 程序的源代碼進行任何更改即可完成這種轉換。瀏覽器可以有效地下載新翻譯的模塊並在沙箱中執行。執行的 Web 模塊可以與其他 Web 技術無縫地交互 - 特別是 JavaScript(JS)。歡迎來到WebAssembly。
對於名稱中帶有 assembly 的語言,WebAssembly 是低級的。但是這種低級角色鼓勵優化:瀏覽器虛擬機的即時(JIT)編譯器可以將可移植的 WebAssembly 代碼轉換爲快速的、特定於平臺的機器代碼。因此,WebAssembly 模塊成爲適用於計算綁定任務(例如數字運算)的可執行文件。
有很多高級語言都能編譯成 WebAssembly,而且這個名單正在增長,但最初的候選是C、C ++ 和 Rust。我們將這三種稱爲系統語言,因爲它們用於系統編程和高性能應用編程。系統語言都具有兩個特性,這使它們適合被編譯爲 WebAssembly。下一節將詳細介紹設置完整的代碼示例(使用 C 和 TypeScript)以及來自 WebAssembly 自己的文本格式語言的示例。
顯式數據類型和垃圾回收
這三種系統語言需要顯式數據類型,例如 int 和 double,用於變量聲明和從函數返回的值。例如以下代碼段說明了 C 中的 64 位加法:
long n1 = random();
long n2 = random();
long sum = n1 + n2;
庫函數 random 聲明以 long 爲返回類型:
long random(); /* returns a long */
在編譯過程中,C 源被翻譯成彙編語言,然後再將其翻譯成機器代碼。在英特爾彙編語言(AT&T flavor)中,上面的最後一個 C 語句的功能類似以下內容(## 爲彙編語言的註釋符號):
addq %rax, %rdx ## %rax = %rax + %rdx (64-bit addition)
%rax 和 %rdx 是 64 位寄存器,addq 指令意味着 add quadwords,其中 quadword 是 64 位大小,這是 C 語言中 long 類型的標準大小。彙編語言強調可執行機器代碼涉及類型,通過指令和參數的混合給出類型(如果有的話)。在這種情況下,add 指令是 addq(64 位加法),而不是例如 addl 這樣的指令,它增加了 C 語言典型的 int 的 32 位值。使用的寄存器字長是完整的 64 位( %rax 和%rdx )而不是其 32 位的(例如,%eax 是 %rax 的低 32 位,%edx 是 %rdx 的低 32 位)。
彙編語言的效果很好,因爲操作數被存儲在 CPU 寄存器中,而合理的 C 編譯器(即使是默認的優化級別)也會生成與此處所示相同的彙編代碼。
這三種系統語言強調顯式類型,是編譯成 WebAssembly 的理想選擇,因爲這種語言也有明確的數據類型:i32 表示 32 位的整數值,f64 表示 64 位的浮點值,依此類推。
顯式數據類型也鼓勵優化函數調用。具有顯式數據類型的函數具有 signature,它用於指定參數的數據類型以及從函數返回的值(如果有)。下面是名爲$add 的 WebAssembly 函數的簽名,該函數使用下面討論的 WebAssembly 文本格式語言編寫。該函數把兩個 32 位的整數作爲參數並返回一個 64 位的整數:
(func $add (param $lhs i32) (param $rhs i32) (result i64))
瀏覽器的 JIT 編譯器應該具有 32 位的整數參數,並把返回的 64 位值存儲在適當大小的寄存器中。
談到高性能 Web 代碼,WebAssembly 並不是唯一的選擇。例如,asm.js 是一種 JS 方言,與 WebAssembly 一樣,可以接近原生速度。 asm.js 方言允許優化,因爲代碼模仿上述三種語言中的顯式數據類型。這是 C 和 am.js 的例子。 C中的示例函數是:
int f(int n) { /** C **/
return n + 1;
}
參數 n 和返回值都以 int 顯式輸入。asm.js 的等效函數是:
function f(n) { /** asm.js **/
n = n | 0;
return (n + 1) | 0;
}
通常,JS 沒有顯式數據類型,但 JS 中的按位或運算符能夠產生一個整數值。這就解釋了看上去毫無意義的按位或運算符:
n = n | 0; /* bitwise-OR of n and zero */
n 和 0 之間的按位或運算得到 n,但這裏的目的是表示 n 保持整數值。 return 語句重複了這個優化技巧。
在 JS 方言中,TypeScript 在顯式數據類型方面脫穎而出,這使得這種語言對於編譯成 WebAssembly 很有吸引力。 (下面的代碼示例說明了這一點。)
三種系統語言都具有的第二個特性是它們在沒有垃圾收集器(GC)的情況下執行。對於動態分配的內存,Rust 編譯器會自動分配和釋放代碼;在其他兩種系統語言中,動態分配內存的程序員負責顯式釋放內存。系統語言避免了自動化 GC 的開銷和複雜性。
WebAssembly 的概述可以總結如下。幾乎所有關於 WebAssembly 語言的文章都提到把近乎原生的速度作爲語言的主要目標之一。 原生速度是指已編譯的系統語言的速度,因此這三種語言也是最初被指定爲編譯成 WebAssembly 的候選者的原因。
WebAssembly,JavaScript 和關注點分離
WebAssembly 語言並非爲了取代 JS,而是爲了通過在計算綁定任務上提供更好的性能來補充 JS。WebAssembly 在下載方面也有優勢。瀏覽器將 JS 模塊作爲文本提取,這正是 WebAssembly 能夠解決的低效率問題之一。WebAssembly 中的模塊是緊湊的二進制格式,可加快下載速度。
同樣令人感興趣的是 JS 和 WebAssembly 如何協同工作。 JS 旨在讀入文檔對象模型(DOM),即網頁的樹形表示。相比之下,WebAssembly 沒有爲 DOM 提供任何內置功能,但是 WebAssembly 可以導出 JS 根據需要調用的函數。這種關注點分離意味着清晰的分工:
DOM<----->JS<----->WebAssembly
無論用什麼方言,JS 都應該管理 DOM,但 JS 也可以用通過 WebAssembly 模塊提供的通用功能。代碼示例有助於說明,本文中的代碼案例可以在我的網站上找到(http://condor.depaul.edu/mkalin)。
冰雹(hailstone)序列和 Collatz 猜想
生產級代碼案例將使 WebAssembly 代碼執行繁重的計算綁定任務,例如生成大型加密密鑰對,或進行加密和解密。
考慮函數 hstone(對於hailstone),它以正整數作爲參數。該函數定義如下:
3N + 1 if N is odd
hstone(N) =
N/2 if N is even
例如,hstone(12) 返回 6,而 hstone(11) 返回 34。如果 N 是奇數,則 3N + 1 是偶數;但如果 N 是偶數,則 N/2 可以是偶數(例如,4/2 = 2)或奇數(例如,6/2 = 3)。
hstone 函數可以通過將返回值作爲下一個參數傳遞來進行迭代。結果是一個 hailstone 序列,例如這個序列,以 24 作爲原始參數開始,返回值 12 作爲下一個參數,依此類推:
24,12,6,3,10,5,16,8,4,2,1,4,2,1,...
序列收斂到 4,2,1 的序列無限重複需要 10 次調用:(3 x 1)+ 1 是 4,它除以 2 得 2,再除以 2 得 1。 Plus 雜誌提供了爲什麼把這些序列的稱做 hailstone 的解釋。
請注意,兩個冪很快收斂,只需要 N 除以 2 得到 1;例如,32 = 25的收斂長度爲5,64 = 26的收斂長度爲6。這裏感興趣的是從初始參數到第一個出現的序列長度。我在 C 和 TypeScript 中的代碼例子計算了冰雹序列的長度。
Collatz 猜想是一個冰雹序列會收斂到 1,無論初始值 N> 0 恰好是什麼。沒有人找到 Collatz 猜想的反例,也沒有人找到證據將猜想提升到一個定理。這個猜想很簡單,就像用程序測試一樣,是數學中一個極具挑戰性的問題。
從 C 到 WebAssembly 一步到位
下面的 hstoneCL 程序是一個非 Web 應用,可以使用常規 C 語言編譯器(例如,GNU 或 Clang)進行編譯。程序生成一個隨機整數值 N> 0 八次,並計算從 N 開始的冰雹序列的長度。兩個程序員定義的函數,main 和 hstone 是有意義的。該應用程序稍後會被編譯爲 WebAssembly。
示例1. C 中的 hstone 函數
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int hstone(int n) {
int len = 0;
while (1) {
if (1 == n) break; /* halt on 1 */
if (0 == (n & 1)) n = n / 2; /* if n is even */
else n = (3 * n) + 1; /* if n is odd */
len++; /* increment counter */
}
return len;
}
#define HowMany 8
int main() {
srand(time(NULL)); /* seed random number generator */
int i;
puts(" Num Steps to 1");
for (i = 0; i < HowMany; i++) {
int num = rand() % 100 + 1; /* + 1 to avoid zero */
printf("%4i %7i\n", num, hstone(num));
}
return 0;
}
代碼可以在任何類 Unix 系統上從命令行編譯和運行(% 是命令行提示符):
% gcc -o hstoneCL hstoneCL.c ## compile into executable hstoneCL
% ./hstoneCL ## execute
以下是例子運行的輸出:
Num Steps to 1
88 17
1 0
20 7
41 109
80 9
84 9
94 105
34 13
系統語言(包括 C)需要專門的工具鏈才能將源代碼轉換爲 WebAssembly 模塊。對於 C/C++ 語言,Emscripten 是一個開創性且仍然廣泛使用的選項,建立在衆所周知的 LLVM (低級虛擬機)編譯器基礎結構之上。我在 C 語言中的示例使用 Emscripten,你可以[使用本指南進行安裝(https://github.com/emscripten...)。
hstoneCL 程序可以通過使用 Emscription 編譯代碼進行 Web 化,而無需任何更改。Emscription工具鏈還與 JS glue(在asm.js中)一起創建一個HTML頁面,該頁面介於 DOM 和計算 hstone 函數的 WebAssembly 模塊之間。以下是步驟:
- 將非 Web 程序 hstoneCL 編譯到WebAssembly中:
% emcc hstoneCL.c -o hstone.html ## generates hstone.js and hstone.wasm as well
文件 hstoneCL.c 中包含上面顯示的源代碼,-o 輸出標誌用於指定 HTML 文件的名稱。任何名稱都可以,但生成的 JS 代碼和 WebAssembly 二進制文件具有相同的名稱(在本例中,分別爲 hstone.js 和 hstone.wasm)。較舊版本的 Emscription(在13之前)可能需要將標誌 -s WASM = 1 包含在編譯命令中。
- 使用 Emscription 開發 Web 服務器(或等效的)來託管 Web 化應用:
% emrun --no_browser --port 9876 . ## . is current working directory, any port number you like
要禁止顯示警告消息,可以包含標誌 --no_emrun_detect。此命令用於啓動 Web 服務器,該服務器承載當前工作目錄中的所有資源;特別是 hstone.html、hstone.js 和 hstone.webasm。
- 用支持 WebAssembly 的瀏覽器(例如,Chrome或Firefox)打開 URL http://localhost:9876/hstone.html。
這個截圖顯示了我用 Firefox 運行的示例輸出。
圖1. web 化 hstone 程序
結果非常顯著,因爲完整的編譯過程只需要一個命令,而且不需要對原始 C 程序進行任何更改。
微調 hstone 程序進行 Web 化
Emscription工具鏈很好地將 C 程序編譯成 WebAssembly 模塊並生成所需的 JS 膠水,但這些是機器生成的典型代碼。例如,生成的 asm.js 文件大小几乎爲 100 KB。 JS 代碼處理多個場景,並且不使用最新的 WebAssembly API。 webified hstone 程序的簡化版本將使你更容易關注 WebAssembly 模塊(位於 hstone.wasm 文件中)如何與 JS 膠水(位於 hstone.js 文件中)進行交互。
還有另一個問題:WebAssembly 代碼不需要鏡像 C 等源程序中的功能邊界。例如,C 程序 hstoneCL 有兩個用戶定義的函數,main 和 hstone。生成的 WebAssembly 模塊導出名爲 _ main 的函數,但不導出名爲 _ hstone 的函數。 (值得注意的是,函數 main 是 C 程序中的入口點。)C 語言 hstone 函數的主體可能在某些未導出的函數中,或者只是包含在 _ main 中。導出的 WebAssembly 函數正是 JS glue 可以通過名稱調用的函數。但是應在 WebAssembly 代碼中按名稱導出哪些源語言函數。
示例2. 修訂後的 hstone 程序
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emscripten/emscripten.h>
int EMSCRIPTEN_KEEPALIVE hstone(int n) {
int len = 0;
while (1) {
if (1 == n) break; /* halt on 1 */
if (0 == (n & 1)) n = n / 2; /* if n is even */
else n = (3 * n) + 1; /* if n is odd */
len++; /* increment counter */
}
return len;
}
如上所示,修改後的 hstoneWA 程序沒有 main 函數,它不再需要,因爲該程序不是作爲獨立程序運行,而是僅作爲具有單個導出函數的 WebAssembly 模塊運行。指令 EMSCRIPTEN_KEEPALIVE(在頭文件 emscripten.h 中定義)指示編譯器在 WebAssembly 模塊中導出 _ hstone 函數。命名約定很簡單:諸如 hstone 之類的 C 函數保留其名稱 —— 但在 WebAssembly 中使用單個下劃線作爲其第一個字符(在本例中爲 _ hstone)。 WebAssembly中的其他編譯器遵循不同的命名約定。
要確認此方法是否有效,可以簡化編譯步驟,僅生成 WebAssembly 模塊和 JS 粘合劑而不是 HTML:
% emcc hstoneWA.c -o hstone2.js ## we'll provide our own HTML file
HTML文件現在可以簡化爲這個手寫的文件:
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script src="hstone2.js"></script>
</head>
<body/>
</html>
HTML 文檔加載 JS 文件,後者又獲取並加載 WebAssembly 二進制文件 hstone2.wasm。順便說一下,新的 WASM 文件大小隻是原始例子的一半。
程序代碼可以像以前一樣編譯,然後使用內置的Web服務器啓動:
% emrun --no_browser --port 7777 . ## new port number for emphasis
在瀏覽器(在本例中爲 Chrome)中請求修改後的 HTML 文檔後,可以用瀏覽器的 Web 控制檯確認 hstone 函數已導出爲 _ hstone。以下是我在 Web 控制檯中的會話段,## 爲註釋符號:
> _hstone(27) ## invoke _hstone by name
< 111 ## output
> _hstone(7) ## again
< 16 ## output
EMSCRIPTEN_KEEPALIVE 指令是使 Emscripten 編譯器生成 WebAssembly 模塊的簡單方法,該模塊將所有感興趣的函數導出到 JS 編程器同樣產生的 JS 粘合劑。一個自定義的 HTML 文檔,無論手寫的 JS 是否合適,都可以調用從 WebAssembly 模塊導出的函數。爲了這個乾淨的方法,向 Emscripten 致敬。
將 TypeScript 編譯爲 WebAssembly
下一個代碼示例是 TypeScript,它是具有顯式數據類型的 JS。該設置需要 Node.js 及其 npm 包管理器。以下 npm 命令安裝 AssemblyScript,它是 TypeScript 代碼的 WebAssembly 編譯器:
% npm install -g assemblyscript ## install the AssemblyScript compiler
TypeScript 程序 hstone.ts 由單個函數組成,同樣名爲 hstone。現在數據類型如 i32(32位整數)緊跟參數和局部變量名稱(在本例中分別爲 n 和 len):
export function hstone(n: i32): i32 { // will be exported in WebAssembly
let len: i32 = 0;
while (true) {
if (1 == n) break; // halt on 1
if (0 == (n & 1)) n = n / 2; // if n is even
else n = (3 * n) + 1; // if n is odd
len++; // increment counter
}
return len;
}
函數 hstone 接受一個 i32 類型的參數,並返回相同類型的值。函數的主體與 C 語言示例中的主體基本相同。代碼可以編譯成 WebAssembly,如下所示:
% asc hstone.ts -o hstone.wasm ## compile a TypeScript file into WebAssembly
WASM 文件 hstone.wasm 的大小僅爲14 KB。
要突出顯示如何加載 WebAssembly 模塊的詳細信息,下面的手寫 HTML 文件(我的網站上找到(http://condor.depaul.edu/mkalin)中的 index.html)包含以下腳本:獲取並加載 WebAssembly 模塊 hstone.wasm 然後實例化此模塊,以便可以在瀏覽器控制檯中調用導出的 hstone 函數進行確認。
示例 3. TypeScript 代碼的 HTML頁面
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script>
fetch('hstone.wasm').then(response => <!-- Line 1 -->
response.arrayBuffer() <!-- Line 2 -->
).then(bytes => <!-- Line 3 -->
WebAssembly.instantiate(bytes, {imports: {}}) <!-- Line 4 -->
).then(results => { <!-- Line 5 -->
window.hstone = results.instance.exports.hstone; <!-- Line 6 -->
});
</script>
</head>
<body/>
</html>
上面的 HTML 頁面中的腳本元素可以逐行說明。第 1 行中的 fetch 調用使用 Fetch 模塊從託管 HTML 頁面的 Web 服務器獲取 WebAssembly 模塊。當 HTTP 響應到達時,WebAssembly 模塊將把它做作爲一個字節序列,它存儲在腳本第 2 行的 arrayBuffer 中。這些字節構成了 WebAssembly 模塊,它是從 TypeScript 編譯的代碼。文件。該模塊沒有導入,如第 4 行末尾所示。
在第 4 行的開頭實例化 WebAssembly 模塊。 WebAssembly 模塊類似於非靜態類,其中包含面嚮對象語言(如Java)中的非靜態成員。該模塊包含變量、函數和各種支持組件;但是與非靜態類一樣,模塊必須實例化爲可用,在本例中是在 Web 控制檯中,但更常見的是在相應的 JS 粘合代碼中。
腳本的第 6 行以相同的名稱導出原始的 TypeScript 函數 hstone。此 WebAssembly 功能現在可用於任何 JS 粘合代碼,因爲在瀏覽器控制檯中的另一個會話將確認。
WebAssembly 具有更簡潔的 API,用於獲取和實例化模塊。新 API 將上面的腳本簡化爲 fetch 和 instantiate 操作。這裏展示的較長版本具有展示細節的好處,特別是將 WebAssembly 模塊表示爲字節數組,將其實例化爲具有導出函數的對象。
計劃是讓網頁以與 JS ES2015 模塊相同的方式加載 WebAssembly 模塊:
<script type='module'>...</script>
然後,JS 將獲取、編譯並以其他方式處理 WebAssembly 模塊,就像是加載另一個 JS 模塊一樣。
文本格式語言
WebAssembly 二進制文件可以轉換爲 文本格式的等價物。二進制文件通常駐留在具有 WASM 擴展名的文件中,而其人類可讀的文本副本駐留在具有 WAT 擴展名的文件中。 WABT 是一套用於處理 WebAssembly 的工具,其中包括用於轉換爲 WASM 和 WAT 格式的工具。轉換工具包括 wasm2wat,wasm2c 和 wat2wasm 等。
文本格式語言採用 Lisp 推廣的 S 表達式(S for symbolic)語法。 S 表達式(簡稱 sexpr)表示把樹作爲具有任意多個子列表的列表。例如這段 sexpr 出現在 TypeScript 示例的 WAT 文件末尾附近:
(export "hstone" (func $hstone)) ## export function $hstone by the name "hstone"
樹表示是:
export ## root
|
+----+----+
| |
"hstone" func ## left and right children
|
$hstone ## single child
在文本格式中,WebAssembly 模塊是一個 sexpr,其第一項是模塊,它是樹的根。下面是一個定義和導出單個函數的模塊的簡單例子,該函數不帶參數但返回常量 9876:
(module
(func (result i32)
(i32.const 9876)
)
(export "simpleFunc" (func 0)) // 0 is the unnamed function's index
)
該函數的定義沒有名稱(即作爲 lambda),並通過引用其索引 0 導出,索引 0 是模塊中第一個嵌套的 sexpr 的索引。導出名稱以字符串形式給出;在當前情況下其名稱爲“simpleFunc”。
文本格式的函數具有標準模式,可以如下所示:
(func <signature> <local vars> <body>)
簽名指定參數(如果有)和返回值(如果有)。例如,這是一個未命名函數的簽名,它接受兩個 32 位整數參數,返回一個 64 位整數值:
(func (param i32) (param i32) (result i64)...)
名稱可以賦予函數、參數和局部變量。名稱以美元符號開頭:
(func $foo (param $a1 i32) (param $a2 f32) (local $n1 f64)...)
WebAssembly 函數的主體反映了該語言的底層棧機器體系結構。棧存儲用於暫存器。考慮一個函數的示例,該函數將其整數參數加倍並返回:
(func $doubleit (param $p i32) (result i32)
get_local $p
get_local $p
i32.add)
每個 get_local 操作都可以處理局部變量和參數,將 32 位整數參數壓入棧。然後 i32.add 操作從棧中彈出前兩個(當前唯一的)值以執行添加。最後 add 操作的和是棧上的唯一值,從而成爲 $doubleit 函數的返回的值。
當 WebAssembly 代碼轉換爲機器代碼時,WebAssembly 棧作爲暫存器應儘可能由通用寄存器替換。這是 JIT 編譯器的工作,它將 WebAssembly 虛擬棧機器代碼轉換爲實際機器代碼。
Web 程序員不太可能以文本格式編寫 WebAssembly,因爲從某些高級語言編譯是一個非常有吸引力的選擇。相比之下,編譯器編的作者可能會發現在這種細粒度級別上工作是有效的。
總結
WebAssembly 的目標是實現近乎原生的速度。但隨着 JS 的 JIT 編譯器不斷改進,並且隨着非常適合優化的方言(例如,TypeScript)的出現和發展,JS 也可能實現接近原生的速度。這是否意味着 WebAssembly 是在浪費精力?我想不是。
WebAssembly 解決了計算中的另一個傳統目標:有意義的代碼重用。正如本文中的例子所示,使用適當語言(如 C 或 TypeScript)的代碼可以輕鬆轉換爲 WebAssembly 模塊,該模塊可以很好地與 JS 代碼一起使用 —— 這是連接 Web 中所使用的一系列技術的粘合劑。因此 WebAssembly 是重用遺留代碼和擴展新代碼使用的一種誘人方式。例如最初作爲桌面應用的用於圖像處理的高性能程序在 Web 應用中也可能是有用的。然後 WebAssembly 成爲重用的有吸引力的途徑。 (對於計算限制的新 Web 模塊,WebAssembly 是一個合理的選擇。)我的預感是 WebAssembly 將在重用和性能方面茁壯成長。
本文首發微信公衆號:前端先鋒
歡迎掃描二維碼關注公衆號,每天都給你推送新鮮的前端技術文章
歡迎繼續閱讀本專欄其它高贊文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 實現虛擬現實遊戲
- 13個幫你提高開發效率的現代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?從調用棧到Promise你需要知道的一切
- WebSocket實戰:在 Node 和 React 之間進行實時通信
- 關於 Git 的 20 個面試題
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什麼?
- 30分鐘用Node.js構建一個API服務器
- Javascript的對象拷貝
- 程序員30歲前月薪達不到30K,該何去何從
- 14個最好的 JavaScript 數據可視化庫
- 8 個給前端的頂級 VS Code 擴展插件
- Node.js 多線程完全指南
- 把HTML轉成PDF的4個方案及實現