Web 版 VS Code で WebAssembly を実行する
2023年6月5日 by 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 にコンパイルする作業を始めており、私たちは喜んでその取り組みに乗じました。この探求の結果は、以下の短いビデオで見ることができます。
デスクトップ版 VS Code で Python コードを実行するのと、見た目はあまり変わりません。では、何がすごいのでしょうか?
- Python のソースコード (
app.py
とhello.py
) は GitHub リポジトリでホストされており、GitHub から直接読み込まれます。Python インタープリターはワークスペース内のファイルに完全にアクセスできますが、それ以外のファイルにはアクセスできません。 - サンプルコードは複数のファイルで構成されています。
app.py
はhello.py
に依存しています。 - 出力は VS Code のターミナルにきれいに表示されます。
- Python の REPL を実行し、完全に対話することができます。
- そしてもちろん、これは Web 上で実行されます。
さらに、WebAssembly (WASM) コードにコンパイルされた Python インタープリターは、Web 版 VS Code で実行するために何の変更も必要ありません。バイナリは CPython チームが作成したものと全く同じです。
どのように動作するのでしょうか?
WebAssembly 仮想マシンには(例えば Java や .NET のように)SDK が付属していません。そのため、WebAssembly コードはそのままではコンソールに何かを出力したり、ファイルの内容を読み取ったりすることはできません。WebAssembly 仕様が定義しているのは、WebAssembly コードが仮想マシンを実行しているホスト内の関数をどのように呼び出すかということです。Web 版 VS Code の場合、ホストはブラウザーです。したがって、仮想マシンはブラウザーで実行される JavaScript 関数を呼び出すことができます。
Python チームは、インタープリターの WebAssembly バイナリを2つの種類で提供しています。1つは emscripten でコンパイルされたもの、もう1つは WASI SDK でコンパイルされたものです。どちらも WebAssembly コードを生成しますが、ホスト実装として提供する JavaScript 関数の特性が異なります。
- emscripten - Web プラットフォームと Node.js に特化しています。WASM コードを生成するだけでなく、ブラウザーまたは Node.js 環境で WASM コードを実行するためのホストとして機能する JavaScript コードも生成します。例えば、この 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 の拡張機能ホストワーカーで実行する必要があります。拡張機能ホストワーカーは、ブラウザーのワーカー API に加えて、VS Code 拡張機能 API 全体を提供します。そのため、C/C++ プログラムの printf
呼び出しをブラウザーのコンソールに結びつけるのではなく、実際には VS Code の Terminal API に結びつけたいのです。これを WASI で行う方が emscripten よりも簡単でした。
VS Code の WASI ホストの現在の実装は、WASI スナップショット preview1 に基づいており、このブログ記事で説明されているすべての実装詳細はそのバージョンに基づいています。
自分の WebAssembly コードを実行するにはどうすればよいですか?
Web 版 VS Code で Python を実行できた後、私たちはこのアプローチが 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 で実行されている拡張機能を示しています。
私たちは WebAssembly のソースとして C/C++ コードを使用しましたが、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 は非同期的です。この特性により、VS Code 拡張機能ホストワーカー内での WebAssembly コードの実行に2つの問題が生じます。
- WebAssembly コード実行中に拡張機能ホストがブロックされるのを防ぐ必要があります。これがブロックされると、他の拡張機能の実行もブロックされてしまいます。
- 非同期の VS Code API とブラウザー API の上に同期の WASI API を実装するメカニズムが必要です。
最初のケースは簡単に解決できます。WebAssembly コードを別のワーカースレッドで実行します。2番目のケースは解決がより困難です。同期コードを非同期コードにマッピングするには、同期実行中のスレッドを中断し、非同期で計算された結果が利用可能になったときに再開する必要があります。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
を使用して有効にする必要がある理由です。
以下の図は、WebAssembly にコンパイルした上記の C プログラムについて、WASM ワーカーと拡張機能ホストワーカー間の相互作用をより詳細に示しています。オレンジ色のボックス内のコードは WebAssembly コードで、緑色のボックス内のコードはすべて JavaScript で実行されます。黄色のボックスは SharedArrayBuffer
を表します。
Web シェル
C/C++ と Rust のコードを WebAssembly にコンパイルし、VS Code で実行できるようになったので、Web 版 VS Code でシェルを実行できるかどうかも探求しました。
Unix シェルのいずれかを WebAssembly にコンパイルすることを調査しました。しかし、一部のシェルは(プロセスの生成など)オペレーティングシステムの機能に依存しており、現時点では WASI で利用できません。そのため、私たちは少し異なるアプローチを取りました。TypeScript で基本的なシェルを実装し、ls
、cat
、date
などの Unix のコアユーティリティのみを WebAssembly にコンパイルしようとしました。Rust は WASM と WASI を非常によくサポートしているため、GNU コアユーティリティのクロスプラットフォーム再実装である uutils/coreutils を試してみました。そして、Et voilà (フランス語で「ほら、この通り」)、最初の最小限の Web シェルが完成しました。
カスタムの WebAssembly やコマンドを実行できない場合、シェルの機能は非常に限られます。Web シェルを拡張するために、他の拡張機能がファイルシステムに追加のマウントポイントや、Web シェルに入力されたときに呼び出されるコマンドを提供できます。コマンドを介した間接化により、具体的な WebAssembly の実行とターミナルに入力される内容が切り離されます。最初から Python 拡張機能でこのサポートを使用することで、プロンプトに python app.py
と入力して Python コードをシェルから直接実行したり、通常 /usr/local/lib/python3.11
にマウントされているデフォルトの Python 3.11 ライブラリを一覧表示したりできます。
次は何が来るのか?
WASM 実行エンジン拡張機能と Web シェル拡張機能は、どちらもプレビューとして実験的なものであり、WebAssembly を使用した製品レベルの拡張機能の実装には使用しないでください。これらは、技術に関する早期のフィードバックを得るために公開されています。ご質問やフィードバックがある場合は、対応する vscode-wasm GitHub リポジトリで issue を作成してください。このリポジトリには、Python の例のソースコード、WASM 実行エンジン、Web シェルのソースコードも含まれています。
私たちが分かっていることは、以下のトピックをさらに探求していくということです。
- WASI チームは仕様の preview2 と preview3 に取り組んでおり、私たちもそれらをサポートする予定です。新しいバージョンでは WASI ホストの実装方法が変わりますが、WASM 実行エンジン拡張機能で公開されている API は、ほとんど安定した状態を保てると確信しています。
- また、WASI をプロセスや futex などの追加のオペレーティングシステムのような機能で拡張する WASIX の取り組みもあります。私たちはこの作業を引き続き注視していきます。
- 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チーム
ハッピーコーディング!