在適用於 Web 的 VS Code 中執行 WebAssembly
2023 年 6 月 5 日,作者:Dirk Bäumer
適用於 Web 的 VS Code (https://vscode.dev) 已經推出了一段時間,我們的目標一直是支援在瀏覽器中完成完整的編輯/編譯/除錯迴圈。對於 JavaScript 和 TypeScript 等語言來說,這相對容易,因為瀏覽器自帶 JavaScript 執行引擎。而對於其他語言則更難,因為我們必須能夠執行(並因此除錯)程式碼。例如,要在瀏覽器中執行 Python 原始碼,就需要一個能夠執行 Python 直譯器的執行引擎。這些語言執行時通常用 C/C++ 編寫。
WebAssembly 是一種虛擬機器二進位制指令格式。WebAssembly 虛擬機器已內置於現代瀏覽器中,並且有工具鏈可以將 C/C++ 編譯為 WebAssembly 程式碼。為了瞭解 WebAssembly 當前的可能性,我們決定採用一個用 C/C++ 編寫的 Python 直譯器,將其編譯為 WebAssembly,並在適用於 Web 的 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 直譯器無需修改即可在適用於 Web 的 VS Code 中執行。這些程式碼與 CPython 團隊建立的程式碼完全相同。
它是如何工作的?
WebAssembly 虛擬機器不附帶 SDK(例如 Java 或 .NET)。因此,開箱即用,WebAssembly 程式碼無法列印到控制檯或讀取檔案內容。WebAssembly 規範定義了 WebAssembly 程式碼如何呼叫執行虛擬機器的宿主中的函式。在適用於 Web 的 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 快照預覽版 1,本博文中描述的所有實現細節均指該版本。
我如何執行自己的 WebAssembly 程式碼?
在我們將 Python 執行在適用於 Web 的 VS Code 中之後,我們很快意識到我們採取的方法允許我們執行任何可以編譯為 WASI 的程式碼。因此,本節演示瞭如何使用 WASI SDK 將一個小的 C 程式編譯為 WASI,並在 VS Code 的擴充套件宿主中執行它。該示例假設讀者熟悉 VS Code 的擴充套件 API,並且知道如何為 適用於 Web 的 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 擴充套件提供了將 WASI API 連線到 VS Code API 的 WebAssembly 執行引擎。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);
}
});
}
下面的影片展示了擴充套件在適用於 Web 的 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 是非同步的。此特性導致在 VS Code 擴充套件主機 worker 中執行 WebAssembly 程式碼時出現兩個問題:
- 我們需要防止擴充套件宿主在執行 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 完成其工作。
- 擴充套件宿主 worker 接收訊息,呼叫相應的 VS Code API,將結果寫回
SharedArrayBuffer,然後使用 Atomics.store 和 Atomics.notify 通知 WASM worker 執行緒喚醒。 - 然後 WASM worker 從
SharedArrayBuffer中讀取任何結果資料並將其返回給 WASI 回撥。
這種方法唯一的困難是 SharedArrayBuffer 和 Atomics 要求站點是跨域隔離的,這本身可能是一項艱鉅的任務,因為 CORS 具有很強的傳染性。這就是為什麼它目前預設僅在 Insiders 版本 insiders.vscode.dev 上啟用,並且必須在 vscode.dev 上使用查詢引數 ?vscode-coi=on 啟用。
下圖更詳細地展示了 WASM worker 和擴充套件宿主 worker 之間針對我們編譯為 WebAssembly 的上述 C 程式的互動。橙色框中的程式碼是 WebAssembly 程式碼,所有綠色框中的程式碼都在 JavaScript 中執行。黃色框表示 SharedArrayBuffer。

一個 Web Shell
現在我們能夠將 C/C++ 和 Rust 程式碼編譯為 WebAssembly 並在 VS Code 中執行,我們探索了是否也能在適用於 Web 的 VS Code 中執行 shell。
我們研究了將一個 Unix shell 編譯為 WebAssembly。然而,一些 shell 依賴於作業系統功能(生成程序等),而這些功能目前在 WASI 中尚不可用。這導致我們採取了略微不同的方法:我們用 TypeScript 實現了一個基本的 shell,並嘗試僅將 Unix 核心工具(如 ls、cat、date 等)編譯為 WebAssembly。由於 Rust 對 WASM 和 WASI 有很好的支援,我們嘗試了 uutils/coreutils,這是一個用 Rust 重新實現的 GNU coreutils 的跨平臺版本。瞧,我們有了一個第一個最小的 web shell。

如果您無法執行自定義 WebAssembly 或命令,那麼 shell 的功能將非常有限。為了擴充套件 Web shell,其他擴充套件可以為檔案系統提供額外的掛載點以及在 Web shell 中輸入時呼叫的命令。透過命令的間接方式,將具體的 WebAssembly 執行與終端中輸入的內容解耦。從一開始就使用 Python 擴充套件中的此支援,您可以直接從 shell 中執行 Python 程式碼,方法是在提示符中輸入 python app.py,或者列出預設的 Python 3.11 庫,該庫通常掛載在 /usr/local/lib/python3.11 下。

下一步是什麼?
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並在適用於 Web 的 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 團隊
編碼愉快!