在 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,我们很高兴地搭乘他们的顺风车。探索结果可以在下面的简短视频中看到
它看起来与在 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 代码如何调用运行虚拟机的宿主的函数。在 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 中运行。
我们使用 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 是异步的。这种特征会导致 WebAssembly 代码在 VS Code 扩展宿主工作进程中执行的两个问题
- 我们需要防止扩展宿主在执行 WebAssembly 代码时被阻塞,因为这会阻止其他扩展执行。
- 需要一种机制来在异步的 VS Code 和浏览器 API 之上实现同步的 WASI API。
第一种情况很容易解决:我们在单独的工作进程线程中运行 WebAssembly 代码。第二种情况比较难解决,因为将同步代码映射到异步代码需要挂起同步执行的线程,并在异步计算的结果可用时恢复它。 用于 WebAssembly 的 JavaScript-Promise 集成提案 在 WASM 层解决了这个问题,并且 V8 中有一个该提案的实验性实现。但是,当我们开始这项工作时,V8 实现还没有可用。因此,我们选择了另一种实现,它使用 SharedArrayBuffer 和 Atomics 将同步的 WASI API 映射到 VS Code 的异步 API。
该方法的工作原理如下
- WASM 工作进程线程使用有关应在 VS Code 侧调用的代码的必要信息创建一个
SharedArrayBuffer
。 - 它将共享内存发布到 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
。
网络外壳
现在我们已经能够将 C/C++ 和 Rust 代码编译为 WebAssembly 并在 VS Code 中执行它,我们探索了是否也可以在 Web 版本的 VS Code 中运行 shell。
我们调查了将其中一个 Unix shell 编译为 WebAssembly。但是,一些 shell 依赖于目前 WASI 中不可用的操作系统功能(生成进程等)。这导致我们采用了一种略有不同的方法:我们在 TypeScript 中实现了一个基本 shell,并尝试仅将 Unix 核心实用程序(如 ls
、cat
、date
等)编译为 WebAssembly。由于 Rust 对 WASM 和 WASI 具有非常好的支持,我们尝试了 uutils/coreutils,这是 GNU coreutils 在 Rust 中的跨平台重新实现。瞧,我们拥有了第一个最小的 web shell。
如果您不能执行自定义 WebAssembly 或命令,那么 shell 的功能将非常有限。为了扩展 web shell,其他扩展可以为文件系统贡献额外的挂载点,以及在终端中输入时调用的命令。通过命令的间接访问将具体的 WebAssembly 执行与在终端中输入的内容解耦。从一开始就使用 Python 扩展中的这种支持,您可以通过在提示符中输入 python app.py
或列出通常挂载在 /usr/local/lib/python3.11
下的默认 python 3.11 库,直接从 shell 中执行 Python 代码。
接下来是什么?
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 和 PHPwasm32-wasi
二进制文件,两者都可以在 VS Code 中运行。
感谢!
Dirk 和 VS Code 团队
祝您编码愉快!