2 points par GN⁺ 2024-11-11 | 1 commentaires | Partager sur WhatsApp
  • 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

 
GN⁺ 2024-11-11
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

    • Porffor https://porffor.dev/ et Static Hermes https://hermesengine.dev/ semblent eux aussi adopter une approche par compilation
      Ce serait intéressant de les comparer à Jaws
    • Exact. Cela dit, pour être honnête, c’est surtout une utilisation astucieuse née du fait que je ne suis pas assez brillant pour écrire tout un interpréteur au-dessus de WASM
  • 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

    • Je n’ai toujours pas trouvé de poste Rust sans lien avec la crypto
      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
    • Le fait qu’un sujet soit survendu sur les réseaux sociaux et très visible ne veut pas forcément dire qu’il est largement utilisé
      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 await et les générateurs, les derniers concepts sémantiques difficiles seront réglés, et je prévois ensuite d’ajouter ces parties plus simples
      Il 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ée
      object.foo fonctionne, mais pas object["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

Le runtime utilisé par le projet est **uniquement WebAssembly**  
Le code généré se compose essentiellement d’environ 3 000 lignes de code WAT dans ce fichier [https://github.com/drogus/jaws/blob/main/src/wat/template.wat](<https://github.com/drogus/jaws/blob/main/src/wat/template.wat>;) et de la partie issue de la conversion du code JS de l’utilisateur  
Par exemple, pour un programme très simple comme `"console.log('foo')"`, toute la partie « générée » se limite à ceci : [https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…](<https://gist.github.com/drogus/1c49c25ed0b14804b2f27e10d2a79928/…;)  
En gros, cela prépare les arguments avec `new_static_string`, puis appelle `console.log`  
Pour l’instant, il faut un peu de glue code côté hôte, mais à terme il devrait être possible d’exécuter de tels binaires dans n’importe quel runtime prenant en charge WASIp2, WASM GC et la proposition de gestion des exceptions

La prise en charge des API Web ou des identifiants globaux propres à chaque environnement n’est pas encore implémentée, mais on peut expliquer comment cela fonctionnerait  
Les **API Node.js** devraient être prises en charge via WASI  
WASI est le standard qui permet à un programme WASM de communiquer avec le monde extérieur  
Il définit un ensemble de fonctions standard utilisables, par exemple, pour envoyer des requêtes HTTP, écrire sur STDOUT, lire/écrire des fichiers, etc.  
Ainsi, lorsqu’on atteint des API comme `fetch` ou `fs`, cela devrait fonctionner dans les runtimes qui prennent en charge WASI preview2  
Les navigateurs peuvent aussi le prendre en charge via un polyfill, mais dans ce cas le support des E/S devient plus spécifique  
Si l’on veut autoriser un programme WASM à lire ou écrire des fichiers, il faut fournir un mécanisme, par exemple pour stocker cela dans localStorage, utiliser une base de données SQLite compilée en WASM, ou même l’envoyer vers un service comme S3
  • 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

    • Je ne m’étais absolument pas rendu compte que le titre sonnait un peu bizarre
  • 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 ?

    • À ce stade, c’est difficile à dire, mais il me semble très peu probable que ce soit plus rapide que SpiderMonkey ou V8 avec le JIT activé
      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