WebAssemblyをWeb用VS Codeで実行
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にコンパイルする作業を開始しており、我々はその取り組みに便乗しました。この探索の結果は、以下の短いビデオで見ることができます。

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仮想マシンにはSDK(例えば、Javaや.NETなど)が付属していません。そのため、初期状態ではWebAssemblyコードはコンソールに出力したり、ファイルの内容を読み取ったりすることはできません。WebAssembly仕様で定義されているのは、WebAssemblyコードが仮想マシンを実行しているホスト内の関数を呼び出す方法です。Web用VS Codeの場合、ホストはブラウザです。したがって、仮想マシンはブラウザで実行されるJavaScript関数を呼び出すことができます。
Pythonチームは、インタープリタのWebAssemblyバイナリを2つのフレーバーで提供しています。emscriptenでコンパイルされたものと、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コードを実行することですが、実際には純粋なブラウザ環境で実行しているわけではありません。WebAssemblyはVS Codeの拡張ホストワーカーで実行する必要があります。これはVS Codeが拡張される標準的な方法であるためです。拡張ホストワーカーは、ブラウザのワーカーAPIに加えて、完全なVS Code拡張APIを提供します。そのため、C/C++プログラムのprintf呼び出しをブラウザのコンソールに接続する代わりに、VS CodeのターミナルAPIに接続したいと考えています。emscriptenよりもWASIでこれを行う方が簡単でした。
VS CodeのWASIホストの現在の実装は、WASIスナップショットプレビュー1に基づいており、このブログ記事で説明されているすべての実装詳細はそのバージョンを参照しています。
自分の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の上に同期WASI APIを実装するためのメカニズムが必要です。
最初のケースは解決が簡単です。WebAssemblyコードを別のワーカー スレッドで実行します。2番目のケースは解決がより困難です。なぜなら、同期コードを非同期コードにマッピングするには、同期実行スレッドを一時停止し、非同期で計算された結果が利用可能になったときにそれを再開する必要があるからです。WebAssemblyのJavaScript-Promise統合提案はWASMレイヤーでこの問題を解決し、V8にはその提案の実験的な実装があります。しかし、私たちが取り組みを開始した時点ではV8の実装はまだ利用できませんでした。そこで、同期WASI APIをVS Codeの非同期APIにマッピングするために、SharedArrayBufferとAtomicsを使用する別の実装を選択しました。
そのアプローチは次のとおりです。
- 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を表します。

ウェブシェル
C/C++およびRustコードをWebAssemblyにコンパイルしてVS Codeで実行できるようになったので、Web用VS Codeでもシェルを実行できるかどうかを検討しました。
Unixシェルの一つをWebAssemblyにコンパイルすることを調査しました。しかし、一部のシェルはオペレーティングシステムの機能(プロセスの生成など)に依存しており、これらは現在WASIでは利用できません。このため、少し異なるアプローチをとることにしました。基本的なシェルをTypeScriptで実装し、ls、cat、dateなどのUnixコアユーティリティのみをWebAssemblyにコンパイルしようとしました。RustはWASMとWASIを非常によくサポートしているため、GNU coreutilsのクロスプラットフォーム再実装であるuutils/coreutilsを試してみました。そして、最初の最小限のウェブシェルが完成しました。

カスタムのWebAssemblyやコマンドを実行できない場合、シェルは非常に限定的です。ウェブシェルを拡張するために、他の拡張機能はファイルシステムに追加のマウントポイントや、ウェブシェルに入力されたときに呼び出されるコマンドを提供できます。コマンドを介した間接的な方法は、具体的なWebAssemblyの実行とターミナルに入力された内容を切り離します。Python拡張機能でこのサポートを最初から使用することで、プロンプトにpython app.pyと入力するか、通常/usr/local/lib/python3.11の下にマウントされているデフォルトのPython 3.11ライブラリをリストすることで、シェル内から直接Pythonコードを実行できます。

次は何が来るのか?
WASM実行エンジン拡張機能とWebシェル拡張機能はどちらもプレビューとしての実験的なものであり、WebAssemblyを使用して本番環境対応の拡張機能を実装するために使用すべきではありません。これらは、テクノロジーに関する早期のフィードバックを得るために公開されました。ご質問やフィードバックがある場合は、対応するvscode-wasm GitHubリポジトリに課題をオープンしてください。このリポジトリには、Pythonの例のソースコード、およびWASM実行エンジンとWebシェルのソースコードも含まれています。
私たちがさらに探求する予定のトピックは次のとおりです。
- WASIチームは仕様のプレビュー2とプレビュー3に取り組んでおり、私たちもそれらをサポートする予定です。新しいバージョンでは、WASIホストの実装方法が変更されます。しかし、WASM実行エンジン拡張機能で公開されている私たちのAPIは、ほとんど安定した状態を保てると確信しています。
- WASIXの取り組みも進められており、これはWASIにプロセスやfutexなどの追加のオペレーティングシステムのような機能を拡張するものです。この作業は引き続き注視していきます。
- 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チーム
ハッピーコーディング!