在 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 系統介面。它定義數個類似作業系統的功能,包括檔案和檔案系統、通訊端、時鐘和隨機數字。使用 WASI SDK 編譯 C/C++ 程式碼只會產生 WebAssembly 程式碼,而不會產生任何 JavaScript 函式。列印 C
printf
陳述式內容所需的 JavaScript 函式必須由主機提供。Wasmtime 例如,是一個執行階段,提供 WASI 主機實作,可將 WASI 連接到作業系統呼叫。
對於 VS Code,我們決定支援 WASI。雖然我們的主要重點是在瀏覽器中執行 WASM 程式碼,但我們實際上並未在純瀏覽器環境中執行它。我們必須在 VS Code 的擴充功能主機背景工作角色中執行 WebAssembly,因為這是擴充 VS Code 的標準方式。除了瀏覽器的背景工作角色 API 之外,擴充功能主機背景工作角色還提供完整的 VS Code 擴充功能 API。因此,我們實際上想要將 C/C++ 程式中的 printf
呼叫連接到 VS Code 的 Terminal 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 擴充功能提供 WebAssembly 執行引擎,可將 WASI API 連接到 VS Code API。node 模組 @vscode/wasm-wasi
提供一個外觀模式,可在 VS Code 中載入並執行 WebAssembly 程式碼。
以下是在 TypeScript 中載入並執行 WebAssembly 程式碼的實際程式碼
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 背景工作角色就會被封鎖,直到執行完成為止),而 VS Code 和瀏覽器的大多數 API 都是非同步的。例如,在 WASI 中從檔案讀取是同步的,而對應的 VS Code API 則是非同步的。此特性會對在 VS Code 擴充功能主機背景工作角色內執行 WebAssembly 程式碼造成兩個問題
- 我們需要防止擴充功能主機在執行 WebAssembly 程式碼時遭到封鎖,因為這會封鎖其他擴充功能的執行。
- 需要一種機制,以在非同步 VS Code 和瀏覽器 API 之上實作同步 WASI API。
第一個案例很容易解決:我們在個別的背景工作執行緒中執行 WebAssembly 程式碼。第二個案例比較難以解決,因為將同步程式碼對應到非同步程式碼需要暫停同步執行執行緒,並在非同步計算結果可用時繼續執行緒。WebAssembly 的 JavaScript-Promise 整合提案 在 WASM 層級解決了這個問題,並且在 V8 中有一個提案的實驗性實作。但是,當我們開始這項工作時,V8 實作尚未可用。因此,我們選擇了不同的實作,它使用 SharedArrayBuffer 和 Atomics,將同步 WASI API 對應到 VS Code 的非同步 API。
方法如下
- WASM 背景工作執行緒會建立一個
SharedArrayBuffer
,其中包含應在 VS Code 端呼叫之程式碼的必要資訊。 - 它會將共用記憶體張貼到 VS Code 的擴充功能主機背景工作角色,然後等待擴充功能主機背景工作角色使用 Atomics.wait 完成其工作。
- 擴充功能主機背景工作角色會接收訊息、呼叫適當的 VS Code API、將結果寫回
SharedArrayBuffer
,然後使用 Atomics.store 和 Atomics.notify 通知 WASM 背景工作執行緒喚醒。 - 然後,WASM 背景工作角色會從
SharedArrayBuffer
讀取任何結果資料,並將其傳回至 WASI 回呼。
此方法的唯一困難之處在於,SharedArrayBuffer
和 Atomics
需要網站是 跨來源隔離,由於 CORS 非常容易蔓延,因此本身可能是一項努力。這就是為什麼它目前預設僅在 Insiders 版本 insiders.vscode.dev 上啟用,並且必須在 vscode.dev 上使用查詢參數 ?vscode-coi=on
啟用。
下方圖表更詳細地顯示 WASM 背景工作角色與擴充功能主機背景工作角色之間的互動,適用於我們編譯為 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 核心公用程式的跨平台版本。瞧!我們有了第一個最簡化的 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 團隊正在開發規格的預覽 2 和預覽 3,我們也計劃支援這些版本。新版本將變更 WASI 主機的實作方式。但是,我們確信我們可以保持我們的 API (在 WASM 執行引擎擴充功能中公開) 大致穩定。
- 還有 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 和 PHPwasm32-wasi
二進位檔,並且兩者都在 VS Code 中執行。
感謝您,
Dirk 和 VS Code 團隊
Coding 愉快!