🚀 VS Code で を入手しましょう!

VS Code for the Web で WebAssembly を実行する

2023年6月5日 Dirk Bäumer 著

VS Code for the Web (https://vscode.dev) はしばらくの間利用可能であり、ブラウザで完全な編集/コンパイル/デバッグサイクルをサポートすることが常に私たちの目標でした。ブラウザには JavaScript 実行エンジンが搭載されているため、JavaScript や TypeScript のような言語では比較的簡単です。他の言語では、コードを実行(したがってデバッグ)できる必要があるため、より困難です。たとえば、ブラウザで Python ソースコードを実行するには、Python インタプリタを実行できる実行エンジンが必要です。これらの言語ランタイムは通常 C/C++ で記述されています。

WebAssembly は仮想マシンのためのバイナリ命令形式です。WebAssembly 仮想マシンは今日の最新のブラウザに搭載されており、C/C++ を WebAssembly コードにコンパイルするためのツールチェーンがあります。今日 WebAssembly で何が可能かを知るために、C/C++ で記述された Python インタプリタを取り上げ、それを WebAssembly にコンパイルし、VS Code for the Web で実行することにしました。幸いなことに、Python チームはすでに CPython を WASM にコンパイルする作業を開始しており、私たちは喜んで彼らの努力に便乗しました。この調査の結果は、以下の短いビデオで見ることができます。

Execute a Python file in VS Code for the Web

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

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

VS Code の WASI ホストの現在の実装は、WASI スナップショットプレビュー 1 に基づいており、このブログ記事で説明されているすべての実装詳細はそのバージョンを参照しています。

自分の WebAssembly コードを実行するには?

VS Code for the Web で Python を実行した後、私たちが採用したアプローチにより、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 に追加され、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);
    }
  });
}

以下のビデオは、VS Code for the Web で実行されている拡張機能を示しています。

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

アプローチは次のとおりです。

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

このアプローチの唯一の難点は、SharedArrayBufferAtomics がサイトを cross-origin isolated にする必要があることです。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

ウェブシェル

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

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

A web shell

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

Python integration into web shell

次は何?

WASM 実行エンジン拡張機能と Web Shell 拡張機能はどちらもプレビューとしての実験的なものであり、WebAssembly を使用して実稼働対応の拡張機能を実装するために使用すべきではありません。テクノロジーに関する早期のフィードバックを得るために公開されています。ご質問やフィードバックがある場合は、対応する vscode-wasm GitHub リポジトリで issue をオープンしてください。このリポジトリには、Python の例WASM 実行エンジンWeb Shell のソースコードも含まれています。

私たちが知っていることは、次のトピックをさらに探求することです。

  • WASI チームは、仕様の preview2 および preview3 に取り組んでおり、私たちもサポートする予定です。新しいバージョンでは、WASI ホストの実装方法が変わります。ただし、WASM 実行エンジン拡張機能で公開されている API は、ほぼ安定させることができると確信しています。
  • WASI をプロセスや futex などの追加の オペレーティングシステムのような機能 で拡張する WASIX の取り組みもあります。私たちはこの作業を注視し続けます。
  • VS Code 用の多くの言語サーバーは、JavaScript や TypeScript 以外の言語で実装されています。これらの言語サーバーを wasm32-wasi にコンパイルし、VS Code for the Web で実行する可能性を探る予定です。
  • ウェブ上の Python のデバッグの改善。私たちはこれに取り組み始めたので、ご期待ください。
  • 拡張機能 B が拡張機能 A によって提供された WebAssembly コードを実行できるようにサポートを追加します。これにより、たとえば、任意の拡張機能が Python WebAssembly を提供した拡張機能を再利用して Python コードを実行できるようになります。
  • wasm32-wasi 用にコンパイルされた他の言語ランタイムが VS Code の WebAssembly 実行エンジン上で実行されるようにします。VMware Labs は Ruby と PHP の wasm32-wasi バイナリを提供しており、どちらも VS Code で実行できます。

ありがとうございます、

Dirk と VS Code チーム

ハッピーコーディング!