在網頁版 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 並與之完全互動。
- 當然,它可以在網頁上執行。
此外,編譯為 WebAssembly (WASM) 程式碼的 Python 直譯器無需修改即可在網頁版 VS Code 中執行。位元與 CPython 團隊建立的完全相同。
它是如何運作的?
WebAssembly 虛擬機器不附帶 SDK(例如,Java 或 .NET)。因此,WebAssembly 程式碼開箱即用時無法列印到主控台或讀取檔案內容。WebAssembly 規範定義了 WebAssembly 程式碼如何調用執行虛擬機器的主機中的函數。在網頁版 VS Code 的情況下,主機是瀏覽器。因此,虛擬機器可以調用在瀏覽器中執行的 JavaScript 函數。
Python 團隊以兩種形式提供其直譯器的 WebAssembly 二進制檔案:一種使用 emscripten 編譯,另一種使用 WASI SDK 編譯。儘管它們都建立 WebAssembly 程式碼,但它們在作為主機實作提供的 JavaScript 函數方面具有不同的特性
- emscripten - 特別關注網頁平台和 Node.js。除了生成 WASM 程式碼外,它還生成 JavaScript 程式碼,作為主機在瀏覽器或 Node.js 環境中執行 WASM 程式碼。例如,JavaScript 程式碼提供了一個函數,用於將 C
printf
語句的內容列印到瀏覽器的主控台。 - WASI SDK - 將 C/C++ 程式碼編譯為 WASM,並假設主機實作符合 WASI 規範。WASI 代表 WebAssembly 系統介面。它定義了幾個類似作業系統的功能,包括檔案和檔案系統、sockets、時鐘和隨機數。使用 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 在網頁版 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.js 模組 @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 工作執行緒就會被阻止,直到執行完成),而 VS Code 和瀏覽器的大部分 API 都是異步的。例如,在 WASI 中從檔案讀取是同步的,而相應的 VS Code API 是異步的。此特性為在 VS Code 擴充功能主機工作執行緒中執行 WebAssembly 程式碼帶來了兩個問題
- 我們需要防止擴充功能主機在執行 WebAssembly 程式碼時被阻止,因為這會阻止其他擴充功能被執行。
- 需要一種機制來在異步 VS Code 和瀏覽器 API 之上實作同步 WASI API。
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
。
網頁 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,它是 GNU coreutils 在 Rust 中的跨平台重新實作。瞧!我們有了第一個最小的網頁 shell。
如果您無法執行自訂 WebAssembly 或命令,則 shell 非常有限。為了擴充網頁 shell,其他擴充功能可以向檔案系統貢獻額外的掛載點,以及在鍵入網頁 shell 時調用的命令。通過命令的間接性將具體的 WebAssembly 執行與在終端機中鍵入的內容分離開來。從一開始就在 Python 擴充功能中使用此支援,您可以通過在提示符中輸入 python app.py
或列出通常掛載在 /usr/local/lib/python3.11
下的預設 python 3.11 庫,直接從 shell 中執行 Python 程式碼。
接下來會是什麼?
WASM 執行引擎擴充功能和網頁 Shell 擴充功能都處於實驗性預覽階段,不應用於使用 WebAssembly 實作生產就緒的擴充功能。它們已公開發布,以獲取有關該技術的早期回饋。如果您有任何問題或回饋,請在相應的 vscode-wasm GitHub 儲存庫中開啟 issue。這個儲存庫還包含 Python 範例 的原始碼,以及 WASM 執行引擎 和 網頁 Shell 的原始碼。
我們知道的是,我們將進一步探索以下主題
- WASI 團隊正在開發規範的 preview2 和 preview3,我們也計劃支援它們。新版本將改變 WASI 主機的實作方式。然而,我們有信心可以保持我們的 API(在 WASM 執行引擎擴充功能中公開)基本穩定。
- 還有 WASIX 工作,它使用額外的 類似作業系統的功能(例如進程或 futex)擴充了 WASI。我們將繼續關注這項工作。
- 許多 VS Code 的語言伺服器都是以 JavaScript 或 TypeScript 以外的語言實作的。我們計劃探索將這些語言伺服器編譯為
wasm32-wasi
並在網頁版 VS Code 中運行的可能性。 - 改善網頁版 Python 的偵錯功能。我們已經開始著手這項工作,敬請期待。
- 添加支援,以便擴充功能 B 可以運行擴充功能 A 貢獻的 WebAssembly 程式碼。例如,這將允許任意擴充功能通過重用貢獻 Python WebAssembly 的擴充功能來執行 Python 程式碼。
- 確保為
wasm32-wasi
編譯的其他語言執行階段在 VS Code 的 WebAssembly 執行引擎之上運行。VMware Labs 提供了 Ruby 和 PHPwasm32-wasi
二進制檔案,並且兩者都在 VS Code 中運行。
感謝,
Dirk 和 VS Code 團隊
Coding 愉快!