Categories
程式開發

WebAssembly上手:基礎指南


授權聲明:本文最初發佈於EVIL MARTIANS博客,原文標題:動手WebAssembly:嘗試基礎,作者Polina Gurtovaya & Andy Barnov。本文經原博客授權由InfoQ中文站翻譯並分享。

只需Web開發的一般知識就能通過本文輕鬆上手WebAssembly。要通過本文的可運行代碼示例嘗試WebAssembly,你只需要一個編輯器、任意現代瀏覽器和本文隨附的,帶有C和Rust工具鏈的Docker映像。

WebAssembly已經誕生三年了。它可以在所有現代瀏覽器中使用,還有一些公司甚至開始勇敢地在生產環境中使用它了(說的自然是Figma)。它背後的名字如雷貫耳:Mozilla、Microsoft、Google、Apple、Intel、RedHat——它們和其他很多公司的一些最優秀的工程師一直在為WebAssembly做出貢獻。人們普遍認為它是Web技術的下一次重大變革,但更主流的前端社區並不急於採用它。我們都知道HTML、CSS和JavaScript是Web的三大基礎,要改造世界需要花費的時間遠不止三年這麼短。尤其是人們一搜索它的概念就會蹦出下面這種內容:

WebAssembly是一種用於基於棧的虛擬機的虛擬指令集架構二進制指令格式。

如果你看了後感到一頭霧水,那肯定很難有興趣繼續研究下去。

這篇文章的目的是以一種更容易理解的方式來解釋WebAssembly,並引導你完成一些在Web頁面上使用WebAssembly的具體示例。如果你是對WebAssembly感到好奇的開發人員,卻從未有過嘗試的機會,那麼本文會很合適你——如果你很喜歡龍的話那就更好了。

龍出沒注意

在我自己深入研究這一主題之前,我對WebAssembly的印象就是某種龍:強大、快速、危險誘人,但又神秘而致命。在我的Web技術思維導圖上,WebAssembly也屬於“此處有龍出沒”類別:探索這些技術時請自行承擔風險。

WebAssembly上手:基礎指南 1

那些擔心其實是沒有根據的,前端開發的基石並沒有被打碎。 WebAssembly仍然屬於客戶端應用程序領域,因此它仍在你的瀏覽器沙箱中運行。它仍然依賴熟悉的JavaScript API。它還允許你直接提供二進製文件,從而極大擴展了可以在客戶端上執行的操作的範圍。

本文將介紹其工作原理,如何將代碼編譯為Wasm以及何時在項目中使用WebAssembly。

人類代碼與機器代碼

在WebAssembly誕生之前,JavaScript是由瀏覽器執行的編程語言中唯一一種全功能的。為Web編寫代碼的人們知道如何使用JS表達想法,並知道客戶端計算機可以運行他們的代碼。

編程小白也能理解以下JavaScript代碼的含義,雖說它“解決”的任務沒什麼意義:將隨機數除以2並將其添加到數字數組11088次。

function div() {
  return Math.random() / 2;
}
const arr = [];
for (let i = 0; i < 11088; i++) {
  arr[i] = div();
}

上面的代碼是人類能讀懂的,但對於通過Web接收代碼的客戶端計算機的CPU而言卻毫無意義,可後者必須運行它。 CPU理解的是機器指令,它們按照處理器生成結果所必須的(相當平淡的)步驟序列來編碼。
要運行我們這一小段代碼,我的CPU(Intel x86-64)需要516條指令。這些指令以彙編語言(機器語言的文字表示)顯示時是下面的樣子。指令名稱是很難懂的,要理解它們,你需要一本處理器隨附的厚厚手冊。

WebAssembly上手:基礎指南 2

x86_64彙編的一些指令

在每個時鐘週期(2GHz表示每秒20億個週期),處理器將嘗試獲取一個或多個指令並執行它們。通常,有許多指令是同時傳送的(稱為指令級並行性)。

為了盡可能快速地運行你的代碼,處理器採用了一些技巧,例如流水線、分支預測、推測執行、預取等。處理器有復雜的緩存系統,以盡快獲取指令數據(以及指令本身)。從主內存中獲取數據比從緩存中獲取數據要慢幾十倍。

不同的CPU實現了不同的指令集架構(ISA),因此PC中的CPU(很可能基於Intel x86)將無法理解智能手機中CPU(最可能是某種ARM架構)的機器代碼。

好消息是——如果你為Web編寫代碼,則不必介意處理器架構之間的差異。現代瀏覽器是高效的編譯器,可以將你的代碼愉快地轉換為客戶端計算機的CPU可以理解的內容。

編譯器入門

為了了解WebAssembly如何工作,我們不得不談論一下編譯器。編譯器的工作是獲取人類可讀的源代碼(JavaScript、C、Rust,諸如此類),並將其轉變為一組指令,供目標處理器理解。在發出機器代碼之前,編譯器首先將你的代碼轉換為中間表示(IR),即對程序進行精確的“重寫”,而這種重寫與源語言和目標語言無關。

編譯器將查看IR,研究如何優化它,可能會因此生成另一個IR,然後生成下一個IR,直到它確定無法做進一步的優化為止。因此,你在編輯器中編寫的代碼可能與計算機將執行的代碼完全不同。

為了具體說明,以下是一些C代碼的加法和乘法運算。

#include 
int main()
{
    int result = 0;
    for (int i = 0; i  10) {
            result += i * 2;
        } else {
            result += i * 11;
        }
    }
    printf("%dn", result);
    return 0;
}

下面是由編譯器生成的,LLVM紅外格式的內部表示形式,這種格式很流行。

define hidden i32 @main() local_unnamed_addr #0 {
entry:
  %0 = tail call i32 (i8*, ...) @iprintf(…), i32 10395)
  ret i32 0
}

這裡的重點是,在執行優化時,編譯器就會得出計算的結果,而不是讓處理器在運行時進行數學運算。因此,i32 10395部分正好是上面的C代碼最終將輸出的數字。
編譯器有很多魔術來加速:避免在運行時執行“效率低下”的人工代碼,並用更優化的機器版本代替。

WebAssembly上手:基礎指南 3

編譯器的工作機制

大多數現代編譯器還有一個“中端”,可在後端和前端之間執行優化。

編譯器管道是一頭複雜的怪獸,但我們可以將其拆分為兩部分:前端和後端。編譯器前端解析源代碼,對其進行分析,然後轉換為IR;然後編譯器後端針對目標優化IR,並生成目標代碼。

WebAssembly上手:基礎指南 4

前端和後端

現在我們回到Web上。

如果我們可以有一種所有瀏覽器都可以理解的中間表示會怎麼樣呢?

然後,我們可以將其用作程序編譯的目標,而不必擔心與客戶端系統的兼容性。我們還可以使用任何語言編寫程序,不再只限於JavaScript。瀏覽器將獲取我們代碼的中間表示,並上演那些後端魔術:將IR轉換為客戶端架構的機器指令。

這就是WebAssembly的全部目的!

WebAssembly上手:基礎指南 5

WebAssembly:Web的IR

為了實現用單一格式表示任何語言編寫代碼的夢想,WebAssembly的開發人員必須做出一些戰略性的架構選擇。

為了使瀏覽器能夠在最短的時間內獲取代碼,格式必須緊湊。二進制是你可以獲得的最緊湊的文件。

為了使編譯高效,我們需要在不犧牲可移植性的情況下盡可能接近機器指令。由於所有指令集架構都依賴於硬件,並且不可能針對能運行瀏覽器的所有系統進行定制,因此WebAssembly的創建者選擇了虛擬ISA:一組用於抽像機器的指令。它不對應任何實際的CPU,但可以用軟件有效地處理。

虛擬ISA非常底層,足以輕鬆轉換為特定的機器指令。與實際的CPU不同,用於WebAssembly的抽像機不依賴寄存器——現代處理器在操作數據之前放置數據的位置。相反,它使用棧數據結構:例如,一條add指令將從棧中彈出兩個最高的數字,將它們加在一起,然後將結果推回棧頂部。

現在,當我們終於了解“基於棧的虛擬機的虛擬指令集架構和二進制格式”的含義時,就該釋放WebAssembly的能量了!

放開那條龍!

我們將實現一個簡單的算法來繪製一條稱為龍曲線的簡單分形曲線。這裡最重要的不是源碼:我們將向你展示創建WebAssembly模塊,並在瀏覽器中運行它所需要的操作。

這裡不會直接使用腳本這類好用的高級工具,而是直接使用一個Clang編譯器,帶有LLVM WebAssembly後端。

最後,我們的瀏覽器將能夠繪製以下圖片:

WebAssembly上手:基礎指南 6

龍曲線和折點

我們將從畫布的起點畫一條線,然後左右交替轉向,以實現所需的分形。

程序的目標是生成一個坐標數組,供我們的直線使用。將其變成圖片是JavaScript的工作。負責生成數組的代碼是用老字號的C語言編寫的。

不用擔心,你用不著花費幾小時來設置開發環境,因為我們已經將你可能用到的所有工具烘焙到了一個Docker視頻中。你在計算機上唯一需要的就是碼頭工人本身,因此,如果你以前從未使用過它——現在是時候安裝它了,只需按照對應你操作系統的指南操作即可。

提示:命令行示例假定你使用的是Linux或Mac。要在Windows上運行,你可以使用WSL(建議升級到WSL2)或更改語法以支持Power Shell:使用反引號代替來換行,並使用${pwd}:/temp代替$(pwd):$(pwd)。

啟動你的終端並創建一個文件夾,在其中放置示例:

mkdir dragon-curve-llvm && cd dragon-curve-llvm
touch dragon-curve.c

現在打開文本編輯器,並將以下代碼放入新創建的文件中:

// dragon-curve-llvm/dragon-curve.c
#ifndef DRAGON_CURVE
#define DRAGON_CURVE
// Helper function for generating x,y coordinates from "turns"
int sign(int x) { return (x % 2) * (2 - (x % 4)); }
// Helper function to generate "turns"
// Adapted from https://en.wikipedia.org/wiki/Dragon_curve#[Un]folding_the_dragon
int getTurn(int n)
{
  int turnFlag = (((n + 1) & -(n + 1)) << 1) & (n + 1);
  return turnFlag != 0 ? -1 : 1; // -1 for left turn, 1 for right
}
// fills source with x and y points [x0, y0, x1, y1,...]
// first argument is a pointer to the first element of the array
// that will be provided at runtime.
void dragonCurve(double source[], int size, int len, double x0, double y0)
{
  int angle = 0;
  double x = x0, y = y0;
  for (int i = 0; i < size; i++)
  {
    int turn = getTurn(i);
    angle = angle + turn;
    x = x - len * sign(angle);
    y = y - len * sign(angle + 1);
    source[2 * i] = x;
    source[2 * i + 1] = y;
  }
}
#endif

現在我們需要使用LLVM的及其WebAssembly後端鏈接器將其編譯為WebAssembly。運行下面的命令讓Docker容器來處理。這只是對帶有一組標誌的clang二進製文件的調用。

docker run --rm -v $(pwd):$(pwd) -w $(pwd) zloymult/wasm-build-kit 
clang --target=wasm32 -O3 -nostdlib -Wl,--no-entry -Wl,--export-all -o dragon-curve.wasm dragon-curve.c
  • –target=wasm32告訴編譯器將WebAssembly作為編譯目標。
  • -O3應用最大優化。
  • -nostdlib聲明不要使用系統庫,因為它們在瀏覽器的上下文中是無用的。
  • -Wl、-no-entry-Wl、-export-all是標誌,它們指示鏈接器導出我們從WebAssembly模塊定義的所有C函數,並忽略main()的缺失。

結果,你將看到一個dragon-curve.wasm文件出現在文件夾中。它是一個包含我們程序中所有530字節的二進製文件!你可以像這樣檢查它:

docker run --rm -v $(pwd):$(pwd) -w $(pwd) zloymult/wasm-build-kit 
wasm-objdump dragon-curve.wasm -s

WebAssembly上手:基礎指南 7

wasm-objdump dragon-curve.wasm

可以使用WebAssembly工具鏈中一個叫做拜納里安的出色工具來優化二進製文件的體積。

docker run --rm -v $(pwd):$(pwd) -w $(pwd) zloymult/wasm-build-kit 
wasm-opt -Os dragon-curve.wasm -o dragon-curve-opt.wasm

這樣我們可以從生成的文件中刪除一百個左右的字節。

龍膽

二進制的文件一個缺陷是它們不能被人類理解。幸運的是,WebAssembly具有兩種格式:二進制和文本。你可以使用WebAssembly Binary工具箱在兩者之間轉換。試著運行:

docker run --rm -v $(pwd):$(pwd) -w $(pwd) zloymult/wasm-build-kit 
wasm2wat dragon-curve-opt.wasm > dragon-curve-opt.wat

現在,我們在文本編輯器中檢查生成的dragon-curve-opt.wat文件。

WebAssembly上手:基礎指南 8

.wat內容

這些有趣的括號稱為s表達式(就像在老派的Lisp中一樣)。它們用於表示樹狀結構。所以我們的Wasm文件是一棵樹。樹的根是一個module。它的工作原理很像你熟悉的JavaScript模塊。它有導入和導出。

WebAssembly的基本構建塊是在棧上運行的指令。

WebAssembly上手:基礎指南 9

wasm指令

指令被組合成可以從模塊導出的函數。

WebAssembly上手:基礎指南 10

導出sign和getTurn

你可能會看到代碼周圍散佈著if、else和loop語句,這是WebAssembly最突出的特性之一:通過使用所謂的結構化控制流(就像高級語言),它可以避免GOTO跳轉並允許一次性解析源。

WebAssembly上手:基礎指南 11

結構化控制流

現在看一下導出的sign函數,並查看基於棧的虛擬ISA的工作方式。

WebAssembly上手:基礎指南 12

sign函數

還有另一個重要的實體,稱為表(Table)。表是線性數組,就像內存一樣,但是它們僅存儲函數引用。無論它們是否是WebAssembly模塊的一部分,它們都用於間接調用函數。

我們的函數接收一個整數參數(param i32)並返回一個整數結果(result i32)。一切都在棧上完成。首先,我們推入值:整數2,其後是函數的第一個參數(local.get 0),然後是整數4。然後應用i32.rem_s指令從棧中刪除兩個值(第一個函數參數和整數4),將第一個值除以第二個值,然後將餘數推回棧。現在,最上面的值是餘數和數字2。 i32.sub從棧中彈出它們,從一個中減去另一個,然後推入結果。前五個指令等效於(2 - (x % 4))。

Wasm使用簡單的線性內存模型:你可以將WebAssembly內存視為簡單的字節數組。

在我們的.wat文件中,它是通過(export memory(memory0))從模塊中導出的。也就是說我們可以從外部在WebAssembly程序的內存上操作,這就是我們下面要做的。

燈光就緒,攝像就緒,開拍!

為了讓瀏覽器繪製一條龍曲線,我們需要一個HTML文件。

touch index.html

放一個帶有空canvas標籤的樣板,並初始化我們的初始值:size是曲線的步數,len是單步的長度,x0和y0設置起始坐標。




  
    Dragon Curve from WebAssembly
  
  
    
    
      const size = 2000;
      const len = 10;
      const x0 = 500;
      const y0 = 500;
    
  

現在,我們需要加載.wasm文件並實例化WebAssembly模塊。與JavaScript不同,我們不需要等待整個模塊加載就可以使用它——WebAssembly是在數據流入時即時編譯和執行的。
我們使用標準的fetch API加載模塊,並使用內置的WebAssembly JavaScript API對其實例化。 WebAssembly.instantiateStreaming返回一個用模塊對象解析的promise,其中包含我們模塊的實例。現在,我們的C函數作為實例的exports可用,並且我們可以根據需要從JavaScript中使用它們。




  
    Dragon Curve from WebAssembly
  
  
    
    
      const size = 2000;
      const len = 10;
      const x0 = 500;
      const y0 = 500;
      WebAssembly.instantiateStreaming(fetch("/dragon-curve.wasm"), {
        // for this example, we don't import anything
        imports: {},
      }).then((obj) => {
        const { memory, __heap_base, dragonCurve } = obj.instance.exports;
        dragonCurve(__heap_base, size, len, x0, y0);
        const coords = new Float64Array(memory.buffer, __heap_base, size);
        const canvas = document.querySelector("canvas");
        const ctx = canvas.getContext("2d");
        ctx.beginPath();
        ctx.moveTo(x0, y0);
        [...Array(size)].forEach((_, i) => {
          ctx.lineTo(coords[2 * i], coords[2 * i + 1]);
        });
        ctx.stroke();
        // If you want to animate your curve, change the last four lines to
        // [...Array(size)].forEach((_, i) => {
        //   setTimeout(() => {
        //     requestAnimationFrame(() => {
        //       ctx.lineTo(coords[2 * i], coords[2 * i + 1]);
        //       ctx.stroke();
        //     });
        //   }, 100 * i);
        // });
      });
    
  

仔細看看instance.exports。除了生成坐標的dragonCurve C函數之外,我們還返回了一個表示WebAssembly模塊線性內存的memory對象。這裡需要小心,因為它可能包含重要內容,例如我們用於虛擬機的指令棧。

技術上講,我們需要一個內存分配器來避免混亂。但對於這個簡單的示例,我們將讀取內部__heap_base屬性,其為我們提供了一個可以安全使用的內存區域(堆)的偏移量。

我們將這個偏移量賦給dragonCurve函數的“好”內存,調用它,然後將填充了坐標的堆內容提取為一個Float64Array。

本章的靈感來自死亡的精彩文章“無需Emscripten將C編譯為WebAssembly

剩下的只是根據從Wasm模塊提取的坐標在畫布上畫一條線。現在我們要做的就是在本地提供HTML。我們需要一個基本的Web服務器,否則將無法從客戶端獲取Wasm模塊。所幸Docker映像已完成了所有設置:

docker run --rm -v $(pwd):$(pwd) -w $(pwd) -p 8000:8000 zloymult/wasm-build-kit 
python -m http.server

轉到http://localhost:8000,龍曲線就在眼前!

該來點進階內容了

上面的“純LLVM”方法的目標是極為簡單的;我們編譯程序時沒用系統庫,還以最糟糕的方式管理內存:計算堆的偏移量,這樣我們得以揭開WebAssembly內存模型的神秘面紗。但在實際的應用程序中,我們希望適當地分配內存並使用系統庫,其中“系統”是我們的瀏覽器:WebAssembly仍在沙箱中運行,無法直接訪問你的操作系統。

所有這些都可以在腳本的幫助下完成:這是一個用於編譯WebAssembly的工具鏈,它可以模擬瀏覽器內部的許多系統功能:使用STDIN、STDOUT和文件系統,甚至可以將OpenGL圖形自動轉換為WebGL。它還集成了我們之前用來壓縮二進製文件的Bynarien,因此我們用不著專門優化體積了。

Emscripten誕生早於WebAssembly:首先,它被用來將C/C++代碼編譯為JavaScript和asm.js,而且現在還能這麼幹!

WebAssembly上手:基礎指南 13

註冊

是時候正常使用WebAssembly了!我們的C代碼不會變。先創建一個單獨的文件夾以便對比代碼,並複制我們的源碼。

cd .. && mkdir dragon-curve-emscripten && cd dragon-curve-emscripten
cp ../dragon-curve-llvm/dragon-curve.c .

我們已經把ecmsripten打包進了Docker映像,因此你無需在系統上安裝任何程序即可運行以下命令:

docker run --rm -v $(pwd):$(pwd) -w $(pwd) zloymult/wasm-build-kit 
emcc dragon-curve.c -Os -o dragon-curve.js 
-s EXPORTED_FUNCTIONS='["_dragonCurve", "_malloc", "_free"]' 
-s EXPORTED_RUNTIME_METHODS='["ccall"]' 
-s MODULARIZE=1

如果命令成功執行,你將看到兩個新文件:小巧的dragon-curve-em.wasm,以及一個15Kb的怪物dragon-curve-em.js(縮小後),其中包含WebAssembly模塊的實例化邏輯和各種瀏覽器polyfills。那就是目前在瀏覽器中運行Wasm的代價:我們仍需要大量JavaScript膠水才能將它們固定在一起。

這是我們所做的:

  • -Os告訴emscripten優化體積:Wasm和JS都要優化。
  • 請注意,我們只需指定.js文件名作為輸出,.wasm就會自動生成。
  • 我們還可以從生成的Wasm模塊中選擇要導出的函數,注意名稱前需要帶下劃線,也就是-s EXPORTED_FUNCTIONS='["_dragonCurve", “_malloc”, “_free”]'。最後兩個函數幫助我們處理內存。
  • 由於我們的源碼是C,因此還必須導出emscripten為我們生成的ccall函數。
  • MODULARIZE=1允許我們使用一個全局Module函數,其返回一個帶有wasm模塊實例的Promise。

現在我們可以創建HTML文件,並粘貼新內容:

touch index.html


  
    Dragon Curve from WebAssembly
  
  
  
    
      Your browser does not support the canvas element.
    
    
      Module().then((instance) => {
        const size = 2000;
        const len = 10;
        const x0 = 500;
        const y0 = 500;
        const canvas = document.querySelector("canvas");
        const ctx = canvas.getContext("2d");
        const memoryBuffer = instance._malloc(2 * size * 8);
        instance.ccall(
          "dragonCurve",
          null,
          ["number", "number", "number", "number"],
          [memoryBuffer, size, len, x0, y0]
        );
        const coords = instance.HEAPF64.subarray(
          memoryBuffer / 8,
          2 * size + memoryBuffer / 8
        );
        ctx.beginPath();
        ctx.moveTo(x0, y0);
        [...Array(size)].forEach((_, i) => {
          ctx.lineTo(coords[2 * i], coords[2 * i + 1]);
        });
        ctx.stroke();
        instance._free(memoryBuffer);
      });
    
  

有了ecmscripten,我們不必直接使用瀏覽器API來實例化WebAssembly,就像在上一個示例中使用WebAssembly.instantiateStreaming所做的那樣。

相反,我們使用emscripten提供給我們的Module函數。當我們編譯程序時,Module將返回一個帶有我們定義的所有導出的promise。當這個promise被解析時,我們可以使用_malloc函數在內存中為坐標保留一個位置。它返回一個帶有偏移量的整數,然後將其保存到memoryBuffer變量中。它比上一個示例中不安全的heap_base方法安全得多。

參數2 * size * 8表示我們將分配足夠長的數組,以便為每個步驟存儲兩個坐標(x,y),每個坐標佔用8個字節的空間(float64)。

Emscripten有一種調用C函數的特殊方法——ccall。我們用它來調用dragonCurve函數,其以memoryBuffer提供的一個偏移量填充內存。畫布代碼與前面的示例相同。我們還利用emscripteninstance._free方法在使用後清理內存。

Rust,和運行其他人的代碼

C能這麼順利地轉換為WebAssembly,原因之一是它使用簡單的內存模型並且不依賴垃圾回收。否則,我們將不得不將整個語言運行時烘焙到我們的Wasm模塊中。從技術上講這是可行的,但是它將把二進製文件撐大好多圈,並影響加載和執行時間。

當然,並不是只有C和C++可以編譯為WebAssembly。擁有LLVM前端的語言是最好的備選,Rust則是其中最突出的。

Rust的妙處在於它有一個出色的內置軟件包管理器貨物,與老字號的C語言相比,它很容易發現和重用現有庫。

我們將展示將現有的Rust庫轉換為WebAssembly模塊有多容易——這裡要用上非常棒的垃圾袋工具鏈,它讓我們能夠飛速引導Wasm項目。

我們的Docker鏡像已經內置了wasm-pack,用它開始一個新項目。如果你仍在上一個示例中的dragon-curve-ecmscripten文件夾中,請返回上一級。 Wasm-pack使用與rails new或create-react-app相同的方法來生成項目:

docker run --rm -v $(pwd):$(pwd) -w $(pwd) -e "USER=$(whoami)" zloymult/wasm-build-kit wasm-pack new rust-example

現在,你可以進入rust-example文件夾並在編輯器中打開。我們已經將龍曲線的C代碼轉換為Rust,並打包成一個Cargo crate。
Rust項目中的所有依賴項都在Cargo.toml文件中管理,其行為與package.json或Gemfile很像。在編輯器中打開它,找到當前僅包含wasm-bindgen的[dependencies]部分,然後添加我們的外部crate。

# Cargo.toml
[dependencies]
# ...
dragon_curve = {git = "https://github.com/HellSquirrel/dragon-curve"}

項目源碼位於src/lib.rs中,我們要做的就是定義一個函數,從導入的crate中調用dragon_curve。將下面的代碼插入文件末尾:

// src/lib.rs
#[wasm_bindgen]
pub fn dragon_curve(size: u32, len: f64, x0: f64, y0: f64) -> Vec
{
  dragon_curve::dragon_curve(size, len, x0, y0)
}

是時候編譯結果了。注意這些標誌看起來更人性化。 Wasm-pack有用於綁定JavaScript的內置Webpack支持,並且需要的話甚至可以生成HTML,但是我們將採用最小方法並設置-- target Web。只需將一個Wasm模塊和一個JS包裝器編譯為一個原生ES模塊。
這一步可能需要一些時間,具體取決於你的機器和網絡連接:

docker run --rm -v $(pwd):$(pwd) -w $(pwd)/rust-example -e "USER=$(whoami)" zloymult/wasm-build-kit wasm-pack build --release --target web

你可以在項目的pkg文件夾中找到結果。是時候在項目根目錄中創建HTML文件了。這裡的代碼是我們所有示例中最簡單的:我們只是原生地將dragon_curve函數作為JavaScript導入來使用。在幕後,Wasm二進製文件負責那些繁重工作,而且我們不再需要像前面的示例中那樣手動處理內存。
另一件事是異步init函數,它讓我們能等待Wasm模塊完成初始化。




  
    
    
    Document
  
  
    
    
      import init, { dragon_curve } from "/pkg/rust_example.js";
      (async function run() {
        await init();
        const size = 2000;
        const len = 10;
        const x0 = 500;
        const y0 = 500;
        const coords = dragon_curve(size, len, x0, y0);
        const canvas = document.querySelector("canvas");
        const ctx = canvas.getContext("2d");
        ctx.beginPath();
        ctx.moveTo(x0, y0);
        [...Array(size)].forEach((_, i) => {
          ctx.lineTo(coords[2 * i], coords[2 * i + 1]);
        });
        ctx.stroke();
      })();
    
  

現在提供HTML並享受結果!

docker run --rm -v $(pwd):$(pwd) -w $(pwd) -p 8000:8000 zloymult/wasm-build-kit 
python -m http.server

顯然,從開發人員的經驗層面來看Rust和wasm-pack明顯勝出。當然,我們只是簡單介紹了一些基礎知識:emscripten或wasm-pack可以做的事情還很多,例如直接操作DOM。

請查閱“DOM你好世界”“使用Rust的單頁應用程序”和Emscripten文檔

同時,在遙遠的瀏覽器中……

WebAssembly不仅带来了可移植性、源独立性和代码重用。它还承诺当浏览器运行Wasm代码时会有显著的性能优势。要了解在WebAssembly中重写Web应用程序逻辑的优点(和缺点),我们必须了解客户端的底层操作,以及它与执行JavaScript有何不同。

在過去的幾十年中,即便將JavaScript轉換為有效的機器代碼並非易事,瀏覽器還是非常擅長運行JS。所有的火箭科學都發生在瀏覽器引擎內部,這是Web上最聰明的人才進行編譯技術競賽的地方。

我們可能無法涵蓋所有​​引擎的內部工作原理,所以這裡只談一下V8,這是Chromium和Node JS的JS運行時,目前它在瀏覽器市場和JavaScript的後端環境中均占主導地位。

JS和Wasm都能由V8編譯並執行,但是方法略有不同。兩者的管道很像:獲取源碼,對其解析、編譯和執行。用戶必須等待所有步驟完成才能在設備上看到結果。

對於JavaScript,主要的權衡是在編譯時間與執行時間之間:我們可以非常快速地生成未優化的機器代碼,但是這將花費更長的時間運行;或者我們可以花更多的時間編譯並確保由此產生的機器指令是最高效的。

V8嘗試解決這一問題的方法如下:

WebAssembly上手:基礎指南 14

V8的工作方式(JS)

首先,V8解析JavaScript並將生成的抽象語法樹提供給名為點火的解釋器,後者將其轉換為基於一個寄存器型虛擬機的內部表示。在處理WebAssembly時這一步可以跳過,因為Wasm源已經是一組虛擬指令了。

在將JS解釋為字節碼時,Ignition會收集其他一些信息(反饋),幫助決定是否進一步優化。標為優化的函數被認為是“熱”的。

生成的字節碼最終出現在名為渦輪風扇的引擎的另一個組件中。它的工作是將內部表示轉換為目標架構的優化機器代碼。

為了獲得最佳性能,TurboFan必鬚根據Ignition的反饋來推測。例如,它可以“猜測”函數的參數類型。如果隨著新代碼的不斷出現,這些猜測失效了,那麼引擎將放棄所有優化並從頭開始。這種機制使代碼的執行時間無法預測。

WebAssembly上手:基礎指南 15

JS執行時間

Wasm讓瀏覽器引擎的工作更加輕鬆:由於.wasm格式,代碼已經採用了內部表示的形式,可以輕鬆進行多線程解析。另外,當我們在開發人員的機器上編譯WebAssembly文件時,一些優化已經包含在其中了。這意味著V8可以立即編譯和執行代碼,而無需像對JavaScript那樣反复優化和反優化。

WebAssembly上手:基礎指南 16

V8的工作方式(Wasm)

升空基準編譯器在V8中提供了“快速啟動”功能。 TurboFan及其出色的優化功能仍在發揮作用,只是這一次它不必猜測任何內容,因為源代碼已經具備所有必要的類型信息。 “熱”函數的概念不再適用,這使我們的執行時間具有確定性:我們提前知道了執行程序需要多長時間。

WebAssembly上手:基礎指南 17

Wasm執行時間

當然,你也可以在瀏覽器外部運行WebAssembly。有許多項目可讓你在任何客戶端上使用Wasm運行任何代碼:Wasm3WasmtimeWAMR瓦斯默等。如你所見,WebAssembly的雄心是最終超越瀏覽器,進入各種系統和設備。

何時使用WebAssembly

WebAssembly的創建是為了補充現有的Web生態系統:絕不是要替代JavaScript。使用現代瀏覽器時JS已經夠快了,並且對於大多數常見的Web任務(例如DOM操作),WebAssembly不會給我們帶來任何性能上的優勢。

WebAssembly的承諾之一是消除Web應用程序與其他各類軟件之間的界限:可以輕鬆地將用不同語言開發的成熟代碼庫引入瀏覽器。許多項目已經移植到Wasm中,包括遊戲圖像編解碼器,機器學習庫,甚至是語言運行時

Figma是現代設計師必不可少的工具,它從一開始就在生產環境中使用WebAssembly

在當下,沒有JavaScript根本無法使用純Wasm:無論你自己編寫代碼還是依靠工俱生成代碼,你都需要那些“膠水”代碼。

如果你希望用Wasm消除性能瓶頸,建議你三思,因為無需完全重寫就可以解決這些瓶頸。你絕不應該依賴對比單個任務的WebAssembly和JS的基準測試結果,因為在實際應用程序中,Wasm和JS是會一直互聯的。

查看WebAssembly建議,以了解Web上二進製文件的前景。

儘管WebAssembly仍處於MVP階段,但現在是開始嘗試它的最佳時機:有了我們在本文中演示的工具,你就可以開始運行它了。

如果你想更深入地研究WebAssembly,請查閱我們撰寫本文時為自己編制的閱讀清單。我們還創建了一個存儲庫,其中包含本文中的所有代碼示例。