在 VS Code 網頁版中執行 WebAssembly
2023 年 6 月 5 日,作者:Dirk Bäumer
VS Code 網頁版(https://vscode.dev)已經推出一段時間了,我們一直以來的目標是在瀏覽器中支援完整的編輯/編譯/除錯迴圈。對於 JavaScript 和 TypeScript 等語言來說,這相對容易,因為瀏覽器本身就帶有 JavaScript 執行引擎。但對於其他語言來說,這要困難得多,因為我們必須能夠執行(因此也能夠除錯)程式碼。例如,要在瀏覽器中執行 Python 原始碼,就需要一個能夠執行 Python 直譯器的執行引擎。這些語言執行時通常是用 C/C++ 編寫的。
WebAssembly 是一種虛擬機器二進位制指令格式。WebAssembly 虛擬機器已內置於現代瀏覽器中,並且存在將 C/C++ 編譯為 WebAssembly 程式碼的工具鏈。為了探究 WebAssembly 在當今能夠實現什麼,我們決定採用一個用 C/C++ 編寫的 Python 直譯器,將其編譯為 WebAssembly,並在 VS Code 網頁版中執行。幸運的是,Python 團隊已經開始著手將 CPython 編譯為 WASM,我們很高興能夠利用他們的成果。探索的結果可以在下面的短影片中看到。
它看起來與在 VS Code 桌面版中執行 Python 程式碼並沒有什麼不同。那麼,為什麼這很酷呢?
- Python 原始碼(
app.py
和hello.py
)託管在一個 GitHub 倉庫中,並直接從 GitHub 讀取。Python 直譯器對工作區中的檔案擁有完全訪問許可權,但對其他檔案則沒有。 - 示例程式碼是多檔案專案。
app.py
依賴於hello.py
。 - 輸出結果很好地顯示在 VS Code 的終端中。
- 你可以執行一個 Python REPL 並與其完全互動。
- 當然,它執行在 Web 上。
此外,編譯為 WebAssembly (WASM) 程式碼的 Python 直譯器無需修改即可在 VS Code 網頁版中執行。這些程式碼與 CPython 團隊建立的程式碼是完全相同的。
它是如何工作的?
WebAssembly 虛擬機器不附帶 SDK(例如,Java 或 .NET)。因此,開箱即用的 WebAssembly 程式碼無法列印到控制檯或讀取檔案內容。WebAssembly 規範定義了 WebAssembly 程式碼如何呼叫執行虛擬機器的宿主中的函式。在 VS Code 網頁版中,宿主是瀏覽器。因此,虛擬機器可以呼叫在瀏覽器中執行的 JavaScript 函式。
Python 團隊提供了兩種 WebAssembly 版本的直譯器二進位制檔案:一種使用 emscripten 編譯,另一種使用 WASI SDK 編譯。儘管它們都生成 WebAssembly 程式碼,但它們在作為宿主實現提供的 JavaScript 函式方面具有不同的特性。
- emscripten - 特別關注 Web 平臺和 Node.js。除了生成 WASM 程式碼外,它還生成 JavaScript 程式碼,作為宿主在瀏覽器或 Node.js 環境中執行 WASM 程式碼。例如,JavaScript 程式碼提供了一個函式,用於將 C
printf
語句的內容列印到瀏覽器控制檯。 - WASI SDK - 將 C/C++ 程式碼編譯為 WASM,並假定宿主實現符合 WASI 規範。WASI 代表 WebAssembly System Interface。它定義了多項類似作業系統的功能,包括檔案和檔案系統、套接字、時鐘和隨機數。使用 WASI SDK 編譯 C/C++ 程式碼只會生成 WebAssembly 程式碼,而不會生成任何 JavaScript 函式。將 C
printf
語句的內容打印出來所需的 JavaScript 函式必須由宿主提供。Wasmtime 就是一個執行時,它提供了一個 WASI 宿主實現,將 WASI 連線到作業系統呼叫。
對於 VS Code,我們決定支援 WASI。儘管我們的主要重點是在瀏覽器中執行 WASM 程式碼,但我們實際上並不是在純瀏覽器環境中執行它。我們必須在 VS Code 的擴充套件宿主工作程式中執行 WebAssembly,因為這是 VS Code 擴充套件的標準方式。除了瀏覽器的 Worker API,擴充套件宿主工作程式還提供了完整的 VS Code 擴充套件 API。因此,我們實際上希望將 C/C++ 程式中的 printf
呼叫連線到 VS Code 的 終端 API,而不是連線到瀏覽器控制檯。在 WASI 中這樣做比在 emscripten 中更容易。
我們當前對 VS Code 的 WASI 宿主實現基於 WASI 快照 preview1,本博文中所述的所有實現細節均指該版本。
如何執行我自己的 WebAssembly 程式碼?
在 Python 能夠在 VS Code 網頁版中執行之後,我們很快意識到我們所採用的方法允許我們執行任何可以編譯為 WASI 的程式碼。因此,本節將演示如何使用 WASI SDK 將一個小 C 程式編譯為 WASI,並在 VS Code 的擴充套件宿主中執行它。本示例假設讀者熟悉 VS Code 的擴充套件 API,並瞭解如何為 VS Code 網頁版編寫擴充套件。
我們執行的 C 程式是一個簡單的“Hello World”程式,看起來像這樣
#include <stdio.h>
int main(void)
{
printf("Hello, World\n");
return 0;
}
假設您已安裝最新的 WASI SDK 並且它在您的 PATH
中,則可以使用以下命令編譯 C 程式
clang hello.c -o ./hello.wasm
這會在 hello.c
檔案旁邊生成一個 hello.wasm
檔案。
VS Code 透過擴充套件新增新功能,我們在將 WebAssembly 整合到 VS Code 時也遵循相同的模型。我們需要定義一個擴充套件來載入並執行 WASM 程式碼。擴充套件的 package.json
清單中的重要部分如下
{
"name": "...",
...,
"extensionDependencies": [
"ms-vscode.wasm-wasi-core"
],
"contributes": {
"commands": [
{
"command": "wasm-c-example.run",
"category": "WASM Example",
"title": "Run C Hello World"
}
]
},
"devDependencies": {
"@types/vscode": "1.77.0",
},
"dependencies": {
"@vscode/wasm-wasi": "0.11.0-next.0"
}
}
ms-vscode.wasm-wasi-core 擴充套件提供了 WebAssembly 執行引擎,它將 WASI API 連線到 VS Code API。node 模組 @vscode/wasm-wasi
提供了一個用於在 VS Code 中載入和執行 WebAssembly 程式碼的門面。
以下是載入和執行 WebAssembly 程式碼的實際 TypeScript 程式碼
import { Wasm } from '@vscode/wasm-wasi';
import { commands, ExtensionContext, Uri, window, workspace } from 'vscode';
export async function activate(context: ExtensionContext) {
// Load the WASM API
const wasm: Wasm = await Wasm.load();
// Register a command that runs the C example
commands.registerCommand('wasm-wasi-c-example.run', async () => {
// Create a pseudoterminal to provide stdio to the WASM process.
const pty = wasm.createPseudoterminal();
const terminal = window.createTerminal({
name: 'Run C Example',
pty,
isTransient: true
});
terminal.show(true);
try {
// Load the WASM module. It is stored alongside the extension's JS code.
// So we can use VS Code's file system API to load it. Makes it
// independent of whether the code runs in the desktop or the web.
const bits = await workspace.fs.readFile(
Uri.joinPath(context.extensionUri, 'hello.wasm')
);
const module = await WebAssembly.compile(bits);
// Create a WASM process.
const process = await wasm.createProcess('hello', module, { stdio: pty.stdio });
// Run the process and wait for its result.
const result = await process.run();
if (result !== 0) {
await window.showErrorMessage(`Process hello ended with error: ${result}`);
}
} catch (error) {
// Show an error message if something goes wrong.
await window.showErrorMessage(error.message);
}
});
}
下面的影片展示了擴充套件在 VS Code 網頁版中執行。
我們使用 C/C++ 程式碼作為 WebAssembly 的源,並且由於 WASI 是一個標準,因此還有其他支援 WASI 的工具鏈。例如:Rust、.NET 或 Swift。
VS Code 的 WASI 實現
WASI 和 VS Code API 共享檔案系統或 stdio(例如,終端)等概念。這使我們能夠在 VS Code API 的基礎上實現 WASI 規範。然而,不同的執行行為是一個挑戰:WebAssembly 程式碼執行是同步的(例如,一旦 WebAssembly 執行開始,JavaScript worker 將被阻塞直到執行完成),而 VS Code 和瀏覽器的大多數 API 都是非同步的。例如,在 WASI 中從檔案讀取是同步的,而相應的 VS Code API 是非同步的。這個特性給 WebAssembly 程式碼在 VS Code 擴充套件宿主 worker 中執行帶來了兩個問題
- 我們需要防止擴充套件宿主在執行 WebAssembly 程式碼時被阻塞,因為這會阻止其他擴充套件的執行。
- 需要一種機制來在非同步 VS Code 和瀏覽器 API 的基礎上實現同步 WASI API。
第一種情況很容易解決:我們在單獨的工作執行緒中執行 WebAssembly 程式碼。第二種情況更難解決,因為將同步程式碼對映到非同步程式碼需要暫停同步執行執行緒並在非同步計算結果可用時恢復它。WebAssembly 的 JavaScript-Promise 整合提案在 WASM 層解決了這個問題,並且在 V8 中有一個實驗性的實現。然而,當我們開始這項工作時,V8 的實現尚未可用。因此,我們選擇了不同的實現,它使用 SharedArrayBuffer 和 Atomics 將同步 WASI API 對映到 VS Code 的非同步 API。
這種方法的工作原理如下
- WASM worker 執行緒建立一個
SharedArrayBuffer
,其中包含關於需要在 VS Code 端呼叫的程式碼的必要資訊。 - 它將共享記憶體釋出到 VS Code 的擴充套件宿主 worker,然後使用 Atomics.wait 等待擴充套件宿主 worker 完成其工作。
- 擴充套件主機工作器接收訊息,呼叫相應的 VS Code API,將結果寫回
SharedArrayBuffer
,然後使用 Atomics.store 和 Atomics.notify 通知 WASM 工作器執行緒喚醒。 - WASM worker 然後從
SharedArrayBuffer
中讀取任何結果資料並將其返回給 WASI 回撥。
這種方法唯一的困難是 SharedArrayBuffer
和 Atomics
要求站點是 跨域隔離 的,由於 CORS 具有很強的傳染性,這本身可能就是一項艱鉅的任務。這就是為什麼它目前只在 insiders.vscode.dev 的 Insiders 版本中預設啟用,並且必須在 vscode.dev 上使用查詢引數 ?vscode-coi=on
啟用。
下面的圖表更詳細地展示了 WASM worker 和擴充套件宿主 worker 之間對於我們編譯到 WebAssembly 的上述 C 程式的互動。橙色框中的程式碼是 WebAssembly 程式碼,所有綠色框中的程式碼都在 JavaScript 中執行。黃色框表示 SharedArrayBuffer
。
一個網路 shell
既然我們已經能夠將 C/C++ 和 Rust 程式碼編譯成 WebAssembly 並在 VS Code 中執行,我們便探索是否也能在 VS Code 網頁版中執行一個 shell。
我們研究了將一個 Unix shell 編譯到 WebAssembly。然而,一些 shell 依賴於作業系統功能(生成程序等),而 WASI 目前無法提供這些功能。這導致我們採取了略微不同的方法:我們用 TypeScript 實現了一個基本的 shell,並嘗試僅將 Unix 核心工具(如 ls
、cat
、date
等)編譯到 WebAssembly。由於 Rust 對 WASM 和 WASI 有很好的支援,我們嘗試了 uutils/coreutils,這是一個用 Rust 對 GNU coreutils 的跨平臺重新實現。結果,我們有了一個最初的最小網路 shell。
如果不能執行自定義 WebAssembly 或命令,shell 的功能會非常有限。為了擴充套件網路 shell,其他擴充套件可以為檔案系統貢獻額外的掛載點,以及在網路 shell 中輸入時呼叫的命令。透過命令進行的間接呼叫將具體的 WebAssembly 執行與在終端中輸入的內容解耦。從一開始就在 Python 擴充套件中使用這種支援,您可以透過在提示符中輸入 python app.py
或列出通常掛載在 /usr/local/lib/python3.11
下的預設 python 3.11 庫,直接從 shell 中執行 Python 程式碼。
下一步是什麼?
WASM 執行引擎擴充套件和 Web Shell 擴充套件都處於實驗性預覽階段,不應用於實現生產就緒的 WebAssembly 擴充套件。它們已公開發布,以獲取對該技術的早期反饋。如果您有任何問題或反饋,請在相應的 vscode-wasm GitHub 儲存庫中提出問題。此儲存庫還包含 Python 示例、WASM 執行引擎 和 Web Shell 的原始碼。
我們知道我們將進一步探索以下主題
- WASI 團隊正在開發規範的 preview2 和 preview3,我們也計劃支援這些版本。新版本將改變 WASI 宿主的實現方式。然而,我們相信我們可以保持我們在 WASM 執行引擎擴充套件中公開的 API 基本上穩定。
- 此外,還有 WASIX 工作,它透過額外的 類作業系統功能(如程序或 futex)擴充套件了 WASI。我們將繼續關注這項工作。
- VS Code 的許多語言伺服器都是用 JavaScript 或 TypeScript 以外的語言實現的。我們計劃探索將這些語言伺服器編譯為
wasm32-wasi
並在 VS Code 網頁版中執行的可能性。 - 改進 Web 上的 Python 除錯。我們已經開始著手這項工作,敬請期待。
- 新增支援,以便擴充套件 B 可以執行由擴充套件 A 貢獻的 WebAssembly 程式碼。例如,這將允許任意擴充套件透過重用貢獻 Python WebAssembly 的擴充套件來執行 Python 程式碼。
- 確保為
wasm32-wasi
編譯的其他語言執行時能夠在 VS Code 的 WebAssembly 執行引擎之上執行。VMware Labs 提供了 Ruby 和 PHP 的wasm32-wasi
二進位制檔案,兩者都可以在 VS Code 中執行。
謝謝,
Dirk 和 VS Code 團隊
編碼愉快!