VS Codeのエージェントモードを拡張するには、を試してください!

Web版 VS CodeでWebAssemblyを実行する

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にコンパイルする作業を開始しており、私たちは喜んで彼らの取り組みに乗っかることができました。この探求の結果は、以下の短いビデオでご覧いただけます。

Execute a Python file in VS Code for the Web

VS CodeデスクトップでPythonコードを実行するのと、見た目はほとんど変わりません。では、なぜこれがすごいのでしょうか?

  • Pythonソースコード(app.pyおよびhello.py)はGitHubリポジトリでホストされており、GitHubから直接読み込まれます。Pythonインタプリタはワークスペース内のファイルにはフルアクセスできますが、他のファイルにはアクセスできません。
  • サンプルコードは複数ファイルです。app.pyhello.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つのフレーバーで提供しています。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コードを実行することですが、実際には純粋なブラウザ環境で実行しているわけではありません。WebAssemblyはVS Codeの拡張機能ホストワーカーで実行する必要があります。これはVS Codeを拡張するための標準的な方法だからです。拡張機能ホストワーカーは、ブラウザのワーカーAPIに加えて、VS Code拡張機能API全体を提供します。そのため、C/C++プログラム内のprintf呼び出しをブラウザのコンソールに接続する代わりに、VS Codeのターミナル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"
    }
}

The 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で実行されている拡張機能を示しています。

Run Hello World

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コードを別のワーカー(worker)スレッドで実行します。2番目のケースは解決がより困難です。同期コードを非同期コードにマッピングするには、同期実行スレッドを一時停止し、非同期で計算された結果が利用可能になったときにそれを再開する必要があります。WebAssemblyのJavaScript-Promise統合提案はWASMレイヤーでこの問題を解決し、V8にはその提案の実験的な実装があります。しかし、私たちがこの取り組みを開始した時点では、V8の実装はまだ利用できませんでした。そこで、私たちは異なる実装を選択しました。それはSharedArrayBufferAtomicsを使用して、同期WASI APIをVS Codeの非同期APIにマッピングするものです。

このアプローチは以下の通りです。

  • WASMワーカー(worker)スレッドは、VS Code側で呼び出す必要のあるコードに関する情報を含むSharedArrayBufferを作成します。
  • 共有メモリをVS Codeの拡張機能ホストワーカーに投稿し、その後Atomics.waitを使用して拡張機能ホストワーカーが作業を終えるのを待ちます。
  • 拡張機能ホストワーカーはメッセージを受け取り、適切なVS Code APIを呼び出し、結果をSharedArrayBufferに書き戻し、その後Atomics.storeAtomics.notifyを使用してWASMワーカー(worker)スレッドにウェイクアップを通知します。
  • その後、WASMワーカー(worker)はSharedArrayBufferから結果データを読み取り、WASIコールバックに返します。

このアプローチの唯一の難点は、SharedArrayBufferAtomicsがサイトをクロスオリジン分離する必要があることです。これは、CORSが非常に広範囲に影響するため、それ自体が大変な作業となる可能性があります。このため、現在ではInsiders版のinsiders.vscode.devでのみデフォルトで有効になっており、vscode.devではクエリパラメータ?vscode-coi=onを使用して有効にする必要があります。

以下は、WebAssemblyにコンパイルした上記のCプログラムにおけるWASMワーカーと拡張機能ホストワーカー間の相互作用をより詳細に示す図です。オレンジ色のボックス内のコードはWebAssemblyコードであり、緑色のボックス内のすべてのコードはJavaScriptで実行されます。黄色のボックスはSharedArrayBufferを表します。

Interaction between the WASM worker and the extension host

Webシェル

C/C++およびRustコードをWebAssemblyにコンパイルし、VS Codeで実行できるようになったので、Web版 VS Codeでもシェルを実行できるかどうかを検討しました。

Unixシェルの一つをWebAssemblyにコンパイルすることを検討しました。しかし、一部のシェルは(プロセス生成などの)オペレーティングシステム機能に依存しており、これらは現在のWASIでは利用できません。このため、私たちは少し異なるアプローチを取りました。TypeScriptで基本的なシェルを実装し、lscatdateなどのUnixコアユーティリティのみをWebAssemblyにコンパイルすることを試みました。RustはWASMとWASIのサポートが非常に優れているため、RustでGNU coreutilsをクロスプラットフォームで再実装したuutils/coreutilsを試したところ、最初の最小限のWebシェルが完成しました。

A web shell

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

Python integration into web shell

今後の展開は?

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のデバッグを改善します。これについては作業を開始しましたので、ご期待ください。
  • 拡張機能Aが提供するWebAssemblyコードを拡張機能Bが実行できるようにするサポートを追加します。これにより、例えば、Python WebAssemblyを提供した拡張機能を再利用することで、任意の拡張機能がPythonコードを実行できるようになります。
  • wasm32-wasi用にコンパイルされた他の言語ランタイムが、VS CodeのWebAssembly実行エンジン上で動作することを確認します。VMware LabsはRubyとPHPのwasm32-wasiバイナリを提供しており、両方ともVS Codeで動作します。

よろしくお願いいたします。

DirkとVS Codeチーム

ハッピーコーディング!