- Jawsm est un compilateur JavaScript→WebAssembly écrit en Rust, un outil expérimental qui crée des binaires WASM autonomes exécutables sans interpréteur
- Comme porffor, il génère du WASM standalone, mais avec une approche d’implémentation différente, en convertissant la syntaxe JavaScript en instructions WASM à l’aide des instructions des propositions récentes WASM GC, de gestion des exceptions et d’optimisation des appels terminaux
- Il réussit actuellement environ 25 % de la suite de tests test262, et les éléments jugés essentiels pour valider sa viabilité — scopes/closures, try/catch, async/await et generators — sont implémentés
- Il n’est pas encore prêt pour la production : de nombreuses fonctionnalités du langage et types intégrés sont absents ou incomplets, et RegExp, la plupart des builtins ainsi que l’arithmétique BigInt manquent encore
- Les binaires générés sont peu portables entre runtimes en raison de leur dépendance à des propositions WASM récentes ; pour l’instant, l’exécution se fait sur Chromium ou Node basés sur V8 avec un polyfill WASIp2
Objectif et positionnement de Jawsm
- Jawsm est un compilateur JavaScript vers WebAssembly, prononcé comme “awesome”
- Il est écrit en Rust et vise à transformer du code JavaScript en standalone WASM binary exécutable sans interpréteur
- Il produit un résultat similaire à porffor, mais avec une approche d’implémentation différente
- C’est actuellement un outil expérimental qui n’est pas prêt pour un usage en production
- De nombreuses fonctionnalités du langage JavaScript sont absentes ou incomplètes
- De nombreux types et méthodes intégrés sont eux aussi absents ou incomplets
- L’objectif à long terme est de prendre en charge 100 % des fonctionnalités du langage JavaScript
Pourquoi Jawsm a été créé
- Le projet a commencé pendant le travail sur Crows, un outil de stress test qui exécute des scénarios WebAssembly
- Crows ne prend actuellement en charge que du code compilé depuis Rust vers WASM
- Les petits tests sont souvent plus simples à écrire dans un langage interprété, mais l’exécution d’un langage de script au-dessus de WASM n’est pas idéale aujourd’hui
- Inclure un interpréteur fait rapidement grimper la taille du binaire à plusieurs Mo au minimum, avec une consommation mémoire plus élevée
- Sinon, il faut utiliser une variante du langage cible, comme TinyGo ou AssemblyScript
- L’idée est qu’en exploitant les propositions WASM modernes, il devrait être possible d’implémenter 100 % des fonctionnalités de JavaScript sans interpréteur compilé
- Le projet part du principe que le runtime WASM est déjà lui-même un interpréteur
Fonctionnalités actuellement opérationnelles
- Jawsm réussit actuellement environ 25 % de la suite de tests test262
- Les quatre fonctionnalités clés utilisées pour vérifier la viabilité du projet sont toutes implémentées
- scopes/closures
- try/catch
- async/await
- generators
- Les autres fonctionnalités censées fonctionner sont les suivantes
- déclarations et affectations
var, let, const
- boucles
do..while, while, for, for..in, for..of
- instruction
switch
- prise en charge limitée de
break et continue
- littéraux de chaîne et concaténation de littéraux de chaîne
- nombres et opérateurs de base
+, -, *, /
- booléens et opérateurs booléens de base
- tableaux et la plupart des fonctions liées à Array
- object literals
- mot-clé
new
async, await
- prise en charge limitée de l’API
Promise
- generator functions
try/catch
- prise en charge très basique de BigInt
Fonctionnalités encore absentes
- Les principaux éléments manquants à ce jour sont les suivants
- la plupart des builtins
- la majorité des méthodes des builtins existants
- les expressions RegExp
- l’arithmétique BigInt
- Le plan pour les prochaines étapes se concentre sur l’implémentation des fonctionnalités suivantes
- prise en charge de base des regexp
- littéraux RegExp
- fonctions très basiques de l’objet RegExp
- littéraux BigInt et prise en charge de base de BigInt
- meilleur automatic casting lors des tests d’égalité ou de l’utilisation de plusieurs opérateurs
- davantage de fonctions pour les builtins de base comme arrays, strings, etc.
Environnement d’exécution et contraintes
- Jawsm utilise plusieurs propositions WASM relativement récentes, donc les binaires générés ne sont pas encore très portables entre runtimes
- L’implémentation cible est pensée pour WASIp2
- Wasmtime est un runtime capable d’exécuter des components et WASIp2, mais il ne prend pas en charge certains éléments utilisés par Jawsm, comme une partie de WASM GC ou la gestion des exceptions
- En attendant que les runtimes rattrapent les propositions standardisées, le projet utilise V8 pour faciliter le développement
- V8 est utilisé via Chromium ou Node
- Les fonctionnalités WASIp2 nécessaires sont complétées par un polyfill JavaScript
- Le dépôt contient un script
run.js qui exécute les binaires générés par Jawsm
- À terme, l’objectif est de pouvoir fonctionner sur tous les runtimes qui implémentent WASM GC, la gestion des exceptions et l’API WASIp2
- y compris via l’utilisation d’un polyfill WASIp2
Utilisation
- Son utilisation n’est pas recommandée pour le moment, sauf dans un but de contribution
- Après avoir cloné le dépôt, on peut utiliser
execute.sh comme suit
./execute.sh --cargo-run path/to/script.js
- Cette commande génère un fichier WAT, le compile en binaire, puis l’exécute avec Node.js
- Les outils nécessaires sont les suivants
cargo de Rust
- une version relativement récente de
wasm-tools
- Node.js v23.0.0 ou supérieur
- Si l’option
--cargo-run est fournie, le projet est d’abord compilé via cargo run, puis exécuté
- Sans
--cargo-run, le script tente d’exécuter un build release ; il faut donc lancer auparavant cargo build --release
Fonctionnement interne
- Jawsm transforme la syntaxe JavaScript en instructions WASM
- Le processus de transformation exploite les instructions issues des propositions WASM suivantes
-
WASM GC
- gestion des exceptions
- optimisations des appels terminaux
- Le code Rust transforme le script, avec un ensemble de types et de fonctions servant à transposer la sémantique JavaScript vers WASM
- La majorité des instructions WASM sont générées avec tarnik
- tarnik est une macro Rust qui génère des instructions WASM à partir d’une syntaxe de type Rust
Exemple de gestion des scopes et closures
- WASM prend en charge les références de fonctions, les structs et les arrays, mais ne fournit pas directement les scope semantics de JavaScript
- Jawsm génère du code WASM supplémentaire pour imiter le comportement des scopes JavaScript
- Voici le code JavaScript d’exemple
let a = "foo";
function bar() {
console.log(a);
}
bar();
- En JavaScript, une définition de fonction hérite du scope dans lequel elle est définie ;
bar() doit donc pouvoir accéder à la variable a
- Le flux de transformation est globalement le suivant
- créer un scope global sans parent
- déclarer la variable
a avec la valeur "foo" dans le scope courant
- lors de la création de l’objet fonction
bar, stocker aussi une référence vers le scope où la fonction est définie
- lors de l’exécution de la fonction, créer un nouveau scope tout en conservant la référence
parentScope
retrieve(scope, "a") recherche a dans le scope courant puis dans tous les scopes parents
- récupérer
bar dans le scope courant puis l’appeler
Licence
- Le code est distribué sous licence Apache 2.0
1 commentaires
Commentaires Hacker News
La proposition WASM GC est exploitée de façon vraiment ingénieuse
Jusqu’ici, les compilateurs JS→WASM embarquaient en pratique tout le moteur JS, donc c’est la première fois que je vois une tentative de mapper directement les structures de JS sur les fonctionnalités primitives de WASM
Ce serait intéressant de les comparer à Jaws
J’avais déjà créé un langage presque équivalent à TypeScript sous forme de compilateur ARM embarqué
C’était bien plus proche de TypeScript qu’AssemblyScript, et certaines techniques que j’avais utilisées à l’époque pourraient être utiles
https://www.microsoft.com/en-us/research/uploads/prod/2019/09/static-typescript-draft2.pdf
Est-ce vraiment juste de dire : « J’adore utiliser Rust, mais je sais aussi que ce n’est pas un langage largement populaire » ?
Rust semble être énormément survendu et donner l’impression d’être utilisé partout en ce moment
En Estonie, dans mon pays, il y a 0 offre locale sur les sites d’emploi que les gens utilisent vraiment, donc au sens concret du terme, on peut difficilement dire que c’est populaire
Tout dépend de la définition de « largement », mais d’après des indices comme StackOverflow, PyPL et les statistiques GitHub, l’usage de Rust semble représenter environ 5 à 10 % de celui de JavaScript ou Python
Si vous êtes « assez confiant sur le fait de pouvoir couvrir au final 100 % de la spécification JavaScript », avez-vous des résultats de test262_runner.rb ?
J’ai découvert test262 via une présentation de l’auteur de Porffor, et ce serait bien d’avoir dans le README un indicateur d’avancement comme https://github.com/CanadaHonk/porffor?tab=readme-ov-file#test262
Beau projet
Pour l’instant, il passe environ 12 % des tests, mais il reste beaucoup de choses faciles à implémenter
C’est d’autant plus vrai quand on considère que le projet n’a commencé qu’il y a deux semaines, même si cela ne veut évidemment pas dire qu’on peut aller jusqu’à 100 % de façon linéaire
Il reste une longue traîne de types et de fonctions intégrés, mais comme j’ai commencé par implémenter les « parties difficiles », certaines parties simples n’ont pas encore été faites
Par exemple, je n’ai implémenté que les conditions et les boucles while nécessaires pour faire tourner le harnais test262, mais pas encore les constructions conditionnelles comme for, for in, for of, do while et switch
Elles peuvent être ajoutées presque de la même façon que les implémentations existantes de if/else et while
Une fois que j’aurai fini d’implémenter
awaitet les générateurs, les derniers concepts sémantiques difficiles seront réglés, et je prévois ensuite d’ajouter ces parties plus simplesIl est difficile de dire à quel point la couverture augmentera, mais par exemple, à l’heure actuelle, 1200 tests échouent parce que la syntaxe
object["foo"]n’est pas implémentéeobject.foofonctionne, mais pasobject["foo"]Cela ne veut pas dire que ces 1200 tests passeront automatiquement d’un coup, mais il arrive souvent que des centaines de tests échouent simplement à cause de ce type d’absence de syntaxe relativement simple
J’aimerais absolument ajouter aussi de beaux graphiques comme Porffor
Même après avoir lu le README.md du projet, je ne comprends pas encore bien quel est le mode d’utilisation visé
Je me demande avec quel runtime le code WASM produit interagit, et de quelle manière
Je me demande aussi si c’est un outil compatible à la fois avec le navigateur et avec d’autres runtimes WASM, ou s’il ne fonctionne qu’avec le runtime lié au projet
Dans le même ordre d’idées, comment réagit-il lorsqu’il rencontre dans le code JavaScript des Web API ou des identifiants globaux définis uniquement dans certains environnements, par exemple des globaux propres aux navigateurs récents ou à Node.js ?
Et si ces environnements ne sont pas ciblés, comment faut-il gérer les E/S ?
Excellentes questions, et j’ajouterai plus de détails dans le README
Ce projet vise principalement l’usage de WebAssembly côté serveur
Faire tourner du JavaScript dans du WebAssembly depuis JavaScript n’a pas énormément d’intérêt à mes yeux, mais avec le temps cela pourrait devenir utile pour le sandboxing de plugins frontend
Que ce soit dans le navigateur ou dans des runtimes backend comme WasmTime ou WasmEdge, exécuter du JavaScript dans WebAssembly n’est actuellement pas idéal
Il faut soit compiler un moteur JS comme V8 ou SpiderMonkey en WASM pour exécuter les scripts dessus, soit se contenter d’un langage « presque JavaScript » comme AssemblyScript
Cela constitue un facteur limitant pour l’exécution de charges serveur
Par exemple, Fastly utilise SpiderMonkey dans ses workers WASM, mais même un hello world consomme 5 à 10 Mo de mémoire par instance
À l’inverse, Shopify utilise WASM pour les personnalisations côté serveur des boutiques et limite les binaires WASM à moins de 250 Ko ; à cette taille, il est difficile d’embarquer le moindre interpréteur
C’est pourquoi le langage « recommandé » est devenu AssemblyScript, comme expliqué ici : https://shopify.engineering/shopify-webassembly
Historiquement, cette situation vient du fait que WASM était un runtime très simple
Compiler du code C en WASM, comme on compilerait vers du code machine, était relativement facile, mais même si WebAssembly est lui-même une sorte d’interpréteur, il n’était pas simple d’y interpréter des langages de plus haut niveau
Aujourd’hui, avec la standardisation de nouvelles propositions comme la prise en charge du garbage collection et la prise en charge des exceptions, WebAssembly devient un interpréteur bien plus puissant, avec des fonctionnalités comme les structs, les tableaux et les références de fonctions
Jaws exploite cela pour transformer du code JS en code WASM, puis laisse WASM interpréter le code résultant sans moteur JS comme SpiderMonkey
En pratique, le binaire généré par Jaws pourrait probablement faire moins de 50 KB, contre 10 MB pour une approche qui consiste à compiler SpiderMonkey en WASM puis à exécuter le script au-dessus
L’utilisation mémoire devrait aussi diminuer fortement
Pour des entreprises comme Fastly, cela signifie pouvoir réduire l’utilisation mémoire et les coûts serveur d’un ordre de grandeur, et pour des entreprises comme Shopify, cela signifie pouvoir faire profiter les auteurs de plugins backend du code JavaScript existant, par exemple des paquets NPM et de l’écosystème JavaScript
On a l’impression que « exécuter du JS sans runtime de navigateur » approche
Porffor, Jaws, ou un autre projet finira sans doute par y parvenir
J’aime vraiment beaucoup cette approche
Au lieu d’essayer de générer directement un binaire, cibler directement WASM à la compilation permet de s’appuyer sur WASM GC et sur le support de l’asynchrone qui semble devoir arriver dans WASI 0.3
Comment gérez-vous les différences d’encodage des chaînes et les utilitaires associés ?
Si je comprends vaguement bien, WASM prend en charge UTF-8 alors que JS prend aussi en charge un UTF-16 potentiellement cassé
Comme pour un jeu d’instructions CPU, la machine abstraite WASM n’a pas de notion de chaîne ou d’encodage
Ce ne sont que des octets dans une mémoire linéaire, et on peut implémenter soi-même l’encodage voulu
WASM spécifie bien UTF-8 pour l’encodage des noms dans le format de fichier, mais cela n’a rien à voir avec la machine virtuelle du runtime
Certains appelleraient ça un compilateur
Quoi qu’il en soit, c’est du beau travail
Est-ce que c’est plus rapide que d’exécuter simplement le même code en JS, ou est-ce surtout pour l’interopérabilité avec d’autres langages ?
Les compilateurs JavaScript modernes optimisent déjà assez bien les chemins souvent exécutés grâce au JIT
L’objectif de ce projet est de permettre l’exécution de JavaScript dans un environnement sandbox WebAssembly
Par exemple, Shopify permet d’étendre du code backend avec WebAssembly, mais limite la taille du binaire à 250 KB
À cette taille, il est aujourd’hui difficile d’utiliser JavaScript, car même un interpréteur simple comme QuickJS atteint plusieurs MB une fois compilé en WASM