翻譯自 Dirk andthe VS Code team 的博客
Web 版 VS 代碼? 已經推出一段時間了,支持瀏覽器中的完整編輯/編譯/調試周期一直是我們的目標。這對于 JavaScript 和 TypeScript 等語言來說相對容易,因為瀏覽器附帶了 JavaScript 執(zhí)行引擎。對于其他語言來說更難,因為我們必須能夠執(zhí)行(并因此調試)代碼。例如,要在瀏覽器中運行 Python 源代碼,就需要有一個可以運行 Python 解釋器的執(zhí)行引擎。這些語言運行時通常用 C/C++ 編寫。
WebAssembly 是虛擬機的二進制指令格式。WebAssembly 虛擬機今天在現(xiàn)代瀏覽器中發(fā)布,并且有工具鏈可以將 C/C++ 編譯為 WebAssembly 代碼。為了找出 WebAssemblies 的可能性,我們決定使用一個用 C/C++ 編寫的 Python 解釋器,將其編譯為 WebAssembly,然后在 VS Code for the Web 中運行它。幸運的是,Python 團隊已經開始著手將 CPython 編譯為 WASM,我們很高興地利用了他們的努力。探索的結果可以在下圖中看到:
?
它看起來與在 VS Code 桌面中執(zhí)行 Python 代碼并沒有什么不同。那么,為什么這很酷呢?
Python 源代碼(app.py 和 hello.py)托管在 GitHub 存儲庫中,可直接從 GitHub 讀取。Python 解釋器可以完全訪問工作區(qū)中的文件,但不能訪問任何其他文件
示例代碼是多文件。app.py 依賴于 hello.py
輸出很好地顯示在 VS Code 的終端中
您可以運行 Python REPL 并與其完全交互
當然,它在網絡上運行
此外,編譯為 WebAssembly(WASM)代碼的 Python 解釋器無需修改即可在 VS Code for the Web 中運行。這些位是一對一的,與 CPython 團隊創(chuàng)建的相同。
? ? ? 它是如何工作的?
WebAssembly 虛擬機不附帶 SDK(例如 Java 或 .NET)。因此,開箱即用的 WebAssembly 代碼無法打印到控制臺或讀取文件的內容。WebAssembly 規(guī)范定義的是 WebAssembly 代碼如何在運行虛擬機的主機中調用函數。對于 Web 的 VS Code,主機是瀏覽器。因此,虛擬機可以調用在瀏覽器中執(zhí)行的 JavaScript 函數。
Python 團隊提供了兩種解釋器的 WebAssembly 二進制文件:一種是使用 emscripten 編譯的,另一種是使用 WASI SDK 編譯的。盡管它們都創(chuàng)建 WebAssembly 代碼,但它們在作為宿主實現(xiàn)提供的 JavaScript 函數方面具有不同的特征:
emscripten - 特別關注 Web 平臺和 Node.js。除了生成 WASM 代碼外,它還生成 JavaScript 代碼,作為宿主在瀏覽器或 Node.js 環(huán)境中執(zhí)行 WASM 代碼。例如,JavaScript 代碼提供了將 C printf 語句的內容打印到瀏覽器控制臺的功能。
WASI SDK - 將 C/C++ 代碼編譯為 WASM 并假定主機實現(xiàn)符合 WASI 規(guī)范。WASI 代表 WebAssembly 系統(tǒng)接口。它定義了幾個類似操作系統(tǒng)的特性,包括文件和文件系統(tǒng)、套接字、時鐘和隨機數。使用 WASI SDK 編譯 C/C++ 代碼只會生成 WebAssembly 代碼,不會生成任何 JavaScript 函數。主機必須提供打印 C printf 語句內容所需的 JavaScript 函數。例如,Wasmtime 是一個運行時,它提供將 WASI 連接到操作系統(tǒng)調用的 WASI 主機實現(xiàn)。
對于 VS Code,我們決定支持 WASI。雖然我們的主要重點是在瀏覽器中執(zhí)行 WASM 代碼,但我們實際上并不是在純?yōu)g覽器環(huán)境中運行它。我們必須在 VS Code 的擴展主機工作器中運行 WebAssemblies,這是擴展 VS Code 的標準方式。除了瀏覽器的 worker API 之外,擴展主機 worker 還提供整個 VS Code 擴展 API。因此,我們實際上不想將 C/C++ 程序中的 printf 調用連接到瀏覽器的控制臺,而是將其連接到 VS Code 的終端 API。在 WASI 中這樣做比在 emscripten 中更容易。
我們當前 VS Code 的 WASI 主機實現(xiàn)基于 WASI 快照預覽1,本文中描述的所有實現(xiàn)細節(jié)均參考該版本。
? ? 如何運行我自己的 WebAssembly 代碼?
在 VS Code for Web 中運行 Python 后,我們很快意識到我們采用的方法允許我們執(zhí)行任何可以編譯為 WASI 的代碼。因此,本節(jié)演示如何使用 WASI SDK 將小型 C 程序編譯為 WASI,并在 VS Code 的擴展主機中執(zhí)行它。該示例假定讀者熟悉 VS Code 的擴展 API,并且知道如何為 Web 編寫 VS Code 的擴展。
我們運行的 C 程序是一個簡單的“Hello World”程序,如下所示:
?
#include假設您安裝了最新的 WASI SDK 并且它在您的 PATH 中,可以使用以下命令編譯 C 程序:int main(void) { printf("Hello, World "); return 0; }
?
?
clang hello.c -o ./hello.wasm
?
這會在 hello.c 文件旁邊生成一個 hello.wasm 文件。
新功能通過擴展添加到 VS Code,我們在將 WebAssemblies 集成到 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 執(zhí)行引擎。節(jié)點模塊 @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 for Web 中運行的擴展。
?
我們使用 C/C++ 代碼作為 WebAssembly 的源代碼,因為 WASI 是一個標準,所以還有其他支持 WASI 的工具鏈。例如:Rust、.NET 或 Swift。
? VS Code 的 WASI 實現(xiàn)
WASI 和 VS Code API 共享文件系統(tǒng)或 stdio(例如,終端)等概念。這使我們能夠在 VS Code?API 之上實施 WASI 規(guī)范。然而,不同的執(zhí)行行為是一個挑戰(zhàn):WebAssembly 代碼執(zhí)行是同步的(例如,一旦 WebAssembly 執(zhí)行開始,JavaScript worker 就會被阻塞直到執(zhí)行完成),而 VS Code 和瀏覽器的大多數 API 都是異步的。例如,從 WASI 中的文件讀取是同步的,而相應的 VS Code API 是異步的。這個特性導致在 VS Code 擴展宿主 worker 中執(zhí)行 WebAssembly 代碼的兩個問題:
我們需要防止擴展主機在執(zhí)行 WebAssembly 代碼時被阻塞,因為這會阻止其他擴展的執(zhí)行。
需要一種機制來在異步 VS Code 和瀏覽器 API 之上實現(xiàn)同步 WASI API。
第一種情況很容易解決:我們在單獨的工作線程中運行 WebAssembly 代碼。第二種情況更難解決,因為將同步代碼映射到異步代碼需要暫停同步執(zhí)行線程,并在異步計算結果可用時恢復它。WebAssembly 的 JavaScript-Promise Integration Proposal 解決了 WASM 層上的這個問題,并且在 V8 中有該提案的實驗性實現(xiàn)。然而,當我們開始努力時,V8 實現(xiàn)還不可用。所以我們選擇了一個不同的實現(xiàn),它使用 SharedArrayBuffer 和 Atomics 將同步 WASI API 映射到 VS Code 的異步 API。
該方法的工作原理如下:
WASM 工作線程創(chuàng)建一個 SharedArrayBuffer,其中包含有關應在 VS Code?端調用的代碼的必要信息。
它將共享內存發(fā)布到 VS Code 的擴展主機工作程序,然后等待擴展主機工作程序使用 Atomics.wait 完成其工作。
擴展主機工作線程獲取消息,調用適當的 VS Code?API,將結果寫回 SharedArrayBuffer,然后使用 Atomics.store 和 Atomics.notify 通知 WASM 工作線程喚醒。
WASM worker 然后從 SharedArrayBuffer 中讀取任何結果數據并將其返回給 WASI 回調。
這種方法的唯一困難是 SharedArrayBuffer 和 Atomics 要求站點跨源隔離,因為 CORS 非常流行,這本身就是一項努力。這就是為什么它目前僅在 Insiders 版本 insiders.vscode.dev 上默認啟用,并且必須在 vscode.dev 上使用查詢參數 ?vscode-coi=on 啟用。
下圖更詳細地顯示了我們編譯為 WebAssembly 的上述 C 程序的 WASM worker 和擴展主機 worker 之間的交互。橙色框中的代碼是 WebAssembly 代碼,綠色框中的所有代碼都在 JavaScript 中運行。黃色框表示 SharedArrayBuffer。
? A Web Shell ?
現(xiàn)在我們能夠將 C/C++ 和 Rust 代碼編譯為 WebAssembly 并在 VS Code 中執(zhí)行它,我們探索了是否也可以在 VS Code for the Web 中運行一個 shell。
我們研究了將其中一個 Unix shell 編譯為 WebAssembly。但是,某些 shell 依賴于操作系統(tǒng)功能(生成進程...),這些功能目前在 WASI 中不可用。這導致我們采取了一種稍微不同的方法:我們在 TypeScript 中實現(xiàn)了一個基本的 shell,并嘗試僅將 Unix 核心實用程序(如 ls、cat、date 等)編譯為 WebAssembly。由于 Rust 對 WASM 和 WASI 有很好的支持,我們嘗試了 uutils/coreutils,這是 GNU coreutils 在 Rust 中的跨平臺重新實現(xiàn)。我們有了第一個最小的 web shell。
如果您不能執(zhí)行自定義 WebAssemblies 或命令,則 shell 非常有限。為了擴展 web shell,其他擴展可以為文件系統(tǒng)提供額外的掛載點,以及在將它們鍵入 web shell 時調用的命令。通過命令的間接訪問將具體的 WebAssembly 執(zhí)行與終端中鍵入的內容分離。從一開始就使用 Python 擴展中的此支持,允許您通過在提示符中輸入 python app.py 或列出默認的 Python 3.11 庫(通常安裝在/usr/local/lib/python3.11下)直接從 shell 中執(zhí)行 Python 代碼。
? 接下來是什么?
WASM 執(zhí)行引擎擴展和 Web Shell 擴展都是實驗性的預覽版,不應用于使用 WebAssemblies 實現(xiàn)生產就緒擴展。它們已公開提供,以獲得對該技術的早期反饋。如果您有任何問題或反饋,請在相應的 vscode-wasm GitHub 存儲庫中打開問題。此存儲庫還包含 Python 示例以及 WASM 執(zhí)行引擎和 Web Shell 的源代碼。
我們將進一步探討以下主題:
WASI 團隊正在研究規(guī)范的預覽版 2 和預覽版 3,我們也計劃支持該規(guī)范。新版本將改變 WASI 主機的實現(xiàn)方式。但是,我們有信心可以保持在 WASM 執(zhí)行引擎擴展中公開的 API 基本穩(wěn)定。
還有 WASIX 努力擴展 WASI,增加了類似操作系統(tǒng)的特性,例如進程或 futex。我們將繼續(xù)關注這項工作。
VS Code 的許多語言服務器都是用不同于 JavaScript 或 TypeScript 的語言實現(xiàn)的。我們計劃探索將這些語言服務器編譯為 wasm32-wasi 并在 VS Code for Web 中運行它們的可能性。
改進 Web 上 Python 的調試。我們已經開始著手解決這個問題,敬請期待。
添加支持,以便擴展 B 可以運行由擴展 A 貢獻的 WebAssembly 代碼。例如,這將允許任意擴展通過重用貢獻 Python WebAssembly 的擴展來執(zhí)行 Python 代碼。
確保為 wasm32-wasi 編譯的其他語言運行時在 VS Code 的 WebAssembly 執(zhí)行引擎之上運行。VMware Labs 提供 Ruby 和 PHP wasm32-wasi 二進制文件,兩者都在 VS Code 中運行。
Happy Coding!
審核編輯:湯梓紅
評論
查看更多