环球焦点!在VS Code for the Web中运行WebAssembly
翻译自 Dirk andthe VS Code team 的博客
Web 版 VS 代码 已经推出一段时间了,支持浏览器中的完整编辑/编译/调试周期一直是我们的目标。这对于 JavaScript 和 TypeScript 等语言来说相对容易,因为浏览器附带了 JavaScript 执行引擎。对于其他语言来说更难,因为我们必须能够执行(并因此调试)代码。例如,要在浏览器中运行 Python源代码,就需要有一个可以运行 Python 解释器的执行引擎。这些语言运行时通常用 C/C++编写。
(相关资料图)
WebAssembly 是虚拟机的二进制指令格式。WebAssembly 虚拟机今天在现代浏览器中发布,并且有工具链可以将 C/C++ 编译为 WebAssembly 代码。为了找出 WebAssemblies 的可能性,我们决定使用一个用 C/C++ 编写的 Python 解释器,将其编译为 WebAssembly,然后在 VS Code forthe Web 中运行它。幸运的是,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 for the Web 中运行。这些位是一对一的,与 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 的扩展主机工作器中运行 WebAssemblies,这是扩展 VS Code 的标准方式。除了浏览器的 worker API之外,扩展主机 worker 还提供整个 VS Code 扩展 API。因此,我们实际上不想将 C/C++ 程序中的 printf 调用连接到浏览器的控制台,而是将其连接到 VS Code 的终端 API。在 WASI 中这样做比在 emscripten 中更容易。
我们当前 VS Code 的 WASI 主机实现基于 WASI 快照预览1,本文中描述的所有实现细节均参考该版本。
如何运行我自己的 WebAssembly 代码?
在 VS Code for Web 中运行 Python 后,我们很快意识到我们采用的方法允许我们执行任何可以编译为 WASI 的代码。因此,本节演示如何使用 WASI SDK 将小型 C 程序编译为 WASI,并在 VS Code 的扩展主机中执行它。该示例假定读者熟悉 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 执行引擎。节点模块 @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) { // Loadthe 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 canuse 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 实现
WASI 和 VS Code API 共享文件系统或 stdio(例如,终端)等概念。这使我们能够在 VS CodeAPI 之上实施 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 Integration Proposal 解决了 WASM 层上的这个问题,并且在 V8 中有该提案的实验性实现。然而,当我们开始努力时,V8 实现还不可用。所以我们选择了一个不同的实现,它使用 SharedArrayBuffer 和 Atomics 将同步 WASI API 映射到 VS Code 的异步 API。
该方法的工作原理如下:
WASM 工作线程创建一个 SharedArrayBuffer,其中包含有关应在 VS Code端调用的代码的必要信息。
它将共享内存发布到 VS Code 的扩展主机工作程序,然后等待扩展主机工作程序使用 Atomics.wait 完成其工作。
扩展主机工作线程获取消息,调用适当的 VS CodeAPI,将结果写回 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现在我们能够将 C/C++ 和 Rust 代码编译为 WebAssembly 并在 VS Code 中执行它,我们探索了是否也可以在 VS Code for the Web 中运行一个 shell。
我们研究了将其中一个 Unix shell 编译为 WebAssembly。但是,某些 shell 依赖于操作系统功能(生成进程...),这些功能目前在 WASI 中不可用。这导致我们采取了一种稍微不同的方法:我们在 TypeScript 中实现了一个基本的 shell,并尝试仅将 Unix 核心实用程序(如 ls、cat、date 等)编译为 WebAssembly。由于 Rust 对 WASM 和 WASI 有很好的支持,我们尝试了 uutils/coreutils,这是 GNU coreutils 在 Rust 中的跨平台重新实现。我们有了第一个最小的 web shell。
如果您不能执行自定义 WebAssemblies 或命令,则 shell 非常有限。为了扩展 web shell,其他扩展可以为文件系统提供额外的挂载点,以及在将它们键入 web shell 时调用的命令。通过命令的间接访问将具体的 WebAssembly 执行与终端中键入的内容分离。从一开始就使用 Python 扩展中的此支持,允许您通过在提示符中输入 python app.py 或列出默认的 Python 3.11 库(通常安装在/usr/local/lib/python3.11下)直接从 shell 中执行 Python 代码。
接下来是什么?
WASM 执行引擎扩展和 Web Shell 扩展都是实验性的预览版,不应用于使用 WebAssemblies 实现生产就绪扩展。它们已公开提供,以获得对该技术的早期反馈。如果您有任何问题或反馈,请在相应的 vscode-wasm GitHub 存储库中打开问题。此存储库还包含 Python 示例以及 WASM 执行引擎和 Web Shell 的源代码。
我们将进一步探讨以下主题:
WASI 团队正在研究规范的预览版 2 和预览版 3,我们也计划支持该规范。新版本将改变 WASI 主机的实现方式。但是,我们有信心可以保持在 WASM 执行引擎扩展中公开的 API 基本稳定。
还有 WASIX 努力扩展 WASI,增加了类似操作系统的特性,例如进程或 futex。我们将继续关注这项工作。
VS Code 的许多语言服务器都是用不同于 JavaScript 或 TypeScript 的语言实现的。我们计划探索将这些语言服务器编译为 wasm32-wasi 并在 VS Code for Web 中运行它们的可能性。
改进 Web 上 Python 的调试。我们已经开始着手解决这个问题,敬请期待。
添加支持,以便扩展 B 可以运行由扩展 A 贡献的 WebAssembly 代码。例如,这将允许任意扩展通过重用贡献 Python WebAssembly 的扩展来执行 Python 代码。
确保为 wasm32-wasi 编译的其他语言运行时在 VS Code 的 WebAssembly 执行引擎之上运行。VMware Labs 提供 Ruby 和 PHP wasm32-wasi 二进制文件,两者都在 VS Code 中运行。
Happy Coding!
审核编辑:汤梓红