了解為何 WebAssembly 是未來 Web 平台中非常重要的一部分
WebAssembly 是當今非常熱門的話題。
WebAssembly 是一種用於網頁的新型低階二進制格式。它不是你要編寫的程式語言,而是其他較高層次的語言(目前有 C、Rust 和 C++)經過編譯後才能運行於瀏覽器中。
它被設計為快速、內存安全和開放的。
你將不會直接編寫 WebAssembly 代碼(也稱為 WASM),而是使用其他語言來編譯成 WebAssembly 低階格式。
它是繼 90 年代 JavaScript 首次出現後第二種能被網頁瀏覽器理解的語言。
WebAssembly 是由W3C WebAssembly 工作組開發的標準。現在,所有現代瀏覽器(Chrome、Firefox、Safari、Edge、移動瀏覽器)和 Node.js 都支援它。
我有說 Node.js 嗎?是的,因為 WebAssembly 誕生於瀏覽器,但 Node.js 自從 8 版本起就已經支援了,你可以使用除 JavaScript 以外的任何語言來構建 Node.js 應用程序的部分組件。
多虧了 WebAssembly,不喜歡 JavaScript 的人或者喜歡使用其他語言的人現在有了選擇,可以使用不同於 JavaScript 的語言編寫 Web 應用程序的部分組件。
請注意:WebAssembly 不打算取代 JavaScript,它只是一種將其他語言編譯到瀏覽器中的方式,用於增強以這些語言編寫的應用程序的功能或者重用現有的應用程序組件。
JavaScript 和 WebAssembly 代碼可以互相操作,為網頁提供出色的用戶體驗。
對於安全性的考慮
WebAssembly 代碼運行在一個沙盒環境中,擁有和 JavaScript 相同的安全策略,瀏覽器會確保遵守同源策略和權限策略。
如果你對此感興趣,我建議你閱讀WebAssembly 中的內存以及WebAssembly 的安全文檔。
性能方面的考慮
WebAssembly 的設計目標是追求速度。它的主要目標是變得非常非常快。它是一種編譯語言,這意味著在執行之前,程序將被轉換成二進制形式。
它可以達到接近本地編譯語言(如 C)的性能。
與動態解釋型的 JavaScript 相比,速度是無法相提並論的。WebAssembly 在性能上始終優於 JavaScript,因為當執行 JavaScript 時,瀏覽器必須解釋指令並在運行時進行優化。
誰正在使用 WebAssembly?
WebAssembly 已經可以使用了嗎?是的!許多公司已經在使用它來提升其在網頁上的產品。
一個很好的例子是你可能已經使用過的 Figma。它是一個設計應用程序,我也使用它來製作一些日常工作中使用的圖形。該應用程序運行在瀏覽器中,並且非常快速。
該應用程序使用 React 構建,但其主要部分,即圖形編輯器,是一個使用 C++ 編譯為 WebAssembly 的應用程序,在 Canvas 上使用 WebGL 渲染。
早在 2018 年初,AutoCAD 就在 Web 應用程序中推出了其受歡迎的設計產品,使用 WebAssembly 渲染其複雜的編輯器,並從桌面客戶端代碼庫遷移。
對於需要核心部分具有高性能功能的產品來說,Web 不再是一種限制性技術。
如何使用 WebAssembly?
可以使用 Emscripten 將 C 和 C++ 應用程序轉換為 WebAssembly。Emscripten 是一個工具鏈,可以將代碼編譯為兩個文件:
- 一個
.wasm
文件 - 一個
.js
文件
其中,.wasm
文件包含實際的 WebAssembly 代碼,.js
文件包含使 JavaScript 能夠執行 WebAssembly 代碼的串接程式。
Emscripten 可以為你完成很多工作,例如將 OpenGL 調用轉換為 WebGL、為 DOM API 和其他瀏覽器和設備 API 提供綁定、提供在瀏覽器中使用的文件系統工具等等。由於 WebAssembly 在直接使用上述功能時預設是無法訪問的,因此這些工具非常有幫助。
Rust 代碼不同,它可以直接編譯為 WebAssembly,具體操作請參考 Mozilla 開發者網站上的 WebAssembly 轉為 Rust 文檔。
WebAssembly 將來有什麼計劃?它的發展方向是什麼?
WebAssembly 目前版本為 1.0。它當前只正式支援 3 種語言(C、Rust、C++),但很多其他語言也在增加支援。目前還不支援 Go、Java 和 C# 的 WebAssembly 編譯,因為它們尚不支援垃圾回收機制。
在 WebAssembly 中調用瀏覽器 API 時,你目前需要先與 JavaScript 進行交互。正在進行的工作是使 WebAssembly 在瀏覽器中更加獨立,使其能夠直接調用 DOM、Web Workers 或其他瀏覽器 API。
此外,正在進行的工作還包括使 JavaScript 代碼能夠加載 WebAssembly 模塊,通過 ES Modules 規範實現。
安裝 Emscripten
通過克隆 emsdk
GitHub 倉庫來安裝 Emscripten:
git clone https://github.com/juj/emsdk.git
然後
dev cd emsdk
現在,確保你安裝了最新版本的 Python。我之前有一個 2.7.10 版本,這導致了一個 TLS 錯誤。
我不得不從 https://www.python.org/getit/ 下載新版本(2.7.15),安裝它,然後運行隨附的 Install Certificates.command
程序。
然後
./emsdk install latest
讓它下載並安裝套件,然後運行
./emsdk activate latest
最後,通過運行以下命令將路徑添加到你的 shell 中:
source ./emsdk\_env.sh
將 C 程序編譯為 WebAssembly
我將創建一個簡單的 C 程序,並希望它可以在瀏覽器中運行。
這是一個相當標準的 “Hello World” 程序:
#include<stdio.h>
int main(int argc, char **argv) {
printf("Hello World\n");
return 0;
}
你可以使用以下命令編譯它:
gcc -o test test.c
運行 ./test
將在控制台中打印 “Hello World”。
現在,讓我們使用 Emscripten 編譯此程序以在瀏覽器中運行:
emcc test.c -s WASM=1 -o test.html
Emscripten 生成了一個已經包裝好編譯後的 WebAssembly 程序的 html 頁面,可以直接運行。但需要注意的是,你需要從 Web 服務器打開它,而非從本地文件系統。你可以使用本地 Web 服務器,例如全局 npm 包 http-server
(如果還未安裝,可以使用 npm install -g http-server
進行安裝)。下面是使用 http-server
的示例:
如你所見,該程序運行並在控制台中打印了 “Hello World”。
這是一種將程序編譯為 WebAssembly 並運行的方法。另一種選擇是使程序公開一個將從 JavaScript 中調用的函數。
從 JavaScript 調用 WebAssembly 函數
讓我們稍微修改之前定義的 Hello World 程序。
首先,將 emscripten
的頭文件包含進來:
#include <emscripten/emscripten.h>
接下來,定義一個 hello
函數:
int EMSCRIPTEN_KEEPALIVE hello(int argc, char **argv) {
printf("Hello!\n");
return 8;
}
需要使用 EMSCRIPTEN_KEEPALIVE
來保留此函數,以避免被自動刪除,因為編譯器會優化最終的編譯代碼,並刪除未使用的函數。但我們將在 JavaScript 中動態調用此函數,編譯器無法知道這一點。
這個小函數會打印 “Hello!",並返回數字 8。
現在,我們使用 emcc
再次編譯:
emcc test.c -s WASM=1 -o test.html -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']"
這次,我們添加了 EXTRA_EXPORTED_RUNTIME_METHODS
標誌,告訴編譯器將 ccall
和 cwrap
函數保留在 Module 對象上,我們將在 JavaScript 中使用它們。
現在,我們可以再次啟動 Web 服務器,在頁面打開後在控制台中調用 Module.ccall('hello', 'number', null, null')
,它將打印 “Hello!",並返回 8:
Module.ccall
函數接受 4 個參數:C 函數名稱、返回類型、參數類型(一個數組)和參數(同為數組)。
如果我們的函數接受兩個字符串作為參數,我們可以像這樣調用它:
Module.ccall('hello', 'number', ['string', 'string'], ['hello', 'world'])
可用的類型包括 null
、string
、number
、array
和 boolean
。
我們還可以使用 Module.cwrap
函數創建 JavaScript 對 hello
函數的封裝,這樣我們就可以在 JavaScript 中反復調用此函數:
const hello = Module.cwrap('hello', 'number', null, null)