翻譯:Marty Kalinjavascript
翻譯:瘋狂的技術宅html
原文:opensource.com/article/19/…前端
未經容許嚴禁轉載java
有這樣一種技術,能夠把用高級語言編寫的非 Web 程序轉換成爲 Web 準備的二進制模塊,而無需對 Web 程序的源代碼進行任何更改便可完成這種轉換。瀏覽器能夠有效地下載新翻譯的模塊並在沙箱中執行。執行的 Web 模塊能夠與其餘 Web 技術無縫地交互 - 特別是 JavaScript(JS)。歡迎來到WebAssembly。node
對於名稱中帶有 assembly 的語言,WebAssembly 是低級的。可是這種低級角色鼓勵優化:瀏覽器虛擬機的即時(JIT)編譯器能夠將可移植的 WebAssembly 代碼轉換爲快速的、特定於平臺的機器代碼。所以,WebAssembly 模塊成爲適用於計算綁定任務(例如數字運算)的可執行文件。git
有不少高級語言都能編譯成 WebAssembly,並且這個名單正在增加,但最初的候選是C、C ++ 和 Rust。咱們將這三種稱爲系統語言,由於它們用於系統編程和高性能應用編程。系統語言都具備兩個特性,這使它們適合被編譯爲 WebAssembly。下一節將詳細介紹設置完整的代碼示例(使用 C 和 TypeScript)以及來自 WebAssembly 本身的文本格式語言的示例。程序員
這三種系統語言須要顯式數據類型,例如 int 和 double,用於變量聲明和從函數返回的值。例如如下代碼段說明了 C 中的 64 位加法:github
long n1 = random();
long n2 = random();
long sum = n1 + n2;
複製代碼
庫函數 random 聲明以 long 爲返回類型:web
long random(); /* returns a long */
複製代碼
在編譯過程當中,C 源被翻譯成彙編語言,而後再將其翻譯成機器代碼。在英特爾彙編語言(AT&T flavor)中,上面的最後一個 C 語句的功能相似如下內容(## 爲彙編語言的註釋符號):npm
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 語言並不是爲了取代 JS,而是爲了經過在計算綁定任務上提供更好的性能來補充 JS。WebAssembly 在下載方面也有優點。瀏覽器將 JS 模塊做爲文本提取,這正是 WebAssembly 可以解決的低效率問題之一。WebAssembly 中的模塊是緊湊的二進制格式,可加快下載速度。
一樣使人感興趣的是 JS 和 WebAssembly 如何協同工做。 JS 旨在讀入文檔對象模型(DOM),即網頁的樹形表示。相比之下,WebAssembly 沒有爲 DOM 提供任何內置功能,可是 WebAssembly 能夠導出 JS 根據須要調用的函數。這種關注點分離意味着清晰的分工:
DOM<----->JS<----->WebAssembly
複製代碼
不管用什麼方言,JS 都應該管理 DOM,但 JS 也能夠用經過 WebAssembly 模塊提供的通用功能。代碼示例有助於說明,本文中的代碼案例能夠在個人網站上找到(condor.depaul.edu/mkalin)。
生產級代碼案例將使 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 猜測的反例,也沒有人找到證據將猜測提高到一個定理。這個猜測很簡單,就像用程序測試同樣,是數學中一個極具挑戰性的問題。
下面的 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,你能夠[使用本指南進行安裝(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** 包含在編譯命令中。
2. 使用 Emscription 開發 Web 服務器(或等效的)來託管 Web 化應用:
```bash
% emrun --no_browser --port 9876 . ## . is current working directory, any port number you like
複製代碼
要禁止顯示警告消息,能夠包含標誌 --no_emrun_detect。此命令用於啓動 Web 服務器,該服務器承載當前工做目錄中的全部資源;特別是 hstone.html、hstone.js 和 hstone.webasm。
這個截圖顯示了我用 Firefox 運行的示例輸出。
圖1. web 化 hstone 程序
結果很是顯著,由於完整的編譯過程只須要一個命令,並且不須要對原始 C 程序進行任何更改。
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,它是具備顯式數據類型的 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 文件(個人網站上找到(condor.depaul.edu/mkalin)中的 index.html)包含如下腳本:獲取並加載 WebAssembly 模塊 hstone.wasm 而後實例化此模塊,以即可以在瀏覽器控制檯中調用導出的 hstone 函數進行確認。
示例 3. TypeScript 代碼的 HTML頁面
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script></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 將在重用和性能方面茁壯成長。