现已推出!阅读 10 月份的更新内容,包括新功能和修复。

在 VS Code for the Web 中运行 WebAssemblies

2023 年 6 月 5 日 由 Dirk Bäumer 发布

VS Code for the Web (https://vscode.dev) 一直可用,我们一直致力于在浏览器中支持完整的编辑/编译/调试周期。对于 JavaScript 和 TypeScript 等语言来说,这相对容易,因为浏览器附带了 JavaScript 执行引擎。对于其他语言来说,这更难,因为我们必须能够执行(因此调试)代码。例如,要在浏览器中运行 Python 源代码,需要一个能够运行 Python 解释器的执行引擎。这些语言运行时通常是用 C/C++ 编写的。

WebAssembly 是一种用于虚拟机的二进制指令格式。WebAssembly 虚拟机如今已包含在现代浏览器中,并且存在将 C/C++ 编译成 WebAssembly 代码的工具链。为了找出 WebAssemblies 目前有哪些可能性,我们决定使用 C/C++ 编写的 Python 解释器,将其编译成 WebAssembly,并在 VS Code for the Web 中运行。幸运的是,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 并与之完全交互。
  • 当然,它在网页上运行

此外,编译成 WebAssembly (WASM) 代码的 Python 解释器无需修改即可在 VS Code for the Web 中运行。这些位与 CPython 团队创建的位是一模一样的。

它是如何工作的?

WebAssembly 虚拟机没有附带 SDK(例如,Java.NET)。因此,WebAssembly 代码开箱即用无法打印到控制台或读取文件的内容。WebAssembly 规范定义的是 WebAssembly 代码如何调用运行虚拟机的宿主的函数。在 VS Code for the Web 的情况下,宿主是浏览器。因此,虚拟机可以调用在浏览器中执行的 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 的扩展宿主工作进程中运行 WebAssemblies,因为这是扩展 VS Code 的标准方式。除了浏览器的 Worker API 之外,扩展宿主工作进程还提供了完整的 VS Code 扩展 API。因此,我们实际上希望将 C/C++ 程序中的 printf 调用连接到 VS Code 的 终端 API,而不是将其连接到浏览器的控制台。在 WASI 中这样做对我们来说比在 emscripten 中更容易。

我们当前对 VS Code 的 WASI 宿主的实现基于 WASI 快照预览版 1,本博文中描述的所有实现细节都参考该版本。

如何运行自己的 WebAssembly 代码?

在 Python 在 VS Code for the Web 中运行之后,我们很快意识到,我们所采取的方法使我们能够执行任何可以编译成 WASI 的代码。因此,本节演示了如何使用 WASI SDK 将一个小型的 C 程序编译成 WASI,并在 VS Code 的扩展宿主中执行它。本示例假设读者熟悉 VS Code 的扩展 API,并知道如何为 VS Code for the Web 编写扩展。

我们运行的 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 通过扩展添加新功能,我们在将 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 扩展提供了 WebAssembly 执行引擎,该引擎将 WASI API 连接到 VS Code API。节点模块 @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 the Web 中运行。

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 工作进程就会被阻塞,直到执行完成),而 VS Code 和浏览器的 API 大多数是异步的。例如,在 WASI 中读取文件是同步的,而相应的 VS Code API 是异步的。这种特征会导致 WebAssembly 代码在 VS Code 扩展宿主工作进程中执行的两个问题

  • 我们需要防止扩展宿主在执行 WebAssembly 代码时被阻塞,因为这会阻止其他扩展执行。
  • 需要一种机制来在异步的 VS Code 和浏览器 API 之上实现同步的 WASI API。

第一种情况很容易解决:我们在单独的工作进程线程中运行 WebAssembly 代码。第二种情况比较难解决,因为将同步代码映射到异步代码需要挂起同步执行的线程,并在异步计算的结果可用时恢复它。 用于 WebAssembly 的 JavaScript-Promise 集成提案 在 WASM 层解决了这个问题,并且 V8 中有一个该提案的实验性实现。但是,当我们开始这项工作时,V8 实现还没有可用。因此,我们选择了另一种实现,它使用 SharedArrayBufferAtomics 将同步的 WASI API 映射到 VS Code 的异步 API。

该方法的工作原理如下

  • WASM 工作进程线程使用有关应在 VS Code 侧调用的代码的必要信息创建一个 SharedArrayBuffer
  • 它将共享内存发布到 VS Code 的扩展宿主工作进程,然后使用 Atomics.wait 等待扩展宿主工作进程完成其工作。
  • 扩展主机工作进程接收消息,调用相应的 VS Code API,将结果写回 SharedArrayBuffer,然后使用 Atomics.storeAtomics.notify 通知 WASM 工作进程唤醒。
  • WASM 工作进程随后从 SharedArrayBuffer 中读取任何结果数据并将其返回给 WASI 回调函数。

这种方法的唯一困难在于 SharedArrayBufferAtomics 要求网站为 跨源隔离,由于 CORS 的病毒性,这本身可能是一项任务。这就是为什么它目前默认情况下仅在 Insiders 版本 insiders.vscode.dev 上启用,并且必须在 vscode.dev 上使用查询参数 ?vscode-coi=on 启用。

以下是显示 WASM 工作进程和扩展主机工作进程之间交互的更详细图表,用于上面编译为 WebAssembly 的 C 程序。橙色框中的代码是 WebAssembly 代码,绿色框中的所有代码都在 JavaScript 中运行。黄色框代表 SharedArrayBuffer

Interaction between the WASM worker and the extension host

网络外壳

现在我们已经能够将 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,这是 GNU coreutils 在 Rust 中的跨平台重新实现。瞧,我们拥有了第一个最小的 web shell。

A web shell

如果您不能执行自定义 WebAssembly 或命令,那么 shell 的功能将非常有限。为了扩展 web shell,其他扩展可以为文件系统贡献额外的挂载点,以及在终端中输入时调用的命令。通过命令的间接访问将具体的 WebAssembly 执行与在终端中输入的内容解耦。从一开始就使用 Python 扩展中的这种支持,您可以通过在提示符中输入 python app.py 或列出通常挂载在 /usr/local/lib/python3.11 下的默认 python 3.11 库,直接从 shell 中执行 Python 代码。

Python integration into web shell

接下来是什么?

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 和 PHP wasm32-wasi 二进制文件,两者都可以在 VS Code 中运行。

感谢!

Dirk 和 VS Code 团队

祝您编码愉快!