ist jetzt verfügbar! Lesen Sie über die neuen Funktionen und Fehlerbehebungen vom November.

WebAssemblies in VS Code for the Web ausführen

5. Juni 2023 von Dirk Bäumer

VS Code for the Web (https://vscode.dev) ist nun schon eine Weile verfügbar und es war schon immer unser Ziel, den vollständigen Edit / Compile / Debug-Zyklus im Browser zu unterstützen. Das ist für Sprachen wie JavaScript und TypeScript relativ einfach, da Browser eine JavaScript-Ausführungs-Engine mitbringen. Für andere Sprachen ist es schwieriger, da wir in der Lage sein müssen, den Code auszuführen (und daher zu debuggen). Zum Beispiel muss es für die Ausführung von Python-Quellcode in einem Browser eine Ausführungs-Engine geben, die den Python-Interpreter ausführen kann. Diese Sprachlaufzeiten sind üblicherweise in C/C++ geschrieben.

WebAssembly ist ein Binärformat für eine virtuelle Maschine. WebAssembly-virtuelle Maschinen sind heute in modernen Browsern vorhanden, und es gibt Toolchains, um C/C++ in WebAssembly-Code zu kompilieren. Um herauszufinden, was heute mit WebAssemblies möglich ist, haben wir uns entschieden, einen in C/C++ geschriebenen Python-Interpreter zu nehmen, ihn in WebAssembly zu kompilieren und ihn in VS Code for the Web auszuführen. Glücklicherweise hat das Python-Team bereits mit dem Kompilieren von CPython in WASM begonnen, und wir haben uns gerne an ihrer Arbeit beteiligt. Das Ergebnis der Untersuchung ist im kurzen Video unten zu sehen.

Execute a Python file in VS Code for the Web

Es sieht nicht wirklich anders aus, als Python-Code in VS Code Desktop auszuführen. Warum ist das also cool?

  • Der Python-Quellcode (app.py und hello.py) wird in einem GitHub-Repository gehostet und direkt von GitHub gelesen. Der Python-Interpreter hat vollen Zugriff auf die Dateien im Workspace, aber nicht auf andere Dateien.
  • Der Beispielcode ist mehrdateiig. app.py hängt von hello.py ab.
  • Die Ausgabe erscheint schön im Terminal von VS Code.
  • Sie können eine Python REPL ausführen und vollständig damit interagieren.
  • Und natürlich läuft es im Web.

Zusätzlich erfordert der in WebAssembly (WASM) kompilierte Python-Interpreter keine Modifikation, um in VS Code for the Web zu laufen. Die Bits sind eins zu eins dieselben, die vom CPython-Team erstellt wurden.

Wie funktioniert das?

WebAssembly-virtuelle Maschinen bringen kein SDK mit (wie zum Beispiel Java oder .NET). Daher kann WebAssembly-Code out-of-the-box keine Konsolenausgaben machen oder den Inhalt einer Datei lesen. Die WebAssembly-Spezifikation definiert, wie WebAssembly-Code Funktionen in dem Host aufrufen kann, der die virtuelle Maschine ausführt. Im Fall von VS Code for the Web ist der Host der Browser. Die virtuelle Maschine kann daher JavaScript-Funktionen aufrufen, die im Browser ausgeführt werden.

Das Python-Team stellt WebAssembly-Binaries seines Interpreters in zwei Varianten zur Verfügung: eine, die mit emscripten kompiliert wurde, und eine andere, die mit dem WASI SDK kompiliert wurde. Obwohl beide WebAssembly-Code erzeugen, haben sie unterschiedliche Eigenschaften in Bezug auf die JavaScript-Funktionen, die sie als Host-Implementierung bereitstellen.

  • emscripten – hat einen besonderen Fokus auf die Web-Plattform und Node.js. Zusätzlich zur Erzeugung von WASM-Code erzeugt es auch JavaScript-Code, der als Host dient, um den WASM-Code entweder im Browser oder in der Node.js-Umgebung auszuführen. Zum Beispiel stellt der JavaScript-Code eine Funktion bereit, um den Inhalt einer C printf-Anweisung auf der Konsole des Browsers auszugeben.
  • WASI SDK – kompiliert C/C++-Code zu WASM und geht von einer Host-Implementierung aus, die der WASI-Spezifikation entspricht. WASI steht für WebAssembly System Interface. Es definiert mehrere betriebssystemähnliche Funktionen, darunter Dateien und Dateisysteme, Sockets, Uhren und Zufallszahlen. Das Kompilieren von C/C++-Code mit dem WASI SDK erzeugt nur WebAssembly-Code, aber keine JavaScript-Funktionen. Die notwendigen JavaScript-Funktionen, um den Inhalt einer C printf-Anweisung auszugeben, müssen vom Host bereitgestellt werden. Wasmtime ist beispielsweise eine Laufzeitumgebung, die eine WASI-Host-Implementierung bereitstellt, die WASI an Betriebssystemaufrufe bindet.

Für VS Code haben wir uns entschieden, WASI zu unterstützen. Obwohl unser Hauptaugenmerk darauf liegt, WASM-Code im Browser auszuführen, führen wir ihn nicht in einer reinen Browserumgebung aus. Wir müssen WebAssemblies im Extension Host Worker von VS Code ausführen, da dies der Standardweg ist, wie VS Code erweitert wird. Der Extension Host Worker stellt neben der Worker-API des Browsers die gesamte VS Code Extension API bereit. Anstatt also einen printf-Aufruf in einem C/C++-Programm mit der Konsole des Browsers zu verbinden, wollen wir ihn tatsächlich mit der Terminal-API von VS Code verbinden. Dies in WASI zu tun, war für uns einfacher als in emscripten.

Unsere aktuelle Implementierung des WASI-Hosts von VS Code basiert auf dem WASI Snapshot Preview1, und alle in diesem Blogbeitrag beschriebenen Implementierungsdetails beziehen sich auf diese Version.

Wie kann ich meinen eigenen WebAssembly-Code ausführen?

Nachdem wir Python in VS Code for the Web zum Laufen gebracht hatten, erkannten wir schnell, dass der von uns gewählte Ansatz es uns ermöglicht, jeden Code auszuführen, der in WASI kompiliert werden kann. Dieser Abschnitt demonstriert daher, wie ein kleines C-Programm mit dem WASI SDK in WASI kompiliert und in VS Codes Extension Host ausgeführt wird. Das Beispiel geht davon aus, dass der Leser mit der VS Code Extension API vertraut ist und weiß, wie man eine Erweiterung für VS Code for the Web schreibt.

Das C-Programm, das wir ausführen, ist ein einfaches "Hello World"-Programm, das wie folgt aussieht:

#include <stdio.h>

int main(void)
{
    printf("Hello, World\n");
    return 0;
}

Unter der Annahme, dass Sie das neueste WASI SDK installiert haben und es in Ihrem PATH vorhanden ist, kann das C-Programm mit dem folgenden Befehl kompiliert werden:

clang hello.c -o ./hello.wasm

Dies erzeugt eine hello.wasm-Datei neben der hello.c-Datei.

Neue Funktionen werden über Erweiterungen zu VS Code hinzugefügt, und wir folgen demselben Modell, wenn wir WebAssemblies in VS Code integrieren. Wir müssen eine Erweiterung definieren, die den WASM-Code lädt und ausführt. Die wichtigen Teile des package.json-Manifests der Erweiterung sind wie folgt:

{
    "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"
    }
}

Die Erweiterung ms-vscode.wasm-wasi-core liefert die WebAssembly-Ausführungs-Engine, die die WASI-API mit der VS Code API verbindet. Das Node-Modul @vscode/wasm-wasi bietet eine Fassade zum Laden und Ausführen von WebAssembly-Code in VS Code.

Unten sehen Sie den eigentlichen TypeScript-Code zum Laden und Ausführen von WebAssembly-Code:

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);
    }
  });
}

Das untenstehende Video zeigt die laufende Erweiterung in VS Code for the Web.

Run Hello World

Wir haben C/C++-Code als Quelle für das WebAssembly verwendet, und da WASI ein Standard ist, gibt es andere Toolchains, die WASI unterstützen. Beispiele sind: Rust, .NET oder Swift.

Die WASI-Implementierung von VS Code

WASI und die VS Code API teilen Konzepte wie ein Dateisystem oder stdio (zum Beispiel ein Terminal). Dies ermöglichte uns, die WASI-Spezifikation auf der VS Code API zu implementieren. Allerdings war das unterschiedliche Ausführungsverhalten eine Herausforderung: Die Ausführung von WebAssembly-Code ist synchron (z. B. sobald eine WebAssembly-Ausführung gestartet ist, ist der JavaScript-Worker blockiert, bis die Ausführung beendet ist), während die meisten APIs von VS Code und dem Browser asynchron sind. Zum Beispiel ist das Lesen aus einer Datei in WASI synchron, während die entsprechende VS Code API asynchron ist. Dieses Merkmal verursacht zwei Probleme für die Ausführung von WebAssembly-Code innerhalb des VS Code Extension Host Workers:

  • Wir müssen verhindern, dass der Extension Host während der Ausführung von WebAssembly-Code blockiert wird, da dies die Ausführung anderer Erweiterungen blockieren würde.
  • Es wird ein Mechanismus benötigt, um die synchrone WASI-API auf der asynchronen VS Code und Browser-API zu implementieren.

Der erste Fall ist einfach zu lösen: Wir führen den WebAssembly-Code in einem separaten Worker-Thread aus. Der zweite Fall ist schwieriger zu lösen, da die Abbildung von synkronem Code auf asynchronen Code das Aussetzen des synchronen Ausführungsthreads und dessen Wiederaufnahme erfordert, wenn das asynchron berechnete Ergebnis verfügbar ist. Der JavaScript-Promise Integration Proposal for WebAssembly löst dieses Problem auf der WASM-Ebene, und es gibt eine experimentelle Implementierung des Vorschlags in V8. Als wir jedoch mit der Arbeit begannen, war die V8-Implementierung noch nicht verfügbar. Daher wählten wir eine andere Implementierung, die SharedArrayBuffer und Atomics verwendet, um die synchrone WASI-API auf die asynchrone VS Code API abzubilden.

Der Ansatz funktioniert wie folgt:

  • Der WASM-Worker-Thread erstellt einen SharedArrayBuffer mit den notwendigen Informationen über den Code, der auf der VS Code-Seite aufgerufen werden soll.
  • Er postet den gemeinsamen Speicher an den Extension Host Worker von VS Code und wartet dann mit Atomics.wait darauf, dass der Extension Host Worker seine Arbeit beendet.
  • Der Extension Host Worker nimmt die Nachricht entgegen, ruft die entsprechende VS Code API auf, schreibt Ergebnisse zurück in den SharedArrayBuffer und benachrichtigt dann den WASM-Worker-Thread, dass er mit Atomics.store und Atomics.notify aufwachen soll.
  • Der WASM-Worker liest dann die Ergebnisdaten aus dem SharedArrayBuffer aus und gibt sie an den WASI-Callback zurück.

Die einzige Schwierigkeit bei diesem Ansatz ist, dass SharedArrayBuffer und Atomics erfordern, dass die Seite cross-origin isolated ist, was aufgrund der viralen Natur von CORS an sich schon eine Herausforderung sein kann. Deshalb ist es derzeit nur standardmäßig auf der Insiders-Version insiders.vscode.dev aktiviert und muss auf vscode.dev über den Query-Parameter ?vscode-coi=on aktiviert werden.

Unten sehen Sie ein Diagramm, das die Interaktion zwischen dem WASM-Worker und dem Extension Host Worker im Detail für das oben kompilierte C-Programm zeigt. Der Code im orangen Kasten ist WebAssembly-Code, und der gesamte Code in den grünen Kästen läuft in JavaScript. Der gelbe Kasten repräsentiert den SharedArrayBuffer.

Interaction between the WASM worker and the extension host

Eine Web-Shell

Nachdem wir C/C++- und Rust-Code in WebAssembly kompilieren und in VS Code ausführen konnten, haben wir untersucht, ob wir auch eine Shell in VS Code for the Web ausführen können.

Wir haben untersucht, wie man eine der Unix-Shells in WebAssembly kompiliert. Einige Shells verlassen sich jedoch auf Betriebssystemfunktionen (Prozessstarts, ...), die in WASI derzeit nicht verfügbar sind. Dies führte uns zu einem leicht anderen Ansatz: Wir implementierten eine einfache Shell in TypeScript und versuchten, nur die Unix-Core-Utils wie ls, cat, date, ... in WebAssembly zu kompilieren. Da Rust eine sehr gute Unterstützung für WASM und WASI hat, haben wir uns an den uutils/coreutils, einer plattformübergreifenden Neuimplementierung der GNU Coreutils in Rust, versucht. Und voilà, wir hatten eine erste minimale Web-Shell.

A web shell

Eine Shell ist sehr eingeschränkt, wenn man keine benutzerdefinierten WebAssemblies oder Befehle ausführen kann. Um die Web-Shell zu erweitern, können andere Erweiterungen zusätzliche Mountpoints zum Dateisystem beisteuern, sowie Befehle, die aufgerufen werden, wenn sie in die Web-Shell eingegeben werden. Die Indirektion über Befehle entkoppelt die konkrete WebAssembly-Ausführung von dem, was im Terminal eingegeben wird. Die Nutzung dieser Unterstützung in der Python-Erweiterung von Anfang an ermöglicht es Ihnen, Python-Code direkt aus der Shell auszuführen, indem Sie python app.py in die Eingabeaufforderung eingeben oder die Standard-Python 3.11-Bibliothek auflisten, die normalerweise unter /usr/local/lib/python3.11 gemountet ist.

Python integration into web shell

Wie geht es weiter?

Die WASM-Ausführungs-Engine-Erweiterung und die Web-Shell-Erweiterung sind beides experimentelle Vorschauen und sollten nicht zur Implementierung produktionsreifer Erweiterungen mit WebAssemblies verwendet werden. Sie wurden öffentlich zugänglich gemacht, um frühes Feedback zur Technologie zu erhalten. Wenn Sie Fragen oder Feedback haben, eröffnen Sie bitte Issues im entsprechenden vscode-wasm GitHub-Repository. Dieses Repository enthält auch den Quellcode für das Python-Beispiel sowie für die WASM-Ausführungs-Engine und die Web-Shell.

Was wir wissen, ist, dass wir die folgenden Themen weiter untersuchen werden:

  • Das WASI-Team arbeitet an einer Preview2 und Preview3 der Spezifikation, die wir ebenfalls unterstützen wollen. Die neuen Versionen werden die Art und Weise ändern, wie ein WASI-Host implementiert wird. Wir sind jedoch zuversichtlich, dass wir unsere API, die in der WASM-Ausführungs-Engine-Erweiterung exponiert wird, größtenteils stabil halten können.
  • Es gibt auch die WASIX-Initiative, die WASI um zusätzliche betriebssystemähnliche Funktionen wie Prozesse oder Futex erweitert. Wir werden diese Arbeit weiter verfolgen.
  • Viele Language Server für VS Code sind in anderen Sprachen als JavaScript oder TypeScript implementiert. Wir planen, die Möglichkeit zu untersuchen, diese Language Server nach wasm32-wasi zu kompilieren und sie ebenfalls in VS Code for the Web auszuführen.
  • Verbesserung des Debuggings für Python im Web. Wir haben damit begonnen, daran zu arbeiten, also bleiben Sie dran.
  • Fügen Sie Unterstützung hinzu, damit Erweiterung B WebAssembly-Code ausführen kann, der von Erweiterung A beigesteuert wird. Dies ermöglicht es beispielsweise beliebigen Erweiterungen, Python-Code auszuführen, indem die Erweiterung wiederverwendet wird, die das Python-WebAssembly beigesteuert hat.
  • Sicherstellen, dass andere Sprachlaufzeiten, die für wasm32-wasi kompiliert sind, auf der WebAssembly-Ausführungs-Engine von VS Code laufen. VMware Labs bietet Ruby und PHP wasm32-wasi Binaries an, und beide laufen in VS Code.

Danke,

Dirk und das VS Code-Team

Viel Spaß beim Programmieren!

© . This site is unofficial and not affiliated with Microsoft.