2 Punkte von GN⁺ 2024-11-11 | 1 Kommentare | Auf WhatsApp teilen
  • Jawsm ist ein experimentelles JavaScript→WebAssembly-Compiler-Tool, das in Rust geschrieben ist und eigenständige WASM-Binärdateien erzeugt, die ohne Interpreter ausgeführt werden können
  • Ähnlich wie porffor erstellt es standalone WASM, nutzt jedoch einen anderen Implementierungsansatz und wandelt JavaScript-Syntax mithilfe von Anweisungen aus den neuesten Vorschlägen für WASM GC, Exception Handling und Tail-Call-Optimierung in WASM-Instruktionen um
  • Derzeit besteht es etwa 25 % der test262-Test-Suite; als entscheidend für die Bestätigung der Machbarkeit angesehene Funktionen wie Scopes/Closures, try/catch, async/await und Generators sind implementiert
  • Es ist noch nicht produktionsreif; viele Sprachfunktionen und Builtin-Typen fehlen noch oder sind unvollständig, außerdem fehlen RegExp, die meisten Builtins und BigInt-Arithmetik
  • Die erzeugten Binärdateien sind wegen der Abhängigkeit von aktuellen WASM-Vorschlägen nur eingeschränkt zwischen Laufzeitumgebungen portabel; derzeit werden sie in V8-basiertem Chromium oder Node zusammen mit einem WASIp2-Polyfill ausgeführt

Ziel und Einordnung von Jawsm

  • Jawsm ist ein JavaScript-to-WebAssembly-Compiler, dessen Name wie „awesome“ ausgesprochen wird
  • Er ist in Rust geschrieben und hat das Ziel, standalone WASM binary aus JavaScript-Code zu erzeugen, die ohne Interpreter ausgeführt werden können
  • Er erzeugt ein ähnliches Ergebnis wie porffor, verwendet jedoch einen anderen Implementierungsansatz
  • Derzeit ist es ein experimentelles Tool und noch nicht bereit für den Produktionseinsatz
    • Viele JavaScript-Sprachfunktionen fehlen oder sind unvollständig
    • Auch viele Builtin-Typen und Methoden fehlen oder sind unvollständig
  • Das langfristige Ziel ist die 100%ige Unterstützung der JavaScript-Sprachfunktionen

Warum Jawsm entwickelt wurde

  • Das Projekt begann während der Arbeit an Crows, einem Stresstest-Tool zum Ausführen von WebAssembly-Szenarien
  • Crows unterstützt derzeit nur Code, der aus Rust nach WASM kompiliert wurde
  • Kleine Tests lassen sich oft leichter in einer interpretierten Sprache schreiben, aber das Ausführen einer Scripting-Sprache auf WASM ist derzeit nicht ideal
    • Wenn man einen Interpreter mitliefert, beträgt die Binärgröße mindestens einige MB und auch der Speicherverbrauch steigt
    • Alternativ muss man Varianten der Zielsprache wie TinyGo oder AssemblyScript verwenden
  • Durch die Nutzung moderner WASM-Vorschläge verfolgt das Projekt das Ziel, 100 % der JavaScript-Funktionen auch ohne einen kompilierten Interpreter zu implementieren
  • Dabei wird vorausgesetzt, dass die WASM-Runtime selbst bereits ein Interpreter ist

Funktionen, die derzeit funktionieren

  • Jawsm besteht derzeit etwa 25 % der test262-Test-Suite
  • Alle vier Kernfunktionen zur Überprüfung der Projekt-Machbarkeit sind implementiert
    • Scopes/Closures
    • try/catch
    • async/await
    • Generators
  • Darüber hinaus sollen folgende Funktionen funktionieren
    • Deklarationen und Zuweisungen mit var, let, const
    • Schleifen do..while, while, for, for..in, for..of
    • switch-Anweisungen
    • eingeschränkte Unterstützung für break und continue
    • String-Literale und Konkatenation von String-Literalen
    • Zahlen und grundlegende Operatoren +, -, *, /
    • Boolean-Werte und grundlegende Boolean-Operatoren
    • Arrays und die meisten Array-bezogenen Funktionen
    • Object Literals
    • das Schlüsselwort new
    • async, await
    • eingeschränkte Unterstützung der Promise-API
    • Generator-Funktionen
    • try/catch
    • sehr grundlegende BigInt-Unterstützung

Noch fehlende Funktionen

  • Derzeit fehlen vor allem die folgenden Punkte
    • die meisten Builtins
    • die meisten Methoden der vorhandenen Builtins
    • RegExp-Ausdrücke
    • BigInt-Arithmetik
  • Die nächsten Schritte konzentrieren sich auf die Implementierung der folgenden Funktionen
    • grundlegende RegExp-Unterstützung
      • RegExp-Literale
      • sehr grundlegende Funktionen des RegExp-Objekts
    • BigInt-Literale und grundlegende BigInt-Unterstützung
    • bessere automatische Typumwandlung bei Equality-Prüfungen oder beim Einsatz verschiedener Operatoren
    • mehr Funktionen für grundlegende Builtins wie Arrays, Strings usw.

Laufzeitumgebung und Einschränkungen

  • Jawsm verwendet einige vergleichsweise neue WASM-Vorschläge, daher sind die erzeugten Binärdateien derzeit nur eingeschränkt zwischen Laufzeitumgebungen portabel
  • Die Zielimplementierung ist auf WASIp2 ausgerichtet
  • Wasmtime ist eine Runtime, die Components und WASIp2 ausführen kann, unterstützt aber einige von Jawsm verwendete Funktionen wie Teile von WASM GC oder Exception Handling nicht
  • Um die Entwicklung zu erleichtern, bis die Runtimes die standardisierten Vorschläge aufgeholt haben, wird V8 verwendet
    • V8 wird über Chromium oder Node genutzt
    • Erforderliche WASIp2-Funktionen werden durch ein JavaScript-Polyfill ergänzt
  • Im Repository gibt es ein Skript run.js, das von Jawsm erzeugte Binärdateien ausführt
  • Langfristig ist das Ziel, auf allen Runtimes lauffähig zu sein, die WASM GC, Exception Handling und die WASIp2-API implementieren
    • einschließlich des Ansatzes mit einem WASIp2-Polyfill

Verwendung

  • Sofern man nicht beitragen möchte, wird die Nutzung derzeit nicht empfohlen
  • Nach dem Klonen des Repositorys kann execute.sh wie folgt verwendet werden
./execute.sh --cargo-run path/to/script.js
  • Dieser Befehl erzeugt eine WAT-Datei, kompiliert sie zu einer Binärdatei und führt sie dann mit Node.js aus
  • Erforderliche Werkzeuge sind
    • Rusts cargo
    • eine vergleichsweise aktuelle Version von wasm-tools
    • Node.js v23.0.0 oder neuer
  • Wenn die Option --cargo-run übergeben wird, wird das Projekt zunächst mit cargo run kompiliert und dann ausgeführt
  • Ohne --cargo-run wird versucht, den Release-Build auszuführen; daher muss zuvor cargo build --release ausgeführt werden

Interne Funktionsweise

  • Jawsm wandelt JavaScript-Syntax in WASM-Instruktionen um
  • Der Umwandlungsprozess nutzt Anweisungen aus den folgenden WASM-Vorschlägen
    • WASM GC

      • Exception Handling
      • Tail-Call-Optimierungen
      • Der Rust-Code transformiert das Skript, zusammen mit einem Satz von Typen und Funktionen, um JavaScript-Semantik nach WASM zu übertragen
      • Die meisten WASM-Instruktionen werden mit tarnik erzeugt
      • tarnik ist ein Rust-Makro, das auf Rust-ähnlicher Syntax basiert und WASM-Instruktionen erzeugt

Beispiel für Scope- und Closure-Verarbeitung

  • WASM unterstützt Funktionsreferenzen, Structs und Arrays, stellt aber die Scope-Semantik von JavaScript nicht direkt bereit
  • Jawsm erzeugt zusätzlichen WASM-Code, um das Scope-Verhalten von JavaScript nachzuahmen
  • Beispielhafter JavaScript-Code
let a = "foo";

function bar() {
  console.log(a);
}

bar();
  • In JavaScript erbt eine Funktionsdefinition den Scope, in dem sie definiert wurde; daher muss bar() auf die Variable a zugreifen können
  • Der Transformationsablauf sieht ungefähr wie folgt aus
    • Es wird ein globaler Scope ohne Parent erzeugt
    • Im aktuellen Scope wird die Variable a mit "foo" deklariert
    • Beim Erzeugen des Funktionsobjekts bar wird auch eine Referenz auf den Scope gespeichert, in dem die Funktion definiert wurde
    • Bei der Ausführung innerhalb der Funktion wird ein neuer Scope erzeugt, wobei die Referenz auf parentScope erhalten bleibt
    • retrieve(scope, "a") sucht a im aktuellen Scope und in allen Parent-Scopes
    • bar wird aus dem aktuellen Scope geholt und aufgerufen

Lizenz

  • Der Code wird unter der Apache-2.0-Lizenz veröffentlicht

1 Kommentare

 
GN⁺ 2024-11-11
Meinungen auf Hacker News
  • Der WASM-GC-Vorschlag wurde wirklich clever genutzt
    Bisher haben JS→WASM-Compiler im Grunde die gesamte JS-Engine mitgeliefert; dass jemand versucht, JS-Strukturen direkt auf native WASM-Funktionen abzubilden, sehe ich zum ersten Mal

    • Porffor https://porffor.dev/ und Static Hermes https://hermesengine.dev/ scheinen ebenfalls einen Compiler-Ansatz zu verfolgen
      Ein Vergleich mit Jaws wäre interessant
    • Stimmt. Ehrlich gesagt ist es aber eher eine clevere Nutzung, die daraus entstanden ist, dass ich nicht schlau genug bin, um einen vollständigen Interpreter auf WASM zu schreiben
  • Ich habe früher einmal eine Sprache, die fast TypeScript entsprach, als Embedded-ARM-Compiler gebaut
    Sie war deutlich näher an TypeScript als AssemblyScript, und einige der damals verwendeten Techniken könnten hilfreich sein
    https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf

  • Stimmt die Aussage: „Ich verwende Rust wirklich gern, weiß aber auch, dass es keine weit verbreitete Sprache ist“?
    Rust wird extrem gehypt und scheint heutzutage überall eingesetzt zu werden

    • Ich habe noch keinen Rust-Job außerhalb des Kryptobereichs gefunden
      In meinem Land Estland gibt es auf den lokalen Jobbörsen, die die Leute nutzen, 0 Stellen vor Ort; daher kann man in einem praktisch relevanten Sinn kaum sagen, dass es beliebt ist
    • Dass etwas in sozialen Medien gehypt wird und sehr sichtbar ist, heißt nicht zwingend, dass es breit eingesetzt wird
      Es hängt davon ab, wie man „breit“ definiert, aber nach Indizes wie StackOverflow und PyPL sowie GitHub-Statistiken scheint die Rust-Nutzung bei etwa 5–10 % von JavaScript oder Python zu liegen
  • Wenn man „ziemlich zuversichtlich ist, am Ende 100 % der JavaScript-Spezifikation abdecken zu können“, gibt es dann Ergebnisse von test262_runner.rb?
    Ich habe test262 durch einen Vortrag des Porffor-Autors kennengelernt, und es wäre schön, im README eine Fortschrittsanzeige wie https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262 zu haben
    Tolles Projekt

    • Derzeit bestehen etwa 12 % der Tests, aber es gibt noch viele leicht zu implementierende Teile
      Das gilt besonders, wenn man bedenkt, dass das Projekt erst vor zwei Wochen gestartet wurde; natürlich heißt das nicht, dass der Weg zu 100 % linear verlaufen kann
      Es gibt noch einen langen Tail an eingebauten Typen und Funktionen, aber weil ich zuerst die „schwierigen Teile“ implementiere, sind einige einfache Dinge noch nicht erledigt
      Zum Beispiel ist von der Syntax nur so viel implementiert, wie nötig ist, um das test262-Harness auszuführen: bedingte Anweisungen und while-Schleifen; for, for in, for of, do while sowie Ausdrücke wie switch sind noch nicht implementiert
      Diese lassen sich fast genauso wie die vorhandenen if/else- und while-Implementierungen hinzufügen

      Sobald await und Generatoren fertig implementiert sind, sind die letzten schwierigen semantischen Konzepte gelöst; danach werde ich solche einfachen Teile umsetzen
      Schwer zu sagen, wie stark die Abdeckung steigen wird, aber zum Beispiel schlagen derzeit 1200 Tests fehl, weil die Syntax object["foo"] noch nicht implementiert ist
      object.foo funktioniert, object["foo"] aber nicht
      Das bedeutet nicht, dass diese 1200 Tests automatisch alle bestehen werden, aber oft scheitern Hunderte Tests an solchen relativ einfachen fehlenden Syntaxelementen

      So ein schönes Diagramm wie bei Porffor möchte ich auf jeden Fall auch hinzufügen

  • Auch nach dem Lesen der README.md des Projekts ist mir noch nicht klar: Was ist die erwartete Art der Nutzung?
    Mich interessiert, wie der ausgegebene WASM-Code mit welcher Runtime interagiert
    Außerdem frage ich mich, ob es ein Tool ist, das sowohl mit Browsern als auch mit anderen WASM-Runtimes kompatibel ist, oder ob es nur in der mit dem Projekt verbundenen Runtime läuft

    In dem Zusammenhang: Wie reagiert es, wenn JavaScript-Code auf Web-APIs oder globale Bezeichner trifft, die nur in bestimmten Umgebungen definiert sind, zum Beispiel globale Bezeichner moderner Browser oder von Node.js?
    Wenn solche Umgebungen nicht das Ziel sind, wie soll I/O funktionieren?

    • Gute Fragen, ich werde das README noch ausführlicher ergänzen
      Dieses Projekt zielt vor allem auf WebAssembly-Nutzung auf dem Server ab
      JavaScript in WebAssembly innerhalb von JavaScript auszuführen, halte ich für wenig sinnvoll, aber mit der Zeit könnte es für Frontend-Plugin-Sandboxing nützlich werden

      Ob im Browser oder in Backend-Runtimes wie WasmTime oder WasmEdge: JavaScript innerhalb von WebAssembly auszuführen, ist derzeit nicht ideal
      Man muss entweder eine JS-Engine wie V8 oder SpiderMonkey nach WASM kompilieren und Skripte darauf ausführen, oder sich mit einer „fast JavaScript“-Sprache wie AssemblyScript zufriedengeben
      Das wird zu einem limitierenden Faktor für Server-Workloads
      Fastly verwendet zum Beispiel SpiderMonkey für WASM-Worker, und schon ein Hello World verbraucht 5–10 MB Speicher pro Instanz
      Shopify dagegen nutzt WASM für serverseitige Anpassungen von Shops und beschränkt WASM-Binaries auf höchstens 250 KB; bei dieser Größe ist es schwierig, irgendeinen Interpreter unterzubringen
      Deshalb wurde AssemblyScript zur „empfohlenen“ Sprache; der Grund wird hier erklärt: https://shopify.engineering/shopify-webassembly

      Historisch lag diese Situation daran, dass WASM eine sehr einfache Runtime war
      Es war vergleichsweise einfach, C-Code nach WASM zu kompilieren, ähnlich wie man ihn in Maschinencode kompiliert; aber obwohl WebAssembly selbst eine Art Interpreter ist, war es nicht leicht, darauf noch höherstufige Sprachen zu interpretieren

      Jetzt, da neue Vorschläge wie Garbage-Collection-Unterstützung und Exception-Handling-Unterstützung standardisiert werden, wird WebAssembly zu einem deutlich leistungsfähigeren Interpreter mit Funktionen wie Structs, Arrays und Funktionsreferenzen

Jaws nutzt diesen Punkt, indem es JS-Code in WASM-Code umwandelt und WASM den resultierenden Code interpretieren lässt, ohne eine JS-Engine wie SpiderMonkey
In der Praxis könnte das von Jaws erzeugte Binary vermutlich unter 50 KB groß sein, im Gegensatz zu den 10 MB bei einem Ansatz, bei dem SpiderMonkey nach WASM kompiliert wird und die Skripte darauf laufen
Auch der Speicherverbrauch dürfte deutlich sinken
Für Unternehmen wie Fastly bedeutet das, Speicherverbrauch und Serverkosten um Größenordnungen senken zu können; für Unternehmen wie Shopify bedeutet es, vorhandenen JavaScript-Code, etwa NPM-Pakete und das JavaScript-Ökosystem, für Autoren von Backend-Plugins nutzbar zu machen

Die vom Projekt verwendete Runtime ist **ausschließlich WebAssembly**  
Der erzeugte Code besteht im Wesentlichen aus etwa 3.000 Zeilen WAT-Code in dieser Datei [https://github.com/drogus/jaws/blob/main/src/wat/template.wat](<https://github.com/drogus/jaws/blob/main/src/wat/template.wat>;) sowie dem umgewandelten Teil des JS-Codes des Nutzers  
Bei einem sehr einfachen Programm wie `"console.log('foo')"` ist der gesamte „generierte“ Teil zum Beispiel nur dies: [https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…](<https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…;)  
Das bereitet im Wesentlichen das Argument mit `new_static_string` vor und ruft dann `console.log` auf  
Derzeit ist auf der Host-Seite noch etwas Glue-Code nötig, aber am Ende sollen solche Binaries auf jeder Runtime laufen können, die WASIp2, WASM GC und den Vorschlag zur Exception-Behandlung unterstützt

Unterstützung für Web-APIs oder umgebungsspezifische globale Bezeichner ist noch nicht implementiert, aber die Funktionsweise lässt sich beschreiben  
**Node.js-APIs** sollen über WASI unterstützt werden  
WASI ist ein Standard, über den WASM-Programme mit der Außenwelt kommunizieren  
Er definiert zum Beispiel eine Menge standardisierter Funktionen zum Senden von HTTP-Requests, Schreiben auf STDOUT sowie Lesen/Schreiben von Dateien  
Wenn man also APIs wie `fetch` oder `fs` erreicht, sollten sie in Runtimes funktionieren, die WASI preview2 unterstützen  
Browser können ebenfalls per Polyfill unterstützt werden, in diesem Fall wird die I/O-Unterstützung aber stärker angepasst  
Wenn man einem WASM-Programm erlaubt, Dateien zu lesen oder zu schreiben, muss man einen Mechanismus bereitstellen, etwa Speichern in localStorage, Nutzung einer nach WASM kompilierten SQLite-Datenbank oder sogar Weiterleitung an etwas wie S3
  • Es fühlt sich an, als rücke „JS ohne Browser-Runtime ausführen“ näher
    Porffor, Jaws oder eines der anderen Projekte wird am Ende wohl Erfolg haben

  • Mir gefällt dieser Ansatz wirklich
    Statt zu versuchen, direkt ein Binary zu erzeugen, kann man direkt auf WASM als Target bauen und sich auf WASM GC sowie die Async-Unterstützung verlassen, die offenbar in WASI 0.3 enthalten sein wird

  • Wie geht man mit Unterschieden bei der String-Codierung und den zugehörigen Utilities um?
    Soweit ich es grob verstehe, unterstützt WASM UTF-8, während JS potenziell auch kaputtes UTF-16 unterstützt

    • Wie bei CPU-Befehlssätzen gibt es in der abstrakten WASM-Maschine kein Konzept von Strings oder Encodings
      Es sind nur Bytes im linearen Speicher, und das gewünschte Encoding implementiert man selbst

      WASM legt im Dateiformat UTF-8 als Encoding für Namen fest, aber das hat nichts mit der Runtime-VM zu tun

  • Manche Leute nennen so etwas auch einen Compiler
    Jedenfalls gut gemacht

    • Mir ist beim Schreiben des Titels überhaupt nicht aufgefallen, dass die Formulierung etwas seltsam ist
  • Ist das schneller, als denselben Code einfach als JS auszuführen, oder geht es um Interop mit anderen Sprachen?

    • In diesem Stadium ist das schwer zu sagen, aber ich halte es für sehr unwahrscheinlich, dass es schneller wird als SpiderMonkey oder V8 mit aktiviertem JIT
      Moderne JavaScript-Compiler optimieren häufig ausgeführte Pfade per JIT ziemlich gut
      Der Zweck dieses Projekts ist es, JavaScript in einer WebAssembly-Sandbox ausführen zu können
      Shopify erlaubt zum Beispiel, Backend-Code mit WebAssembly zu erweitern, begrenzt die Binary-Größe aber auf 250 KB
      In dieser Größenordnung ist JavaScript derzeit schwer nutzbar, weil selbst einfache Interpreter wie QuickJS, nach WASM kompiliert, mehrere MB groß werden