現已釋出!閱讀關於 11 月新增功能和修復的內容。

在適用於 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,我們很高興地利用了他們的成果。這項探索的成果可以在下面的短影片中看到:

Execute a Python file in VS Code for the Web

這看起來與在 VS Code 桌面版中執行 Python 程式碼並沒有什麼不同。那麼,這為什麼很酷呢?

  • Python 原始碼(app.pyhello.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 中執行。

Run Hello World

我們使用 C/C++ 程式碼作為 WebAssembly 的源,並且由於 WASI 是一個標準,因此還有其他支援 WASI 的工具鏈。例如:Rust.NETSwift

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 實現尚未推出。因此,我們選擇了一種不同的實現,它使用 SharedArrayBufferAtomics 將同步 WASI API 對映到 VS Code 的非同步 API。

該方法的工作原理如下:

  • WASM worker 執行緒建立一個 SharedArrayBuffer,其中包含有關應在 VS Code 側呼叫的程式碼的必要資訊。
  • 它將共享記憶體釋出到 VS Code 的擴充套件宿主 worker,然後使用 Atomics.wait 等待擴充套件宿主 worker 完成其工作。
  • 擴充套件宿主 worker 接收訊息,呼叫相應的 VS Code API,將結果寫回 SharedArrayBuffer,然後使用 Atomics.storeAtomics.notify 通知 WASM worker 執行緒喚醒。
  • 然後 WASM worker 從 SharedArrayBuffer 中讀取任何結果資料並將其返回給 WASI 回撥。

這種方法唯一的困難是 SharedArrayBufferAtomics 要求站點是跨域隔離的,這本身可能是一項艱鉅的任務,因為 CORS 具有很強的傳染性。這就是為什麼它目前預設僅在 Insiders 版本 insiders.vscode.dev 上啟用,並且必須在 vscode.dev 上使用查詢引數 ?vscode-coi=on 啟用。

下圖更詳細地展示了 WASM worker 和擴充套件宿主 worker 之間針對我們編譯為 WebAssembly 的上述 C 程式的互動。橙色框中的程式碼是 WebAssembly 程式碼,所有綠色框中的程式碼都在 JavaScript 中執行。黃色框表示 SharedArrayBuffer

Interaction between the WASM worker and the extension host

一個 Web Shell

現在我們能夠將 C/C++ 和 Rust 程式碼編譯為 WebAssembly 並在 VS Code 中執行,我們探索了是否也能在適用於 Web 的 VS Code 中執行 shell。

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

A web shell

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

Python integration into web shell

下一步是什麼?

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 團隊

編碼愉快!

© . This site is unofficial and not affiliated with Microsoft.