- 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
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
Ein Vergleich mit Jaws wäre interessant
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
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
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
awaitund Generatoren fertig implementiert sind, sind die letzten schwierigen semantischen Konzepte gelöst; danach werde ich solche einfachen Teile umsetzenSchwer zu sagen, wie stark die Abdeckung steigen wird, aber zum Beispiel schlagen derzeit 1200 Tests fehl, weil die Syntax
object["foo"]noch nicht implementiert istobject.foofunktioniert,object["foo"]aber nichtDas 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
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
Ist das schneller, als denselben Code einfach als JS auszuführen, oder geht es um Interop mit anderen Sprachen?
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